commits
Garden agents push GardenReport (version) and GardenSeedsReport
(profile generations) over the channel. Persist-only behavior meant
the show page only refreshed on full reload.
Add GardenPubSub broadcasting on garden:view:<sid> after each report
ingestion, and handle the events in GardenLive.Show to refresh the
garden record and seed generations live.
Ticket: sow-180
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Name the lateral-join binding `:latest_deployment` and expose
`deploy_result` as a Flop join_field sourced from it, then wire the
Gardens index Deploy column to that field so headers become sort links.
Sort order is the raw enum string (alphabetic: failure, partial,
success), not by severity. Gardens with no deployment use Postgres
defaults (NULLs last on ASC, first on DESC).
sow-178
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replace :gen_tcp.listen + close (whose socket-file lifecycle varies
across platforms) with a plain File.touch!. Connecting to a regular
file at {:local, path} reliably returns :econnrefused on Linux, which
is what the test needs to verify.
sow-167
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The test did not await the spawned Task.Supervisor child, causing the
test process (DB sandbox owner) to exit while the task still held a
connection. Apply the same Process.monitor + assert_receive pattern used
by handle_deployment_request/2 tests.
sow-154
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Users can show/hide columns via a Columns dropdown above the gardens
table. Selection is persisted in the URL as ?cols=... so views are
shareable and survive reload. The existing :version field is exposed as
the first togglable column; Name is locked on so the table can't be
emptied.
Column visibility and Flop sort/page state coexist because cols= is
baked into the path passed to the table and pagination components;
Flop.Phoenix.build_path preserves existing query params on the base.
feat(sow-177): move Columns toggle to header as icon button
Relocate the Columns toggle into the page header's :actions slot as an
icon-only button (hero-view-columns) with aria-label/title. Reclaims the
vertical space previously taken by a dedicated row above the table.
sow-177
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Garden socket was clearing credentials and re-registering whenever
reauthentication failed, including for connection/transport errors.
Now only re-register when the server actually rejects the credentials
(4xx response); bubble up transport and 5xx errors so the socket
reconnect backoff handles the retry with existing credentials.
sow-173
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Garden now sends a structured GardenReport (version) over a new
garden:report channel message on private-channel join. Server persists
it on the gardens row and the garden show page renders a Details
section with the reported version.
sow-172
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
user → user_, user_token → utok_, nix_cache → nxc_,
forge_connection → forg_, repository → repo_
access_token left unprefixed by design (SID is part of the full token).
nix eval request left as-is.
sow-159
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Make action_to_mode seed-type-aware: service activate maps to
"restart" mode per spec, not "switch"
- Add specific UI error messages for policy_denied and
confirmation_required instead of generic "Deployment failed"
- Add tests for deploy_subscription/2 policy denial and confirmation
paths
- Add from_legacy + poll_on_connect round-trip tests through evaluate
- Add service seed type activation mode test
sow-163
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Garden now uses policy rules for all deployment decisions:
- Add Policy.highest_permitted_action/4 for trigger-agnostic action resolution
- Replace poll_on_connect field filter with Policy.evaluate in Lifecycle
- Add policy evaluation in Scheduler before deploying (deny logs warning)
- Replace Garden.Seed.activation_mode with policy-derived action in Deployer
- Replace reboot_reason logic: restart permitted by policy + boot profile changed
- Add from_legacy conversion with deprecation warning in config preprocessing
- Stage-only policy skips activation (download only)
sow-163
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Policy rules now support a name field for better config ergonomics,
especially in Nix where attrsets are easier to merge and override than
arrays. Config preprocessing converts map-format policy to array with
names injected, matching the existing subscription naming pattern.
Policy is now a map (keyed by rule name) in the client-side OpenAPI
schema, matching Nix attrset ergonomics. The server continues to store
policy as embeds_many (list) with list<->map conversion at the
registration boundary.
- Change Subscription.policy from array to object with additionalProperties
- Remove name from Policy schema (name is the map key)
- Add normalize_rules/1 to evaluator to handle both map and list formats
- from_legacy/1 now returns a map
- Remove preprocess_policy config preprocessor (schema handles it natively)
- Convert list->map when casting server subscription back to client struct
sow-166
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Server now uses policy rules for all deployment decisions:
- Add Policy.from_legacy/1 to convert old subscription fields to policy rules
- Server converts old-format subscriptions at registration time
- Replace within_window? with Policy.evaluate in DeploySubscription worker
- Replace allow_realtime filter with policy-based realtime trigger check
- Add policy evaluation gate in deploy_subscription/2 entry point
- Add user_retry and poll_on_connect to deployment_event_reason enum
- Change retry reason from :retry to :user_retry
- Display policy rules on subscription show page
- Update all affected tests
Old gardens without policy field continue to work via from_legacy conversion.
sow-162
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Build the foundation for the deployment policy system:
- Add Policy.Rule OpenAPI schema (actions, triggers, window, confirm)
- Add shared evaluator: Policy.evaluate/5 and Policy.trigger_for_reason/1
- Window evaluation with overnight span support (subsumes sow-160)
- Make Window tz field optional for policy window reuse
- Add policy field to Subscription schema (client + server)
- DB migration: add policy column (jsonb) to subscriptions table
- Full unit tests for evaluator (49 tests)
No behavior change — existing code paths untouched.
sow-161
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Show event activity timeline on deployment detail page and trigger
reason column on deployment index. Events display human-readable
descriptions, timestamps, and actor SIDs.
sow-155
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace retried_by_user_id/retried_at on deployments with a
deployment_events table that records created/canceled events with
reason and actor_sid at each deployment lifecycle point.
sow-151
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
These guarded against pre-0.8.0 config formats (list subscriptions,
deployment_profiles). Since 0.9.0 requires 0.8.0+, they are no longer
needed.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
With Boruta-only socket auth and API-based registration, local_sid
and the garden:hello channel handshake are no longer needed.
Removes:
- garden:hello handler and do_handle_hello from GardenChannel
- get_garden/2 hello-matching logic from Garden
- maybe_provision_oauth_client (only used by hello flow)
- get_garden_local_sid functions and delegates
- local_sid from Garden schema, changeset, JSON derive, and UI
- local_sid from Garden.Storage and default generation
- SowerClient.GardenHello schema module
- Dead authorize_private_join local_sid clause
- DB migration to drop local_sid column from gardens
sow-150, sow-153
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
name was added in 0.8.0 as optional; now that all gardens send it,
make it required in SowerClient.Orchestration.Subscription. The
server-side create_subscription retains its seed_name fallback for
web UI and fixture use.
Updates contract baseline and all test fixtures to include name.
sow-122
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Garden socket now only accepts Boruta OAuth tokens. Removes:
- AccessToken-based authentication path from GardenSocket
- Query param token fallback (extract_token_from_params)
- migrate_agent_sid from Garden.Storage (all gardens on 0.8.0+)
Updates all channel tests to use Boruta-authenticated connections
via a new create_garden_with_oauth helper.
Also removes the channel-based garden registration test since
registration now happens exclusively via the API.
sow-136, sow-148
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Remove migrate_agent_sid/1 from Garden.Storage (all gardens are on 0.8.0+)
- Narrow garden:register permission from all(Garden) to create(Garden)
- Update socket auth check from read? to create? to match narrowed permission
sow-132
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Remove all backwards-compatible "agent:*" channel handlers, socket path,
broadcast duplicates, permission role, and old SowerClient type aliases
introduced during the Agent → Garden rename.
Also removes the deprecated DeploymentLogUploadRequest handler that was
already returning {:error, :deprecated}.
Includes a migration to update any existing agent:register access tokens
to garden:register.
sow-84
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The rekey flow only handled a corrupted state (garden exists but OAuth
client doesn't) that isn't a normal operational scenario. Simplify the
client-side fallback: when reauthentication fails, clear credentials
and re-register as a new garden.
Removes: rekey API endpoint, GardenRekey schema, rekey_garden/2,
Registration.rekey/3, and all client-side rekey logic.
sow-157
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Narrow token invalidation to only fire when the server returns 401/403
(upgrade_failure), indicating the token or OAuth client is invalid.
Normal disconnects (server restart, network blip) no longer force an
unnecessary HTTP reauthentication.
sow-157
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When a garden is disconnected (e.g. server rejected auth because the
OAuth client was deleted), the cached access token is invalidated.
This forces the next reconnect to go through reauthenticate → rekey
instead of reusing a token the server will reject again.
sow-157
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When a garden has stored credentials but the OAuth client no longer
exists on the server, reauthentication fails. Previously this was a
hard error that left the garden retrying forever. Now it falls through
to the rekey flow, which can recover the garden's identity.
If the rekey also fails with garden_not_found (garden was deleted),
it falls through to fresh registration.
sow-157
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Gardens with stored credentials that fail reauthentication no longer
silently re-register as new gardens. Instead they attempt a rekey via
the new POST /api/v1/gardens/:sid/rekey endpoint, which creates a new
OAuth client for the existing garden identity.
Server-side changes:
- Add rekey_garden/2 to Sower.Orchestration.Garden
- Add rekey action to GardenController with garden:register permission
- Rescue ArgumentError in GardenSocket when Boruta token references a
deleted OAuth client
Client-side changes:
- Add SowerClient.Registration.rekey/3
- Add try_http_rekey path in resolve_connect_token when garden has a
stored garden_sid but no usable credentials
- Fresh registration only happens for truly new gardens (no garden_sid)
sow-157
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When the garden process starts and cannot obtain a token (e.g. server
not yet ready), it now starts with an unconnected socket and schedules
reconnection with backoff, instead of returning :ignore which left
the garden permanently stranded.
Token resolution functions now return {:ok, token}/{:error, reason}
tuples instead of token/nil, so failures propagate clearly through
build_connect_config and do_connect.
sow-158
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace the dynamic add/remove dropdown pattern for permissions with
simple checkboxes. Fix modal and form dark mode backgrounds (zinc-300
was too bright, now zinc-800) and text color.
SOW-31
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Garden client no longer sends garden:hello over websocket. After HTTP
registration it joins the private channel directly with stored
garden_sid.
Server channel join now dispatches on access_token type: boruta-
authenticated gardens are authorized by matching garden_id from the
OAuth context, while legacy registration-token gardens still use
local_sid matching.
sow-149
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Run reconcile_deployments in a background task instead of blocking the
channel process, preventing assert_reply timeouts in slow environments
- Drain TaskSupervisor children in channel_case on_exit to prevent
sandbox teardown while tasks are still running
- Increase assert_reply timeouts from 100ms default to 1000ms
- Use wait_until_succeeds for garden deploy RPC in e2e test to handle
subscription sync race
sow-125
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Use progressive backoff delays (200ms, 500ms, 1s, 2s) when the garden
socket disconnects, instead of immediately retrying. Resets the backoff
counter on successful connection.
sow-142
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Garden client no longer falls back to sending the registration token
over the websocket. HTTP registration is now the only path for new
gardens without stored OAuth credentials.
sow-149
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add POST /api/v1/gardens/register endpoint so gardens can register via
HTTP before connecting the websocket, moving registration tokens off
the websocket path.
Server: new GardenController with OpenApiSpex operation, permission
check consistent with other API controllers, delegates to existing
register_new_garden/1.
Client: SowerClient.Registration HTTP client module, Garden.Socket
tries HTTP registration before falling back to websocket registration
token for backward compat.
sow-149
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Move auth token from URL query param to x-auth-token header (sow-147)
Server accepts both header and query param for backward compat.
Garden client now sends via header only.
- Remove longpoll transport from garden socket (sow-144)
- Use Base.decode64 instead of decode64! for untrusted tokens (sow-145)
- Use exact scope matching instead of String.contains? (sow-146)
sow-144, sow-145, sow-146, sow-147
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace the unmaintained Durable workflow library (GitHub-only dep) with
Oban for the RealtimeDeploy workflow. Improves the design by fanning out
to per-subscription deploy jobs instead of retrying the entire batch.
sow-143
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Garden agents push GardenReport (version) and GardenSeedsReport
(profile generations) over the channel. Persist-only behavior meant
the show page only refreshed on full reload.
Add GardenPubSub broadcasting on garden:view:<sid> after each report
ingestion, and handle the events in GardenLive.Show to refresh the
garden record and seed generations live.
Ticket: sow-180
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Name the lateral-join binding `:latest_deployment` and expose
`deploy_result` as a Flop join_field sourced from it, then wire the
Gardens index Deploy column to that field so headers become sort links.
Sort order is the raw enum string (alphabetic: failure, partial,
success), not by severity. Gardens with no deployment use Postgres
defaults (NULLs last on ASC, first on DESC).
sow-178
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replace :gen_tcp.listen + close (whose socket-file lifecycle varies
across platforms) with a plain File.touch!. Connecting to a regular
file at {:local, path} reliably returns :econnrefused on Linux, which
is what the test needs to verify.
sow-167
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The test did not await the spawned Task.Supervisor child, causing the
test process (DB sandbox owner) to exit while the task still held a
connection. Apply the same Process.monitor + assert_receive pattern used
by handle_deployment_request/2 tests.
sow-154
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Users can show/hide columns via a Columns dropdown above the gardens
table. Selection is persisted in the URL as ?cols=... so views are
shareable and survive reload. The existing :version field is exposed as
the first togglable column; Name is locked on so the table can't be
emptied.
Column visibility and Flop sort/page state coexist because cols= is
baked into the path passed to the table and pagination components;
Flop.Phoenix.build_path preserves existing query params on the base.
feat(sow-177): move Columns toggle to header as icon button
Relocate the Columns toggle into the page header's :actions slot as an
icon-only button (hero-view-columns) with aria-label/title. Reclaims the
vertical space previously taken by a dedicated row above the table.
sow-177
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Garden socket was clearing credentials and re-registering whenever
reauthentication failed, including for connection/transport errors.
Now only re-register when the server actually rejects the credentials
(4xx response); bubble up transport and 5xx errors so the socket
reconnect backoff handles the retry with existing credentials.
sow-173
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Garden now sends a structured GardenReport (version) over a new
garden:report channel message on private-channel join. Server persists
it on the gardens row and the garden show page renders a Details
section with the reported version.
sow-172
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Make action_to_mode seed-type-aware: service activate maps to
"restart" mode per spec, not "switch"
- Add specific UI error messages for policy_denied and
confirmation_required instead of generic "Deployment failed"
- Add tests for deploy_subscription/2 policy denial and confirmation
paths
- Add from_legacy + poll_on_connect round-trip tests through evaluate
- Add service seed type activation mode test
sow-163
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Garden now uses policy rules for all deployment decisions:
- Add Policy.highest_permitted_action/4 for trigger-agnostic action resolution
- Replace poll_on_connect field filter with Policy.evaluate in Lifecycle
- Add policy evaluation in Scheduler before deploying (deny logs warning)
- Replace Garden.Seed.activation_mode with policy-derived action in Deployer
- Replace reboot_reason logic: restart permitted by policy + boot profile changed
- Add from_legacy conversion with deprecation warning in config preprocessing
- Stage-only policy skips activation (download only)
sow-163
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Policy rules now support a name field for better config ergonomics,
especially in Nix where attrsets are easier to merge and override than
arrays. Config preprocessing converts map-format policy to array with
names injected, matching the existing subscription naming pattern.
Policy is now a map (keyed by rule name) in the client-side OpenAPI
schema, matching Nix attrset ergonomics. The server continues to store
policy as embeds_many (list) with list<->map conversion at the
registration boundary.
- Change Subscription.policy from array to object with additionalProperties
- Remove name from Policy schema (name is the map key)
- Add normalize_rules/1 to evaluator to handle both map and list formats
- from_legacy/1 now returns a map
- Remove preprocess_policy config preprocessor (schema handles it natively)
- Convert list->map when casting server subscription back to client struct
sow-166
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Server now uses policy rules for all deployment decisions:
- Add Policy.from_legacy/1 to convert old subscription fields to policy rules
- Server converts old-format subscriptions at registration time
- Replace within_window? with Policy.evaluate in DeploySubscription worker
- Replace allow_realtime filter with policy-based realtime trigger check
- Add policy evaluation gate in deploy_subscription/2 entry point
- Add user_retry and poll_on_connect to deployment_event_reason enum
- Change retry reason from :retry to :user_retry
- Display policy rules on subscription show page
- Update all affected tests
Old gardens without policy field continue to work via from_legacy conversion.
sow-162
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Build the foundation for the deployment policy system:
- Add Policy.Rule OpenAPI schema (actions, triggers, window, confirm)
- Add shared evaluator: Policy.evaluate/5 and Policy.trigger_for_reason/1
- Window evaluation with overnight span support (subsumes sow-160)
- Make Window tz field optional for policy window reuse
- Add policy field to Subscription schema (client + server)
- DB migration: add policy column (jsonb) to subscriptions table
- Full unit tests for evaluator (49 tests)
No behavior change — existing code paths untouched.
sow-161
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
With Boruta-only socket auth and API-based registration, local_sid
and the garden:hello channel handshake are no longer needed.
Removes:
- garden:hello handler and do_handle_hello from GardenChannel
- get_garden/2 hello-matching logic from Garden
- maybe_provision_oauth_client (only used by hello flow)
- get_garden_local_sid functions and delegates
- local_sid from Garden schema, changeset, JSON derive, and UI
- local_sid from Garden.Storage and default generation
- SowerClient.GardenHello schema module
- Dead authorize_private_join local_sid clause
- DB migration to drop local_sid column from gardens
sow-150, sow-153
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
name was added in 0.8.0 as optional; now that all gardens send it,
make it required in SowerClient.Orchestration.Subscription. The
server-side create_subscription retains its seed_name fallback for
web UI and fixture use.
Updates contract baseline and all test fixtures to include name.
sow-122
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Garden socket now only accepts Boruta OAuth tokens. Removes:
- AccessToken-based authentication path from GardenSocket
- Query param token fallback (extract_token_from_params)
- migrate_agent_sid from Garden.Storage (all gardens on 0.8.0+)
Updates all channel tests to use Boruta-authenticated connections
via a new create_garden_with_oauth helper.
Also removes the channel-based garden registration test since
registration now happens exclusively via the API.
sow-136, sow-148
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Remove all backwards-compatible "agent:*" channel handlers, socket path,
broadcast duplicates, permission role, and old SowerClient type aliases
introduced during the Agent → Garden rename.
Also removes the deprecated DeploymentLogUploadRequest handler that was
already returning {:error, :deprecated}.
Includes a migration to update any existing agent:register access tokens
to garden:register.
sow-84
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The rekey flow only handled a corrupted state (garden exists but OAuth
client doesn't) that isn't a normal operational scenario. Simplify the
client-side fallback: when reauthentication fails, clear credentials
and re-register as a new garden.
Removes: rekey API endpoint, GardenRekey schema, rekey_garden/2,
Registration.rekey/3, and all client-side rekey logic.
sow-157
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Narrow token invalidation to only fire when the server returns 401/403
(upgrade_failure), indicating the token or OAuth client is invalid.
Normal disconnects (server restart, network blip) no longer force an
unnecessary HTTP reauthentication.
sow-157
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When a garden is disconnected (e.g. server rejected auth because the
OAuth client was deleted), the cached access token is invalidated.
This forces the next reconnect to go through reauthenticate → rekey
instead of reusing a token the server will reject again.
sow-157
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When a garden has stored credentials but the OAuth client no longer
exists on the server, reauthentication fails. Previously this was a
hard error that left the garden retrying forever. Now it falls through
to the rekey flow, which can recover the garden's identity.
If the rekey also fails with garden_not_found (garden was deleted),
it falls through to fresh registration.
sow-157
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Gardens with stored credentials that fail reauthentication no longer
silently re-register as new gardens. Instead they attempt a rekey via
the new POST /api/v1/gardens/:sid/rekey endpoint, which creates a new
OAuth client for the existing garden identity.
Server-side changes:
- Add rekey_garden/2 to Sower.Orchestration.Garden
- Add rekey action to GardenController with garden:register permission
- Rescue ArgumentError in GardenSocket when Boruta token references a
deleted OAuth client
Client-side changes:
- Add SowerClient.Registration.rekey/3
- Add try_http_rekey path in resolve_connect_token when garden has a
stored garden_sid but no usable credentials
- Fresh registration only happens for truly new gardens (no garden_sid)
sow-157
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When the garden process starts and cannot obtain a token (e.g. server
not yet ready), it now starts with an unconnected socket and schedules
reconnection with backoff, instead of returning :ignore which left
the garden permanently stranded.
Token resolution functions now return {:ok, token}/{:error, reason}
tuples instead of token/nil, so failures propagate clearly through
build_connect_config and do_connect.
sow-158
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Garden client no longer sends garden:hello over websocket. After HTTP
registration it joins the private channel directly with stored
garden_sid.
Server channel join now dispatches on access_token type: boruta-
authenticated gardens are authorized by matching garden_id from the
OAuth context, while legacy registration-token gardens still use
local_sid matching.
sow-149
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Run reconcile_deployments in a background task instead of blocking the
channel process, preventing assert_reply timeouts in slow environments
- Drain TaskSupervisor children in channel_case on_exit to prevent
sandbox teardown while tasks are still running
- Increase assert_reply timeouts from 100ms default to 1000ms
- Use wait_until_succeeds for garden deploy RPC in e2e test to handle
subscription sync race
sow-125
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add POST /api/v1/gardens/register endpoint so gardens can register via
HTTP before connecting the websocket, moving registration tokens off
the websocket path.
Server: new GardenController with OpenApiSpex operation, permission
check consistent with other API controllers, delegates to existing
register_new_garden/1.
Client: SowerClient.Registration HTTP client module, Garden.Socket
tries HTTP registration before falling back to websocket registration
token for backward compat.
sow-149
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Move auth token from URL query param to x-auth-token header (sow-147)
Server accepts both header and query param for backward compat.
Garden client now sends via header only.
- Remove longpoll transport from garden socket (sow-144)
- Use Base.decode64 instead of decode64! for untrusted tokens (sow-145)
- Use exact scope matching instead of String.contains? (sow-146)
sow-144, sow-145, sow-146, sow-147
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>