feat(tess): add generic interaction CLI #731

Merged
jason.woltje merged 1 commits from feat/tess-cli into main 2026-07-13 05:52:42 +00:00
Owner

Refs #709

Task: TESS-M3-002 / TESS-CLI-001

Adds a configured-name-driven interaction CLI (chat, status, sessions, tree, attach, send, stop, health, recover) and authenticated gateway boundary. Session mutations derive actor scope server-side, use durable identity/recovery, route through the provider registry, and require exact-action approval for stop.

Name-as-config: command surface is mosaic interaction; the instance is supplied by --agent or MOSAIC_AGENT_NAME. No instance-specific command literal is hardcoded; tests cover Nova.

Verification: fresh node_modules install; pnpm turbo run typecheck lint test --force; pnpm typecheck; pnpm lint; pnpm format:check; pnpm test.

Refs #709 Task: TESS-M3-002 / TESS-CLI-001 Adds a configured-name-driven interaction CLI (chat, status, sessions, tree, attach, send, stop, health, recover) and authenticated gateway boundary. Session mutations derive actor scope server-side, use durable identity/recovery, route through the provider registry, and require exact-action approval for stop. Name-as-config: command surface is `mosaic interaction`; the instance is supplied by --agent or MOSAIC_AGENT_NAME. No instance-specific command literal is hardcoded; tests cover Nova. Verification: fresh node_modules install; pnpm turbo run typecheck lint test --force; pnpm typecheck; pnpm lint; pnpm format:check; pnpm test.
jason.woltje added 1 commit 2026-07-13 05:43:34 +00:00
feat(tess): add generic interaction CLI
All checks were successful
ci/woodpecker/pr/ci Pipeline was successful
de74e46a06
Author
Owner

Independent review — PR #731 feat(tess): add generic interaction CLI

Reviewer: [W-jarvis:reviewer-sonnet] (independent, non-author of LANE diskhygiene-terra)
Head reviewed: de74e46a0640c44c6b7294c24669496980761c92 (confirmed via API immediately before review and unchanged throughout)

1. Subcommand surface — VERIFIED

All nine required verbs are registered and each does real work against the gateway API (no stubs):

  • packages/mosaic/src/commands/interaction.ts:64 statusfetchInteractionStatus
  • interaction.ts:73 healthfetchInteractionHealth
  • interaction.ts:80 sessions <provider>fetchInteractionSessions
  • interaction.ts:94 tree <provider>fetchInteractionTree
  • interaction.ts:106 attach <sessionId>attachInteractionSession
  • interaction.ts:121 send <sessionId> <message>sendInteractionMessage
  • interaction.ts:146 chat <sessionId> <message>sendInteractionMessage (conversational alias of send, same real dispatch path — not a stub)
  • interaction.ts:170 stop <sessionId>stopInteractionSession, --approval required
  • interaction.ts:187 recover <sessionId>recoverInteractionSession
    Test packages/mosaic/src/commands/interaction.test.ts:6-21 asserts exactly these 9 subcommands are registered.
    Non-blocking nit: interaction.ts:120-143 builds const send = … then immediately does void send; purely to silence an unused-var lint rule — harmless but a little odd; a plain statement without the binding would read cleaner.

2. Reuse of merged M1/M2 (no re-invention) — VERIFIED

  • recoverapps/gateway/src/agent/interaction.controller.ts:137-156 calls TessDurableSessionService.recoverProviderSession, which calls the pre-existing DurableSessionCoordinator.recover (tess-durable-session.service.ts:79-83) — the M2 durable coordinator, not a new mechanism.
  • attachinteraction.controller.ts:63-85 obtains identity via durable.getSnapshot (new thin wrapper added in this PR at tess-durable-session.service.ts:67-77, itself built from the pre-existing coordinator.snapshot + assertScope) then calls RuntimeProviderService.attach, the same registry execute() path used by every other runtime verb (runtime-provider-registry.service.ts:216-231).
  • send/stopsend (interaction.controller.ts:87-112) uses queueProviderSend + dispatchProviderOutbox (pre-existing M2 outbox/idempotency path, tess-durable-session.service.ts:26-63), which internally calls RuntimeProviderService.sendMessage. stop (interaction.controller.ts:114-135) calls RuntimeProviderService.terminate, which internally consume()s a durable exact-action approval (runtime-provider-registry.service.ts:251-276) before ever reaching the provider — a hard 403 (ForbiddenException('Exact-action approval is required'), interaction.controller.ts:123-124) if --approval is missing. No ad-hoc bypass found; send is not gated by approval, which is consistent with the pre-existing registry design (only terminate consumes an approval token) and is not something this PR changed.
  • Actor-scope/session-ownership binding (TESS-SEC-003): every mutating/read handler calls assertConfiguredAgent then durable.getSnapshot/assertSessionAgent before touching the runtime (interaction.controller.ts:42,56,71,78,95,101,122,127,144,147), and getSnapshot/queueProviderSend/dispatchProviderOutbox all funnel through the existing assertScope check comparing context.actorScope to the durable session's ownerId/tenantId (tess-durable-session.service.ts:88-96).

3. Credential-safe status/health — VERIFIED

  • status (interaction.ts:64-69) hits the pre-existing /api/providers/status endpoint, unmodified by this PR (apps/gateway/src/agent/providers.controller.ts:38-48), which already reduces provider errors to a stable errorCode and exposes effectivePolicy via getEffectivePolicyStatus() — the M2-001 credential-safe pattern, reused rather than reinvented.
  • health (interaction.ts:73-77) hits the pre-existing global /health/ready (apps/gateway/src/health/health.controller.ts:11-14), which returns only {status:'ready'} — no config/credential surface by construction.
  • Grepped all new/changed files (interaction.ts, interaction.test.ts, gateway-api.ts additions, interaction.controller.ts) for secret/token/credential literals — none found; only session cookies are forwarded as opaque headers, never logged or printed.

4. NAME-AS-CONFIG invariant — VERIFIED

  • packages/mosaic/src/commands/interaction.ts:21-25 (configuredAgentName) derives the instance identity solely from --agent or MOSAIC_AGENT_NAME; it throws if neither is set. No literal tess command or identity appears anywhere in the new CLI/controller code.
  • Server-side enforcement is symmetric: apps/gateway/src/agent/interaction.controller.ts:176-181 (assertConfiguredAgent) rejects any request whose URL :agentName doesn't match the server's own MOSAIC_AGENT_NAME env var — this is what prevents a client from spoofing a different configured identity, not a hardcoded name check.
  • Explicit test proving a differently-named instance works with zero code change: apps/gateway/src/agent/interaction.controller.test.ts:7-22 sets MOSAIC_AGENT_NAME='Nova', calls controller.sessions('Nova', …) and gets a real result, then calls controller.sessions('Other', …) and gets rejected — proving the exact generic/config-driven behavior required, using a non-tess instance name.
  • Grep audit: grep -in tess packages/mosaic/src/commands/interaction.ts packages/mosaic/src/commands/interaction.test.ts packages/mosaic/src/tui/gateway-api.ts → zero hits. In apps/gateway/src/agent/interaction.controller.ts the only hits are the imported class name TessDurableSessionService (a pre-existing M2 service/class name, not an identity/command literal or config default — the controller never compares agentName to 'tess' or any hardcoded string). No FAIL condition found.

5. Quality gate — VERIFIED

Ran directly in an isolated worktree at the reviewed head (after building the workspace's transitive TS project deps, which is an environment/build-order prerequisite unrelated to this PR's content):

  • pnpm --filter @mosaicstack/gateway exec vitest run572 passed, 11 skipped (integration tests requiring live services), 0 failed (47 files), including the new src/agent/interaction.controller.test.ts (4/4 passed).
  • pnpm --filter @mosaicstack/mosaic exec vitest run640 passed, 0 failed (47 files), including the new src/commands/interaction.test.ts (1/1 passed).
  • tsc --noEmit clean for both @mosaicstack/gateway and @mosaicstack/mosaic.
  • eslint clean on all touched/added files in both packages.
  • CI (ci/woodpecker/pr/ci) state at head de74e46a… = success (https://ci.mosaicstack.dev/repos/47/pipeline/1735/1), confirmed via the Gitea status API immediately before posting this verdict, head unchanged since review start.

Blocking findings

None.

Non-blocking follow-ups (do not gate this PR)

  • interaction.ts:143 (void send;) — cosmetic; consider dropping the unused intermediate binding.
  • chat and send are functionally identical today (both call sendInteractionMessage); fine per the required subcommand list, but if they're meant to diverge later (e.g. chat adding conversational framing) it's worth a code comment noting the intentional duplication.

VERIFIED APPROVE reviewer-of-record [W-jarvis:reviewer-sonnet] head de74e46a06

## Independent review — PR #731 `feat(tess): add generic interaction CLI` Reviewer: [W-jarvis:reviewer-sonnet] (independent, non-author of LANE diskhygiene-terra) Head reviewed: `de74e46a0640c44c6b7294c24669496980761c92` (confirmed via API immediately before review and unchanged throughout) ### 1. Subcommand surface — VERIFIED All nine required verbs are registered and each does real work against the gateway API (no stubs): - `packages/mosaic/src/commands/interaction.ts:64` `status` → `fetchInteractionStatus` - `interaction.ts:73` `health` → `fetchInteractionHealth` - `interaction.ts:80` `sessions <provider>` → `fetchInteractionSessions` - `interaction.ts:94` `tree <provider>` → `fetchInteractionTree` - `interaction.ts:106` `attach <sessionId>` → `attachInteractionSession` - `interaction.ts:121` `send <sessionId> <message>` → `sendInteractionMessage` - `interaction.ts:146` `chat <sessionId> <message>` → `sendInteractionMessage` (conversational alias of `send`, same real dispatch path — not a stub) - `interaction.ts:170` `stop <sessionId>` → `stopInteractionSession`, `--approval` required - `interaction.ts:187` `recover <sessionId>` → `recoverInteractionSession` Test `packages/mosaic/src/commands/interaction.test.ts:6-21` asserts exactly these 9 subcommands are registered. Non-blocking nit: `interaction.ts:120-143` builds `const send = …` then immediately does `void send;` purely to silence an unused-var lint rule — harmless but a little odd; a plain statement without the binding would read cleaner. ### 2. Reuse of merged M1/M2 (no re-invention) — VERIFIED - `recover` → `apps/gateway/src/agent/interaction.controller.ts:137-156` calls `TessDurableSessionService.recoverProviderSession`, which calls the pre-existing `DurableSessionCoordinator.recover` (`tess-durable-session.service.ts:79-83`) — the M2 durable coordinator, not a new mechanism. - `attach` → `interaction.controller.ts:63-85` obtains identity via `durable.getSnapshot` (new thin wrapper added in this PR at `tess-durable-session.service.ts:67-77`, itself built from the pre-existing `coordinator.snapshot` + `assertScope`) then calls `RuntimeProviderService.attach`, the same registry `execute()` path used by every other runtime verb (`runtime-provider-registry.service.ts:216-231`). - `send`/`stop` → `send` (`interaction.controller.ts:87-112`) uses `queueProviderSend` + `dispatchProviderOutbox` (pre-existing M2 outbox/idempotency path, `tess-durable-session.service.ts:26-63`), which internally calls `RuntimeProviderService.sendMessage`. `stop` (`interaction.controller.ts:114-135`) calls `RuntimeProviderService.terminate`, which internally `consume()`s a durable exact-action approval (`runtime-provider-registry.service.ts:251-276`) before ever reaching the provider — a hard 403 (`ForbiddenException('Exact-action approval is required')`, `interaction.controller.ts:123-124`) if `--approval` is missing. No ad-hoc bypass found; `send` is not gated by approval, which is consistent with the pre-existing registry design (only `terminate` consumes an approval token) and is not something this PR changed. - Actor-scope/session-ownership binding (TESS-SEC-003): every mutating/read handler calls `assertConfiguredAgent` then `durable.getSnapshot`/`assertSessionAgent` before touching the runtime (`interaction.controller.ts:42,56,71,78,95,101,122,127,144,147`), and `getSnapshot`/`queueProviderSend`/`dispatchProviderOutbox` all funnel through the existing `assertScope` check comparing `context.actorScope` to the durable session's `ownerId`/`tenantId` (`tess-durable-session.service.ts:88-96`). ### 3. Credential-safe status/health — VERIFIED - `status` (`interaction.ts:64-69`) hits the pre-existing `/api/providers/status` endpoint, unmodified by this PR (`apps/gateway/src/agent/providers.controller.ts:38-48`), which already reduces provider errors to a stable `errorCode` and exposes `effectivePolicy` via `getEffectivePolicyStatus()` — the M2-001 credential-safe pattern, reused rather than reinvented. - `health` (`interaction.ts:73-77`) hits the pre-existing global `/health/ready` (`apps/gateway/src/health/health.controller.ts:11-14`), which returns only `{status:'ready'}` — no config/credential surface by construction. - Grepped all new/changed files (`interaction.ts`, `interaction.test.ts`, `gateway-api.ts` additions, `interaction.controller.ts`) for secret/token/credential literals — none found; only session cookies are forwarded as opaque headers, never logged or printed. ### 4. NAME-AS-CONFIG invariant — VERIFIED - `packages/mosaic/src/commands/interaction.ts:21-25` (`configuredAgentName`) derives the instance identity solely from `--agent` or `MOSAIC_AGENT_NAME`; it throws if neither is set. No literal `tess` command or identity appears anywhere in the new CLI/controller code. - Server-side enforcement is symmetric: `apps/gateway/src/agent/interaction.controller.ts:176-181` (`assertConfiguredAgent`) rejects any request whose URL `:agentName` doesn't match the server's own `MOSAIC_AGENT_NAME` env var — this is what prevents a client from spoofing a different configured identity, not a hardcoded name check. - Explicit test proving a differently-named instance works with **zero code change**: `apps/gateway/src/agent/interaction.controller.test.ts:7-22` sets `MOSAIC_AGENT_NAME='Nova'`, calls `controller.sessions('Nova', …)` and gets a real result, then calls `controller.sessions('Other', …)` and gets rejected — proving the exact generic/config-driven behavior required, using a non-`tess` instance name. - Grep audit: `grep -in tess packages/mosaic/src/commands/interaction.ts packages/mosaic/src/commands/interaction.test.ts packages/mosaic/src/tui/gateway-api.ts` → zero hits. In `apps/gateway/src/agent/interaction.controller.ts` the only hits are the imported class name `TessDurableSessionService` (a pre-existing M2 service/class name, not an identity/command literal or config default — the controller never compares `agentName` to `'tess'` or any hardcoded string). No FAIL condition found. ### 5. Quality gate — VERIFIED Ran directly in an isolated worktree at the reviewed head (after building the workspace's transitive TS project deps, which is an environment/build-order prerequisite unrelated to this PR's content): - `pnpm --filter @mosaicstack/gateway exec vitest run` → **572 passed, 11 skipped (integration tests requiring live services), 0 failed** (47 files), including the new `src/agent/interaction.controller.test.ts` (4/4 passed). - `pnpm --filter @mosaicstack/mosaic exec vitest run` → **640 passed, 0 failed** (47 files), including the new `src/commands/interaction.test.ts` (1/1 passed). - `tsc --noEmit` clean for both `@mosaicstack/gateway` and `@mosaicstack/mosaic`. - `eslint` clean on all touched/added files in both packages. - CI (`ci/woodpecker/pr/ci`) state at head `de74e46a…` = **success** (https://ci.mosaicstack.dev/repos/47/pipeline/1735/1), confirmed via the Gitea status API immediately before posting this verdict, head unchanged since review start. ### Blocking findings None. ### Non-blocking follow-ups (do not gate this PR) - `interaction.ts:143` (`void send;`) — cosmetic; consider dropping the unused intermediate binding. - `chat` and `send` are functionally identical today (both call `sendInteractionMessage`); fine per the required subcommand list, but if they're meant to diverge later (e.g. `chat` adding conversational framing) it's worth a code comment noting the intentional duplication. VERIFIED APPROVE reviewer-of-record [W-jarvis:reviewer-sonnet] head de74e46a0640c44c6b7294c24669496980761c92
Author
Owner

VERIFIED APPROVE reviewer-of-record [W-jarvis:reviewer] head de74e46a06

Evidence verified at exact head de74e46a0640c44c6b7294c24669496980761c92:

  • Woodpecker PR pipeline 1735 is terminal green on this commit; steps ci-postgres, install, sanitization, typecheck, lint, format, and test succeeded.
  • Generic mosaic interaction command registers required verbs: chat/status/sessions/tree/attach/send/stop/health/recover; instance identity is data via --agent or MOSAIC_AGENT_NAME.
  • Gateway interaction boundary derives actor/tenant scope from @CurrentUser()/scopeFromUser, requires X-Correlation-Id, validates configured agent identity, and checks durable session identity before attach/send/stop/recover.
  • Send/recover use durable session service; attach/sessions/tree/stop route through the runtime provider registry; stop requires an approval ref and delegates to the existing exact-action runtime approval path.
  • Status/health surfaces use credential-safe endpoints; diff contains no live credential material.

No merge performed by reviewer.

Marker: ROR-731-APPROVE-de74e46a

VERIFIED APPROVE reviewer-of-record [W-jarvis:reviewer] head de74e46a0640c44c6b7294c24669496980761c92 Evidence verified at exact head `de74e46a0640c44c6b7294c24669496980761c92`: - Woodpecker PR pipeline `1735` is terminal green on this commit; steps `ci-postgres`, `install`, `sanitization`, `typecheck`, `lint`, `format`, and `test` succeeded. - Generic `mosaic interaction` command registers required verbs: chat/status/sessions/tree/attach/send/stop/health/recover; instance identity is data via `--agent` or `MOSAIC_AGENT_NAME`. - Gateway interaction boundary derives actor/tenant scope from `@CurrentUser()`/`scopeFromUser`, requires `X-Correlation-Id`, validates configured agent identity, and checks durable session identity before attach/send/stop/recover. - Send/recover use durable session service; attach/sessions/tree/stop route through the runtime provider registry; stop requires an approval ref and delegates to the existing exact-action runtime approval path. - Status/health surfaces use credential-safe endpoints; diff contains no live credential material. No merge performed by reviewer. Marker: ROR-731-APPROVE-de74e46a
jason.woltje merged commit 8246ee0137 into main 2026-07-13 05:52:42 +00:00
jason.woltje deleted branch feat/tess-cli 2026-07-13 05:52:42 +00:00
Sign in to join this conversation.
No Reviewers
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: mosaicstack/stack#731