Skip to content

mecatui: /agent command and picker for session-scoped agent definitions - #2055

Draft
kantord wants to merge 19 commits into
mainfrom
impl/session-scoped-agent-identity-mecatui
Draft

kantord wants to merge 19 commits into
mainfrom
impl/session-scoped-agent-identity-mecatui

Conversation

@kantord

@kantord kantord commented Oct 1, 2026 •

Copy link
Copy Markdown
Member

Summary and stack

Adds the TUI entrypoint for the agent-bound session API in #2003: /agent <name> creates a named-definition session; bare /agent opens a filterable picker. /agents remains the definition-details view.

Stack: main → #2003, backend → this PR. The current diff against the backend is only cmd/mecatui/: 15 files, +653/−4. The backend fixes and test cleanup were placed in #2003, then merged into this branch without rewriting published history. There were no additional TUI code changes in that backend repair pass.

Design provenance and review scope

The underlying root-session semantics come from the design-only #1783, including the human approval, and the reviewed/merged Plan / Interface #1815. The ADR was renumbered to 0353 in #1812. Permanent references: ADR snapshot and merged acceptance criteria/resolved decisions. #2105 removed that process scaffolding from main.

Those checkpoints accepted the backend design, not this implementation or every command/picker UX choice. This PR still needs its own client/UX review. Main has continued to move; the current implementation and integration checks, not the retired task list, are the evidence for review.

Behavior

  • /agent <name> sends agent_definition_name through the existing client/session interface.
  • Bare /agent, including from the slash palette, lists definitions, filters by text, and creates the selected definition on Enter. Submitting the command closes the slash palette.
  • The request carries the operator's active provider/model selection. Under the agreed backend precedence, an explicit request pair wins over the definition's pair. Thus the command can override a definition's model pin; changing that UI choice is a separate decision, not an unspecified server rule.
  • An unknown name produces a nonfatal status message rather than a terminal connection screen. The operator can enter another command.
  • The picker supports keyboard selection and wheel scrolling; mouse-click row selection is not implemented.

Verification — October 15, 2026

Automated checks

The integrated backend-plus-TUI tree passed local task lint, the full task --concurrency 1 test and task --concurrency 1 test:race suites, and the offline demo's tool → permission ask → approval → result flow. The full suites include cmd/mecatui and its subpackages. Docs, generated-contract/API checks, SDK typecheck/discovery tests, and the site build also passed during the implementation pass.

The full suites were serialized, with a short temporary root and an isolated loopback namespace to avoid host fixture conflicts; setup capabilities were dropped before testing. No tests were disabled. After the stack was rearranged, head c704ae874 was verified tree-identical to the tested working tree. These results do not certify an untested future integration with newer main, and do not replace CI.

Client/UI coverage is in agent request tests and command/picker tests. The detailed backend alias, authority, persistence, ownership, and transport regression evidence is in #2003, rather than duplicated here.

Separate manual smoke report

The operator supplied an additional agent-assisted manual report using locally built mecated and mecatui, the offline mock provider, loopback with authentication disabled, and temporary HOME/XDG/workspace directories. No operator credentials or paid providers were used. This working-tree run was not pinned to an immutable final commit.

Reported TUI observations:

  • The picker listed the configured definitions and filtered them by text.
  • Choosing reviewer created a bound session with server-enforced plan mode.
  • /agent nope produced a recoverable status message.

The same manual pass drove HTTP/SSE and gRPC creation, exact binding echoes, mode/limit clamps, runtime turn limits, restricted tool calls, aliases, rejection paths, fork/clear, and a persistent-server restart. #2003 records those observations and their limitations. In particular, the mock smoke does not prove real-model behavior, authenticated deployed ownership isolation, or mixed-version client safety.

API compatibility and remaining client review items

No new wire fields are introduced by this TUI-only diff; it consumes #2003's additive API-major-1 contract. That backend now advertises session_agent_definitions; the previous description saying no feature identifier existed is obsolete.

This PR is still a draft. The following client gaps are not fixed by the backend changes or by a passing happy-path smoke:

  1. Mixed-version safety: CreateSessionWithAgent currently sends the field without requiring session_agent_definitions, and the shared create helper does not validate resolved_agent_definition_name. An older server can ignore the field and create an ordinary session. Require support and validate the echo before treating the session as bound. See client request and response handling.
  2. Failed handoff: the command resets the local session state and closes the old session before the replacement create succeeds. A nonfatal error message does not mean the previous session/view was preserved. Review and test failure-safe handoff. See handoff implementation.
  3. Effective state and error presentation: the ready message carries the requested mode, while the server may clamp it; all InvalidArgument create errors are labeled as an unknown definition, including unavailable-provider errors. Verify/refetch the enforced mode and preserve the actual error cause. The manual server-side plan-mode observation does not independently establish correct UI mode display.

These are implementation review items, not reasons to reopen the already accepted root-session design. Review/merge #2003 first, keep this PR based on its updated head, and finish these client checks before declaring the TUI ready.

What the local checks do not establish

No manual live-provider run, authenticated multi-owner deployment, Unix-socket-only test of the inner client-MCP guard, live TypeScript SDK journey, or deployed mecak8s/Redis/remote-driver/TLS/mTLS/microVM/execution-template qualification was performed in the reported smoke pass. Those require appropriate fixtures or environments and can be performed before merge; they are not inherently post-merge-only checks.

Main integration update: #2003 now includes main at ce0f816e0 through c4b5b1fe0. Its sole textual conflict was resolved by retaining both independent changelog entries, and full local lint/docs/offline/race/demo checks passed on the merged backend. This PR incorporates that backend in 62cdfcb8c; the TUI package race suite and scoped lint passed again, and the diff remains 15 TUI-only files. Both PRs are conflict-free at this update. The manual smoke observations above were not rerun during conflict resolution. Latest CI and the outstanding client items above remain required; merging main did not fix those client issues.

@kantord
kantord force-pushed the impl/session-scoped-agent-identity branch from 595be9f to e114ac1 Compare October 8, 2026 11:41
@kantord
kantord force-pushed the impl/session-scoped-agent-identity-mecatui branch from cda6eee to 4b1871c Compare October 8, 2026 11:41
@kantord
kantord marked this pull request as draft October 13, 2026 11:05
@kantord
kantord force-pushed the impl/session-scoped-agent-identity branch from e114ac1 to c23734a Compare October 13, 2026 11:09
kantord and others added 12 commits October 13, 2026 13:10
Adds the CreateSessionRequest.agent_definition_name / CreateSessionResponse
.resolved_agent_definition_name wire fields, a write-once AgentDefinitionName
label on session.Session and sessnap.Snapshot, and verbatim echo on the
response. Plumbing only: no catalog restriction, provider/model/limits/
permission-mode resolution, or validation is wired from this field yet — that
is later work in docs/acceptance/session-scoped-agent-identity.md.

Pins AC2.1 (TestSessionScopedAgentIdentity_Scenario2_PersistsAcrossSnapshotRoundTrip)
and AC2.2 (TestSessionScopedAgentIdentity_Scenario2_ResponseEchoesBoundName).

Plan / Interface PR: #1815 (merged, a9dfd84)

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…l-widening paths

Task D of the session-scoped-agent-identity plan (AC3.1, AC3.2). Authority
gains an optional, write-once Ceiling capability set, bound once alongside
BindAuthority/RestoreLabels and never re-derived. GrantToolAuthority and
CompleteWorkspaceEnrollment — the two aggregate methods that can widen a
bound session's tools post-bind — now reject any grant that would exceed
the Ceiling, making it a Session-aggregate invariant rather than an
enumerated adapter-layer guard list.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…atalog (ADR 0353, Task B)

CreateSession with agent_definition_name set now builds the session's catalog
exclusively from the resolved AgentDef's tools/disallowedTools/mcpServers,
mints session.Authority from that same resolved catalog (never the
deployment default), and skips Config.MCPBroker attachment entirely — while
keeping ordinary main-session guardrails/governance/ask-flow (never the
child-shaped headless auto-deny).

- internal/app/agentdefs.go: extract resolvedAgentDefCatalog from
  buildAgentDefEngine (byte-identical for its 3 existing child call sites);
  add baseSubagentToolsNoFS for the no-fs profile intersection.
- internal/app/agentdef_root.go: new buildAgentDefRootEngine (bottoms out in
  engineDepsForProvider + attachGuardrailReviewer instead of the child-shaped
  newChildEngineForProvider; mints Authority via mintRootAuthority over the
  def's own catalog) and agentDefSessionEngineFactory.
- internal/app/build.go: wire AgentDefSessionEngine onto server.Config from
  the same collaborators as SessionEngineWithTools.
- internal/adapter/server/service.go: new AgentDefSessionEngineFactory type
  and Config field, SessionEngineResult.Authority field, agent_definition_name
  validation (rejects a debug target or client/debug MCP servers), and a
  dedicated create-path branch that never reaches MCPBroker attachment.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Task B (mints session.Authority via mintRootAuthority) and Task D (added
Authority.Ceiling + its enforcement in GrantToolAuthority/
CompleteWorkspaceEnrollment) were dispatched concurrently and never wired
together: buildAgentDefRootEngine minted an Authority with a nil Ceiling for
every real agent-bound session, making the Ceiling mechanism dead code in
production even though its unit tests (which set Ceiling by hand) passed.

- buildAgentDefRootEngine now sets Ceiling to a full-field copy of the
  CapabilitySet it just minted, for every agent-bound session (AC3.1).
- BindAuthority now rejects a payload whose CapabilitySet.Tools already
  exceeds its own Ceiling, tools-only (matching GrantToolAuthority /
  CompleteWorkspaceEnrollment's existing AllowsTool convention, not the
  fuller CapabilitySet.Contains dimensional check) — hardening flagged by
  external review, since neither post-bind guard could otherwise repair an
  already-inconsistent bind.
- Extended TestSessionScopedAgentIdentity_Scenario1_AuthorityMintedFromDefCatalog
  (AC1.9) to also assert Ceiling is set and consistent, and added a
  mutation-verified rejection subtest to
  TestSessionScopedAgentIdentity_Scenario3_AuthorityCeilingBoundOnce (AC3.1).

Both gaps found by independently re-verifying the codebase (not trusting a
report) after an external review flagged the BindAuthority half; the more
severe wiring gap was found in the course of checking that specific claim.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…ped Role

Task E's AC1.7/AC1.8 pinning tests surfaced a real production gap:
buildAgentDefRootEngine set Deps.Role to "agent-root:<def>" purely for
diagnostic readability, but engine/agent uses Role != "" throughout
(loop.go, steer.go, actionreview.go, dispatch.go) as the signal that an
engine is a delegated child — gating principal-prompt-provenance trust,
review-principal establishment, and external-authorization presentation.

Consequence in production: establishReviewPrincipal short-circuited for
every agent-bound session, so PrincipalFactsComplete was permanently
false, so every "acceptable" contextual-guardrail verdict was rejected as
untrustworthy (acceptable_with_incomplete_context) and fell back to
"unresolved" — which a headless/non-interactive engine (the Slack-bot
deployment this whole feature targets, issue #1053) denies outright. Every
guardrail-matched tool call on a real agent-bound session running headless
would have been silently blocked, whenever GuardrailsRules matched it.

Considered adding an explicit Deps.ChildShaped field decoupled from the
display-only Role, set once at the shared child-engine builder
(childEngineDeps/childEngineDepsForProvider) and read at all 8 behavior-
gating call sites instead of Role. Reverted: several existing engine/agent
tests construct a Deps{Role: ...} literal directly (engine-level tests
cannot import internal/app's builders) to simulate a child, and lacking
the new field they were silently reclassified as root-shaped — one such
test then presented an interactive authorization ask nothing resolves,
deadlocking `go test ./agent/...` (goroutine dump: askRegistry.await via
surfaceAsk/authorizeDecision). That approach's blast radius was broader
than it looked.

Minimal fix instead: buildAgentDefRootEngine simply never sets Role.
Zero changes to engine/agent's core dispatch logic; the bound def's name
is already logged once per engine build by agentDefSessionEngineFactory's
own diagnostic call, so no observability is lost. This is also more
faithful to AC1.6 ("ordinary main-session behavior... in every respect
except its tool catalog") than a differentiated Role ever was. The now-
unused role parameter is removed from buildAgentDefRootEngine and its
three call sites.

Verified: fresh `go test -count=1 ./agent/...` (engine module, was hanging
to the 600s timeout with the reverted ChildShaped approach, now green in
~5s) and `go test -count=1 ./internal/app/... ./internal/adapter/server/...`
— all green; scoped lint clean.

Pins AC1.7 (TestSessionScopedAgentIdentity_Scenario1_ContextualGuardrailInspectsEffectivePayload)
and AC1.8 (TestSessionScopedAgentIdentity_Scenario1_HookMutationCannotBypassGuardrail).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
… (ADR 0353, Task C)

Implements ADR 0353's 5-case provider/model pair resolution
(resolveAgentDefRootProviderModel) and tighten-only Limits/PermissionMode
clamps (tightenLimits, clampPermissionMode) for an agent-bound session,
wired into agentDefSessionEngineFactory in place of the placeholder
resolveChildProvider call (whose def-wins-over-parent, silent-fallback
contract is wrong for case 5 — an unavailable def-pinned provider must
fail session creation, never silently fall back).

Case 1 delegates to the existing resolveProviderModel for its base-pair
computation, per the plan's own implementation guidance; cases 2-5 are
fresh logic against the request's own ProviderSelector.

Pins AC1.12 (TestSessionScopedAgentIdentity_Scenario1_LimitsTightenOnly),
AC1.13 (TestSessionScopedAgentIdentity_Scenario1_PermissionModeClampedNotRaised),
AC1.14 (TestSessionScopedAgentIdentity_Scenario1_ProviderModelPairResolution).

Plan / Interface PR: #1815 (merged, a9dfd84)

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…ADR 0353, Task F)

SetMode, LoadSessionWithMCP, the needsRehydration()/engineAndEnvironmentFor
choke point (StartRunContent, RetryFailedRun, CompactSession,
resumeFromAwaiting), rebuildGrantedAuthorizationEngine, and
ConnectWorkspaceServices all now refuse an agent-bound session rather than
silently rebuilding on the deployment's default catalog, so its tool-scope
ceiling survives every rebuild trigger that doesn't know about
agent_definition_name.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…coped project memory (ADR 0353, Task G)

ForkSession/ClearSession now unconditionally copy the source session's
agent_definition_name onto the successor (Profile-style, no override field)
and route the successor's engine rebuild through AgentDefSessionEngine rather
than the deployment's default catalog factory, skipping Config.MCPBroker
attachment for agent-bound successors the same way create-time does (AC2.3,
AC2.4).

resolveAgentMemoryHead's "project" tier now binds to the caller's own resolved
placement root instead of the single process-wide cfg.Workspace, using the
root-aware projectIngestionAdmittedForRoot gate — two agent-bound sessions on
the same def but different placements (e.g. a fork onto an alternate
worktree) no longer share or leak the same project-memory file, and a
placement the operator never specifically vetted is never admitted merely
because the deployment's global trust flag is true. Under profile: "no-fs"
this is a silent no-op, never an error (AC2.5, AC2.6). The session's own
placement root reaches AgentDefSessionEngineFactory's closure via a small
ContextWithAgentDefSessionRoot/AgentDefSessionRootFromContext context-value
pair rather than growing the factory's frozen signature.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…y.Ceiling

Aggregate-gates pass after all 7 tasks landed. Two gaps found:

1. task api:update was never run for the two additive engine/session surface
   changes from earlier tasks (Session.AgentDefinitionName, Authority.Ceiling)
   — engine/api/session.txt now reflects both; classified Added (minor) in
   engine/CHANGELOG.md per COMPATIBILITY.md.

2. A second, separate persistence path — eventsource.SessionMeta/Fold, used
   for durable event-log session reconstruction (distinct from sessnap, which
   Task A already handled) — never carried AgentDefinitionName at all.
   TestFoldContractDocMatchesSessionFields (a reflective drift guard) caught
   this: it fails until a new session.Session field is classified in
   COMPATIBILITY.md's reconstruction contract. Fixed: SessionMeta gained the
   field, Fold restores it by direct assignment (mirroring Profile exactly),
   the one production caller that already threads full session metadata
   through this path (internal/app/source_evidence.go) now includes it, and
   COMPATIBILITY.md's contract table + the drift-guard test's field map are
   both updated.

Also regenerated user-docs/reference/grpc-schema.md via `task docs` (buf was
missing from this environment; installed via `go install
github.com/bufbuild/buf/cmd/buf@latest` for this session) — it had never
picked up Task A's agent_definition_name/resolved_agent_definition_name proto
fields until now.

Verified: full `go test ./...` in both engine/ and root modules green
(including TestPublicAPIUnchanged and TestFoldContractDocMatchesSessionFields);
`task docs` passes (0 broken links/anchors); scoped lint clean.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
… (ADR 0353)

External review found sessnap.Restore and eventsource.Fold could restore a
session carrying AgentDefinitionName with no Ceiling. Everything else routes
it as agent-bound, yet GrantToolAuthority and CompleteWorkspaceEnrollment
treat a nil Ceiling as unrestricted, defeating the non-widenable ceiling.

Both restore paths set AgentDefinitionName after binding authority, so a
check inside BindAuthority could not see the label. Reorder both to set it
first (as the create path already does) and make BindAuthority reject an
agent-bound bind without a Ceiling.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…(ADR 0353)

createRequest.matches ignored AgentDefinitionName, so a same-owner retry
could swap between an agent-bound and an ordinary (or different) session.
It also compared raw request limits with persisted limits, which for an
agent-bound session are the definition's clamped values, so legitimate
retries could be rejected.

Compare the binding explicitly and skip the raw-limits comparison for an
agent-bound request.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…ovider

After rebasing onto main, engine construction takes a provider+model pair.
buildAgentDefRootEngine was given only a model string, so the pair carried the
deployment default provider even when the definition or request selected a
different one. Pass the resolved ProviderModelID instead, matching
buildAgentDefEngine.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@kantord
kantord force-pushed the impl/session-scoped-agent-identity branch from c23734a to 6065a11 Compare October 13, 2026 11:13
kantord and others added 2 commits October 13, 2026 13:13
… definition

Gives a developer an end-to-end way to exercise ADR 0353 from the TUI. An
unknown name fails loudly and recoverably. The create carries the operator's
active model selection like every other create path; sending none made a def
without its own model fall to the server default.

Bare /agent only points at /agents; a filterable picker is not built yet.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…ions

Bare /agent (typed or from the palette) now opens a modal picker listing the
server's definitions by name; Enter starts a session bound to the highlighted
one, using the same create path as /agent <name>. Submitting /agent also
closes the slash-command palette, which previously stayed open.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@kantord
kantord force-pushed the impl/session-scoped-agent-identity-mecatui branch from 4b1871c to 61b1fba Compare October 13, 2026 11:16
kantord and others added 3 commits October 15, 2026 14:06
Resolve root model aliases independently of delegation defaults, preserve effective provider configuration, and reuse owner-scoped context and pinned runtime assets. Enforce successor attenuation, validate factory acknowledgments and restored authority, preserve original retry intent, and close failed-build resources.

Complete HTTP/gRPC binding parity, advertise session_agent_definitions, and add integration regressions and owning documentation. Leave the stacked TUI changes untouched.

API compatibility: additive optional binding fields and discovery feature; wire major remains 1. Clients relying on binding must require support and verify the echo. Ordinary clients and legacy ordinary sessions remain unchanged. Valid legacy bound records remain readable, but missing retry fingerprints cannot establish explicit-ID retry equality; malformed authority fails closed. Engine additions and behavioral guards are classified separately in the changelog.

Validated with full lint, offline and race suites, docs, SDK checks, and the offline demo. Full test suites used serialized tasks and isolated loopback networking to avoid host fixture conflicts.

Co-authored-by: Mecatl <noreply@mecatl.dev>
Consolidate catalog cases and broker setup, trim integration permutations already covered by unit matrices, and correct stale binding comments. Preserve distinct lifecycle and transport assertions; no production behavior changes.

Co-authored-by: Mecatl <noreply@mecatl.dev>
Keep PR #2055 based on the updated backend from PR #2003 without rewriting published history. No additional TUI changes.

Co-authored-by: Mecatl <noreply@mecatl.dev>
kantord and others added 2 commits October 15, 2026 14:51
Resolve the changelog-only conflict by retaining both the agent-bound authority entry and main's scheduling contract entries. Preserve all automatic merge results.

Validated the merged backend with task lint, task docs, complete offline and race suites, and the offline demo.

Co-authored-by: Mecatl <noreply@mecatl.dev>
Bring in the backend's main integration while keeping PR #2055's diff limited to its original 15 TUI files. TUI race tests and scoped lint pass.

Co-authored-by: Mecatl <noreply@mecatl.dev>
Base automatically changed from impl/session-scoped-agent-identity to main October 16, 2026 15:28

This branch was successfully deployed

1 active deployment
Preview — 62cdfcb8 Deployed Oct 15, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant