feat(architect): provision persistent Docker container via reconcile (#1131) #1134

Merged
code-lead merged 2 commits from code-lead/1131 into main 2026-05-13 21:06:33 +00:00
Collaborator

Summary

  • Architect now has a persistent claude-hooks-architect-default container reconciled by reconcileOne / reconcileAll exactly like every other agent — no orchestration-layer special-casing. Hot lifecycle, shared claude-hooks:dev image, single bind-mount (the credentials dir → CLAUDE_CONFIG_DIR).
  • The architect chat path continues dispatching inline (host_mode=true). The container is a warm placeholder for the upcoming relay story; flipping /architect/chat onto docker exec becomes a routing change instead of a deploy.
  • host_mode is decoupled from container.enabled — derived from role === "host", matching the existing is_host_mode derivation in main.ts. Architect declares container.enabled: true, lifecycle: "hot" while staying host_mode for dispatch.
  • dockerRun / matchesDesired collapse the bind-mount set to a single credentials bind for host-mode instances (skip projects sub-bind, skip ~/.cursor). The named state volume is kept so the relay can use /state without a recreate.
  • New spec at specs/architect-dockerize.md, plus updates to docs/architect.md, docs/containers.md, and the project CLAUDE.md overview block.

Closes #1131.

Test plan

  • bun test apps/server/src/infrastructure/container/ — 56 tests pass, including 3 new ones for the host-mode singleton (created action, single-bind argv shape, no spurious projects-bind drift).
  • bun test apps/server/src/ — full server suite (3303 tests) passes.
  • just qa — typecheck + lint + format + sql-layer + paraglide + i18n + flow-schema all green (bun install was needed first to pull in a missing pre-existing react-resizable-panels dep on the web typecheck).
  • On the desktop after merge: just agents-sync should report created for architect-default; docker inspect claude-hooks-architect-default | jq '[.Mounts[] | select(.Type=="bind")] | length' should print 1; just restart leaves the container running; just containers-rebuild architect-default recreates only it.

🤖 Generated with Claude Code

## Summary - Architect now has a persistent `claude-hooks-architect-default` container reconciled by `reconcileOne` / `reconcileAll` exactly like every other agent — no orchestration-layer special-casing. Hot lifecycle, shared `claude-hooks:dev` image, single bind-mount (the credentials dir → `CLAUDE_CONFIG_DIR`). - The architect chat path **continues dispatching inline** (host_mode=true). The container is a warm placeholder for the upcoming relay story; flipping `/architect/chat` onto `docker exec` becomes a routing change instead of a deploy. - `host_mode` is decoupled from `container.enabled` — derived from `role === "host"`, matching the existing `is_host_mode` derivation in `main.ts`. Architect declares `container.enabled: true, lifecycle: "hot"` while staying host_mode for dispatch. - `dockerRun` / `matchesDesired` collapse the bind-mount set to a single credentials bind for host-mode instances (skip projects sub-bind, skip `~/.cursor`). The named state volume is kept so the relay can use `/state` without a recreate. - New spec at `specs/architect-dockerize.md`, plus updates to `docs/architect.md`, `docs/containers.md`, and the project `CLAUDE.md` overview block. Closes #1131. ## Test plan - [ ] `bun test apps/server/src/infrastructure/container/` — 56 tests pass, including 3 new ones for the host-mode singleton (created action, single-bind argv shape, no spurious projects-bind drift). - [ ] `bun test apps/server/src/` — full server suite (3303 tests) passes. - [ ] `just qa` — typecheck + lint + format + sql-layer + paraglide + i18n + flow-schema all green (`bun install` was needed first to pull in a missing pre-existing `react-resizable-panels` dep on the `web` typecheck). - [ ] On the desktop after merge: `just agents-sync` should report `created` for `architect-default`; `docker inspect claude-hooks-architect-default | jq '[.Mounts[] | select(.Type=="bind")] | length'` should print `1`; `just restart` leaves the container running; `just containers-rebuild architect-default` recreates only it. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
feat(architect): provision persistent Docker container via reconcile (#1131)
All checks were successful
qa / sql-layer-check (pull_request) Successful in 2m2s
qa / db-schema (pull_request) Successful in 2m6s
qa / i18n-string-check (pull_request) Successful in 2m8s
qa / dockerfile (pull_request) Successful in 2m10s
qa / qa-1 (pull_request) Successful in 8m44s
qa / qa (pull_request) Successful in 0s
cb2f563664
Bring the architect under the same per-agent container deployment model
as every other instance in the fleet — `claude-hooks-architect-default`
is now a hot, persistent singleton reconciled by `reconcileOne` /
`reconcileAll` exactly like code-lead, dev, reviewer, … with no
orchestration-layer special-casing.

The architect chat path keeps dispatching inline (host_mode=true). The
container is a warm placeholder for the upcoming relay story; flipping
chat onto `docker exec` against this container becomes a routing change
instead of a deploy.

Decouple `host_mode` from `container.enabled` by deriving it from
`role === "host"`, matching the existing `is_host_mode` derivation in
main.ts. Architect now declares `container.enabled: true,
lifecycle: "hot"` and stays host_mode for dispatch routing.

`dockerRun` / `matchesDesired` collapse the bind-mount set to a single
credentials bind for host-mode instances (no projects sub-bind, no
~/.cursor) so `docker inspect claude-hooks-architect-default` reports
exactly one Type=bind mount per the AC. The named state volume is kept
so the relay can use /state without a recreate.

New test coverage: `reconcileOne("architect-default")` emits 'created',
asserts the single-bind argv shape, and stays 'unchanged' on a
matching present container (regression for a spurious projects-bind
drift loop).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
reviewer requested changes 2026-05-13 20:41:41 +00:00
Dismissed
reviewer left a comment

Review — feat(architect): provision persistent Docker container via reconcile

Overall the implementation is clean: the minimalMount branch in dockerRun, the matchesDesired gate for the projects-bind check, and the host_mode: type.role === "host" re-derivation are all principled. The spec doc and test suite are thorough. One blocking correctness issue needs fixing before merge, plus two stale guard sites that should be updated in the same pass.


🔴 Blocking — three dispatch safety rails now silently let the architect through

resolveAgentByUser, resolveAgentByType, and resolveAgentByRole each contain:

if (type.container?.enabled === false) return null;

Their JSDoc is explicit: "webhook dispatch paths must never reach them". Before this PR that held because the architect declared container.enabled: false. Now that the architect declares container.enabled: true the guard is dead for it — all three functions return the full resolved agent for the architect.

Practical risk today is low (the architect Forgejo user will not be auto-assigned; no flow YAML should call resolve_agent with type: "architect"). But the contract stated in the JSDoc is now broken, dispatchable: false is not checked by any of these callers (janitor, pr-flow-impl, tail-pr-rebase-watchdog), and the relay story will make the architect more prominent. Fix now rather than carry the landmine.

Required fix — update all three guards to key on role rather than container.enabled:

// resolveAgentByUser / resolveAgentByType / resolveAgentByRole
if (type.role === "host") return null;

This is consistent with the host_mode re-derivation already applied in mergeAgent and keeps the semantic ("host-mode singletons are invisible to webhook dispatch") true regardless of their container state.


🟡 Medium — two stale guard sites anchored to container.enabled === false

1. Non-anthropic provider check (webhook-config.ts ~line 1522):

// container-bypass guard (mergeAgent's host_mode = container?.enabled === false)
if (chain[0].provider !== "anthropic" && container?.enabled === false) {

With container.enabled: true on the architect, this guard no longer fires for it. A future misconfiguration of provider: "cursor" on the architect would slip through without a load-time error. Update the condition and the stale comment to use role === "host" consistently.

2. Stale inline comment (same file, ~line 1518):

// container-bypass guard (mergeAgent's host_mode = container?.enabled === false)

Update to reflect the new derivation: host_mode = role === "host".


What's good

  • minimalMount in dockerRun — clean branch; the conditional mkdir and the spread into the args array are correctly in lockstep, so docker inspect reports exactly one Type=bind entry as the AC demands.
  • matchesDesired gate — correctly skips the projects-bind drift check for host_mode instances; prevents the spurious recreate-loop the third test exercises.
  • host_mode: type.role === "host" — principled unification with the pre-existing is_host_mode derivation in main.ts. Decouples dispatch routing from container provisioning cleanly.
  • Test suite — the three new scenarios (created, single-bind argv shape, unchanged on matching container) directly map to the AC. The volumeFlags bind-counting approach is readable and will catch regressions.
  • specs/architect-dockerize.md — honest about scope, in/out-of-scope items are clear, AC mirrors the issue checklist.
  • agents-default-config.tslifecycle: "hot" added alongside enabled: true; the decoupling comment is exactly what future readers need.

Summary

Fix the three resolveAgentBy* guards from container?.enabled === falserole === "host", and update the stale comment + provider guard in the same pass. Everything else is merge-ready.

## Review — feat(architect): provision persistent Docker container via reconcile Overall the implementation is clean: the `minimalMount` branch in `dockerRun`, the `matchesDesired` gate for the projects-bind check, and the `host_mode: type.role === "host"` re-derivation are all principled. The spec doc and test suite are thorough. One **blocking correctness issue** needs fixing before merge, plus two stale guard sites that should be updated in the same pass. --- ### 🔴 Blocking — three dispatch safety rails now silently let the architect through `resolveAgentByUser`, `resolveAgentByType`, and `resolveAgentByRole` each contain: ```ts if (type.container?.enabled === false) return null; ``` Their JSDoc is explicit: _"webhook dispatch paths must never reach them"_. Before this PR that held because the architect declared `container.enabled: false`. Now that the architect declares `container.enabled: true` the guard is dead for it — all three functions return the full resolved agent for the architect. Practical risk today is low (the architect Forgejo user will not be auto-assigned; no flow YAML should call `resolve_agent` with `type: "architect"`). But the contract stated in the JSDoc is now broken, `dispatchable: false` is **not** checked by any of these callers (janitor, pr-flow-impl, tail-pr-rebase-watchdog), and the relay story will make the architect more prominent. Fix now rather than carry the landmine. **Required fix** — update all three guards to key on `role` rather than `container.enabled`: ```ts // resolveAgentByUser / resolveAgentByType / resolveAgentByRole if (type.role === "host") return null; ``` This is consistent with the `host_mode` re-derivation already applied in `mergeAgent` and keeps the semantic ("host-mode singletons are invisible to webhook dispatch") true regardless of their container state. --- ### 🟡 Medium — two stale guard sites anchored to `container.enabled === false` **1. Non-anthropic provider check** (`webhook-config.ts` ~line 1522): ```ts // container-bypass guard (mergeAgent's host_mode = container?.enabled === false) if (chain[0].provider !== "anthropic" && container?.enabled === false) { ``` With `container.enabled: true` on the architect, this guard no longer fires for it. A future misconfiguration of `provider: "cursor"` on the architect would slip through without a load-time error. Update the condition and the stale comment to use `role === "host"` consistently. **2. Stale inline comment** (same file, ~line 1518): ``` // container-bypass guard (mergeAgent's host_mode = container?.enabled === false) ``` Update to reflect the new derivation: `host_mode = role === "host"`. --- ### ✅ What's good - **`minimalMount` in `dockerRun`** — clean branch; the conditional `mkdir` and the spread into the args array are correctly in lockstep, so `docker inspect` reports exactly one `Type=bind` entry as the AC demands. - **`matchesDesired` gate** — correctly skips the projects-bind drift check for `host_mode` instances; prevents the spurious recreate-loop the third test exercises. - **`host_mode: type.role === "host"`** — principled unification with the pre-existing `is_host_mode` derivation in `main.ts`. Decouples dispatch routing from container provisioning cleanly. - **Test suite** — the three new scenarios (created, single-bind argv shape, unchanged on matching container) directly map to the AC. The `volumeFlags` bind-counting approach is readable and will catch regressions. - **`specs/architect-dockerize.md`** — honest about scope, in/out-of-scope items are clear, AC mirrors the issue checklist. - **`agents-default-config.ts`** — `lifecycle: "hot"` added alongside `enabled: true`; the decoupling comment is exactly what future readers need. --- ### Summary Fix the three `resolveAgentBy*` guards from `container?.enabled === false` → `role === "host"`, and update the stale comment + provider guard in the same pass. Everything else is merge-ready.
fix(architect): key dispatch + provider guards on role instead of container.enabled
All checks were successful
qa / sql-layer-check (pull_request) Successful in 41s
qa / i18n-string-check (pull_request) Successful in 47s
qa / dockerfile (pull_request) Successful in 47s
qa / db-schema (pull_request) Successful in 56s
qa / qa-1 (pull_request) Successful in 7m39s
qa / qa (pull_request) Successful in 0s
82993f2995
Review feedback on #1131: the three resolveAgentBy* functions and the
parseProviderChain host-mode → anthropic guard all keyed on
`container?.enabled === false`. With the architect now declaring
`container.enabled: true` for its #1131 warm-placeholder container,
those gates fell silent — the architect Forgejo user would have become
dispatchable via webhooks, and a misconfigured `provider: deepseek` on
the architect would have slipped past load-time validation.

Move every host-mode safety rail onto `role === "host"`, matching
`mergeAgent`'s `host_mode` re-derivation. Container state is now
irrelevant to dispatch routing — the singleton stays invisible to
webhooks whether or not it has a container.

- `resolveAgentByUser` / `resolveAgentByType` / `resolveAgentByRole`:
  filter on `type.role === "host"` and refresh the JSDoc to explain
  the #1131 context.
- `parseProviderChain`: signature now takes `role: AgentRole = "code"`
  instead of `container: ContainerConfig | undefined`; the cross-cutting
  anthropic-tier-1 check fires when `role === "host"`. Updated both
  call sites (config loader, agent_type_config PUT handler).
- Drop the now-unused `getContainerConfigForProviderValidation` export.
- `webhook-config.ts:2546` `host-mode opt-out` token-file exemption keys
  on role for the same reason.
- New regression tests cover all three resolvers + the provider guard
  under the new `container.enabled: true, role: "host"` shape so the
  rail can't silently drift again.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Author
Collaborator

Thanks for the careful read — the dispatch landmine was real and worth fixing now. Pushed 82993f2 addressing all three points:

Blocking — resolver guards (webhook-config.ts):

  • resolveAgentByUser, resolveAgentByType, resolveAgentByRole now gate on type.role === "host" instead of type.container?.enabled === false. JSDoc on each is refreshed to explain the #1131 context (warm-placeholder container ≠ dispatchable).

Medium — provider chain + stale comment (webhook-config.ts:1517-1526):

  • The cross-cutting "host-mode → anthropic tier-1" check now keys on role. parseProviderChain's signature is (typeName, raw, role: AgentRole = "code")container was only used for that one branch, so I dropped it rather than thread an unused param. Comment + error message updated to reference role="host".
  • Both call sites updated: the loader passes the role parsed by parseTypeRouting; handleUpsertDispatch looks it up via roleForType(name) ?? "code" (DB-first, same fallback chain as the existing dispatch path).
  • Dropped the now-unused getContainerConfigForProviderValidation export.

Also caught while sweeping:

  • loadWebhookConfig's token-file forge-validator loop (line ~2546) had the same container?.enabled === false opt-out comment. Flipped to role === "host" with a #1131 reference.

Tests:

  • 3 new regression tests pin the new behaviour under the container.enabled: true, role: "host" shape — one per resolver, plus one for the provider-chain guard. Without the role-keyed gate, all four would have silently let the architect through.
  • Existing test name + comment updated ((container.enabled=false)(role: host)) so future readers don't get the wrong impression about what the rail keys on.

bun test apps/server/src/ — 3312 pass (was 3307; +5 new tests, no regressions). just qa green.

Re-requesting review.

Thanks for the careful read — the dispatch landmine was real and worth fixing now. Pushed `82993f2` addressing all three points: **Blocking — resolver guards (`webhook-config.ts`):** - `resolveAgentByUser`, `resolveAgentByType`, `resolveAgentByRole` now gate on `type.role === "host"` instead of `type.container?.enabled === false`. JSDoc on each is refreshed to explain the #1131 context (warm-placeholder container ≠ dispatchable). **Medium — provider chain + stale comment (`webhook-config.ts:1517-1526`):** - The cross-cutting "host-mode → anthropic tier-1" check now keys on role. `parseProviderChain`'s signature is `(typeName, raw, role: AgentRole = "code")` — `container` was only used for that one branch, so I dropped it rather than thread an unused param. Comment + error message updated to reference `role="host"`. - Both call sites updated: the loader passes the role parsed by `parseTypeRouting`; `handleUpsertDispatch` looks it up via `roleForType(name) ?? "code"` (DB-first, same fallback chain as the existing dispatch path). - Dropped the now-unused `getContainerConfigForProviderValidation` export. **Also caught while sweeping:** - `loadWebhookConfig`'s token-file forge-validator loop (line ~2546) had the same `container?.enabled === false` opt-out comment. Flipped to `role === "host"` with a #1131 reference. **Tests:** - 3 new regression tests pin the new behaviour under the `container.enabled: true, role: "host"` shape — one per resolver, plus one for the provider-chain guard. Without the role-keyed gate, all four would have silently let the architect through. - Existing test name + comment updated (`(container.enabled=false)` → `(role: host)`) so future readers don't get the wrong impression about what the rail keys on. `bun test apps/server/src/` — 3312 pass (was 3307; +5 new tests, no regressions). `just qa` green. Re-requesting review.
reviewer approved these changes 2026-05-13 21:03:55 +00:00
reviewer left a comment

Code Review — feat(architect): provision persistent Docker container via reconcile (#1131)

APPROVED — code is correct and all AC are met. CI (qa / qa-1) is still running; deferring merge to the post-CI path.


Overview

Two-commit PR:

  1. feat: Provisions claude-hooks-architect-default as a hot persistent singleton via the existing reconcileOne/reconcileAll machinery — no orchestration-level special-casing. The only architect-specific logic lives inside dockerRun: minimalMount: true collapses the bind set to the single credentials bind.
  2. fix: Migrates three dispatch-invisibility guards (resolveAgentByUser/ByType/ByRole) and one provider-chain guard (parseProviderChain) from container?.enabled === false to role === "host". This was a necessary follow-on because enabling container.enabled: true for architect silently opened all four gates that previously relied on that flag.

The approach is sound: role === "host" is semantically richer, already matches the is_host_mode derivation in main.ts, and decouples dispatch routing from the container lifecycle config.


Acceptance Criteria Verification

AC Status Evidence
reconcileAll() creates claude-hooks-architect-default when instance row exists reconcileOne test emits 'created'; reconcileAll delegates to it
reconcileOne("architect-default") via standard CRUD, no special-casing No new branch in orchestration layer; only dockerRun is parameterised
Persistent singleton (just restart leaves it running) lifecycle: "hot"--restart unless-stopped
Same claude-hooks:dev image No per-architect image override
Exactly one bind-mount (credentials dir → CLAUDE_CONFIG_DIR) minimalMount gate + test asserts binds.length === 1
No Workspace / process.cwd() / other host paths minimalMount skips projects + cursor spreads
docker inspect confirms one Type=bind Test filters on leading / to count binds
Standard bridge network Same dockerRun path as all other agents
just agents-sync / containers-rebuild / render-agent-env work No changes to those paths; architect goes through the same config-driven flow
Unit test: reconcileOne emits 'created' for absent container container-reconcile.test.ts:935
just qa passes 🟡 qa-1 in progress; all other jobs green

Code Quality

Strengths

  • const minimalMount = agent.host_mode === true is well-named and its single boolean cleanly gates both the mkdir pre-create and the two --volume spread args — easy to follow.
  • matchesDesired gated on agent.host_mode !== true is correctly paired with a dedicated "no spurious drift" regression test — the class of bug this guards against (infinite recreate loop) is exactly the one worth pinning.
  • The loadConfigWithArchitect() fixture in tests mirrors the exact production shape (container.enabled: true, lifecycle: "hot", role: "host") rather than a simplified stub — the tests exercise the real boot path.
  • getContainerConfigForProviderValidation is correctly removed; its only caller (handleUpsertDispatch) now resolves role directly via roleForType.
  • The state volume claude-hooks-architect-default-state:/state is preserved without adding to the bind count, keeping the relay story's migration to docker exec dep-free.

Minor observations (non-blocking)

  1. matchesDesired doesn't check the cursor bind for normal agents. A manually added cursor bind wouldn't be detected as drift. This is a pre-existing gap; the PR doesn't make it worse.
  2. Two-commit structure. The fix commit is a clean self-correction but would be tidier squashed before merge. The commit messages are clear enough that this is cosmetic only.
  3. roleForType(name) ?? "code" fallback in handleUpsertDispatch. Safe in practice (only "host" triggers the constraint), but worth knowing: a type added to agents.json but not yet flushed to the DB would silently pass the guard on a live PUT request. Unreachable in normal operation.

Summary

Solid implementation. The decoupling of host_mode from container.enabled is the right call and brings the two derivation paths (mergeAgent and main.ts) into alignment. Test coverage is thorough — all three resolver guards and the provider guard are regression-pinned with the exact container.enabled: true, role: "host" shape from this PR. Will merge once qa / qa-1 goes green.

## Code Review — feat(architect): provision persistent Docker container via reconcile (#1131) **APPROVED** — code is correct and all AC are met. CI (`qa / qa-1`) is still running; deferring merge to the post-CI path. --- ### Overview Two-commit PR: 1. **feat**: Provisions `claude-hooks-architect-default` as a hot persistent singleton via the existing `reconcileOne`/`reconcileAll` machinery — no orchestration-level special-casing. The only architect-specific logic lives inside `dockerRun`: `minimalMount: true` collapses the bind set to the single credentials bind. 2. **fix**: Migrates three dispatch-invisibility guards (`resolveAgentByUser/ByType/ByRole`) and one provider-chain guard (`parseProviderChain`) from `container?.enabled === false` to `role === "host"`. This was a necessary follow-on because enabling `container.enabled: true` for architect silently opened all four gates that previously relied on that flag. The approach is sound: `role === "host"` is semantically richer, already matches the `is_host_mode` derivation in `main.ts`, and decouples dispatch routing from the container lifecycle config. --- ### Acceptance Criteria Verification | AC | Status | Evidence | |----|--------|----------| | `reconcileAll()` creates `claude-hooks-architect-default` when instance row exists | ✅ | `reconcileOne` test emits `'created'`; `reconcileAll` delegates to it | | `reconcileOne("architect-default")` via standard CRUD, no special-casing | ✅ | No new branch in orchestration layer; only `dockerRun` is parameterised | | Persistent singleton (`just restart` leaves it running) | ✅ | `lifecycle: "hot"` → `--restart unless-stopped` | | Same `claude-hooks:dev` image | ✅ | No per-architect image override | | Exactly one bind-mount (credentials dir → `CLAUDE_CONFIG_DIR`) | ✅ | `minimalMount` gate + test asserts `binds.length === 1` | | No Workspace / `process.cwd()` / other host paths | ✅ | `minimalMount` skips projects + cursor spreads | | `docker inspect` confirms one `Type=bind` | ✅ | Test filters on leading `/` to count binds | | Standard bridge network | ✅ | Same `dockerRun` path as all other agents | | `just agents-sync` / `containers-rebuild` / `render-agent-env` work | ✅ | No changes to those paths; architect goes through the same config-driven flow | | Unit test: `reconcileOne` emits `'created'` for absent container | ✅ | `container-reconcile.test.ts:935` | | `just qa` passes | 🟡 | `qa-1` in progress; all other jobs green | --- ### Code Quality **Strengths** - `const minimalMount = agent.host_mode === true` is well-named and its single boolean cleanly gates both the `mkdir` pre-create and the two `--volume` spread args — easy to follow. - `matchesDesired` gated on `agent.host_mode !== true` is correctly paired with a dedicated "no spurious drift" regression test — the class of bug this guards against (infinite recreate loop) is exactly the one worth pinning. - The `loadConfigWithArchitect()` fixture in tests mirrors the exact production shape (`container.enabled: true, lifecycle: "hot", role: "host"`) rather than a simplified stub — the tests exercise the real boot path. - `getContainerConfigForProviderValidation` is correctly removed; its only caller (`handleUpsertDispatch`) now resolves role directly via `roleForType`. - The state volume `claude-hooks-architect-default-state:/state` is preserved without adding to the bind count, keeping the relay story's migration to `docker exec` dep-free. **Minor observations (non-blocking)** 1. **`matchesDesired` doesn't check the cursor bind for normal agents.** A manually added cursor bind wouldn't be detected as drift. This is a pre-existing gap; the PR doesn't make it worse. 2. **Two-commit structure.** The fix commit is a clean self-correction but would be tidier squashed before merge. The commit messages are clear enough that this is cosmetic only. 3. **`roleForType(name) ?? "code"` fallback in `handleUpsertDispatch`.** Safe in practice (only `"host"` triggers the constraint), but worth knowing: a type added to agents.json but not yet flushed to the DB would silently pass the guard on a live PUT request. Unreachable in normal operation. --- ### Summary Solid implementation. The decoupling of `host_mode` from `container.enabled` is the right call and brings the two derivation paths (`mergeAgent` and `main.ts`) into alignment. Test coverage is thorough — all three resolver guards and the provider guard are regression-pinned with the exact `container.enabled: true, role: "host"` shape from this PR. Will merge once `qa / qa-1` goes green.
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
charles/agent-hooks!1134
No description provided.