Skip to content

Implement session-scoped Agent Definitions - #2003

Open
kantord wants to merge 15 commits into
mainfrom
impl/session-scoped-agent-identity
Open

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

Conversation

@kantord

@kantord kantord commented Sep 29, 2026 •

Copy link
Copy Markdown
Member

Summary and stack

Implements session roots bound to named agent definitions: a definition-specific tool catalog and nonwidenable authority ceiling, with ordinary root-session guardrails and approval handling. Motivated by #1053.

Backend PR: main → this PR → #2055, the TUI command and picker. This PR contains the server, engine, persistence, protocol, SDK feature vocabulary, documentation, and backend tests—not the /agent UI.

Previously agreed design; implementation review remains necessary

This is the implementation of the design and acceptance criteria reviewed under the former ADR/acceptance-plan process, not a new proposal for the feature:

Main has moved. Those approvals establish design intent, not that old implementation recipes or an old review still prove today's code correct. This implementation adapts that intent to current owner-scoped sources, placement and MCP-runtime lifecycles, provider-aware aliases (#2019), and contextual guardrail composition. Historical task letters are not the readiness checklist.

Implemented behavior

  • Optional agent_definition_name on gRPC and HTTP creation, with resolved_agent_definition_name echoed back. Unknown names and incompatible debug/client-MCP combinations are rejected.
  • The root catalog comes from the definition's bounded tool set and selected MCP servers. Authority is minted from that catalog; later grants cannot exceed its ceiling. The engine retains root approval and guardrail behavior rather than delegated-child behavior.
  • Definition model aliases retain their provider/model pairing. A root does not inherit the generic subagent default or invoke automatic delegation routing. Request overrides follow the agreed five-case pair resolution; wire model_id remains literal. An unavailable explicitly pinned provider still fails creation—this PR does not introduce a different fallback policy.
  • Requested limits and permission mode only tighten the definition. Fork/Clear retain the binding and inherited authority, apply the current definition catalog, and enforce tighter returned mode/limits. Unsafe runtime rebuilds fail closed.
  • Owner/execution-scoped sources, remote/no-FS policy and prompt configuration, provider effort/capability reconstruction, and operation-pinned MCP assets use the existing composition mechanisms. Failed construction/publication releases acquired resources.
  • Effective identity persists separately from original explicit-ID create intent. Identical retries survive clamping; changed requests are rejected. Snapshot and event-source restore reject bound records without valid authority, while learning-evidence reconstruction retains the required metadata.
  • Project memory follows the session placement and its trust decision. No-FS sessions do not receive project-memory content. Enrollment checks ownership before revealing whether a session is agent-bound.

API compatibility

Additive wire change; API major remains 1. Omitted binding fields preserve ordinary-session behavior. The stable session_agent_definitions feature reports protocol support on both transports, independent of whether definitions are configured. Clients relying on binding must require this feature and verify the exact response echo before sending a prompt; an older gRPC server can silently ignore the new field. Existing clients do not need changes. Upgrade the server before relying on binding; no wire-breaking change or major-bump approval is requested.

Go and stored-data compatibility are separate: new engine identifiers are classified as Added and authority-enforcement behavior as Changed (pre-v1 minor) in the engine changelog. Ordinary legacy sessions need no migration. Valid legacy agent-bound records without the new retry fingerprint remain readable but cannot prove explicit-ID retry equality. Malformed bound authority is rejected, not reconstructed from today's definition. Metadata readability does not imply permission to resume execution after restart.

Additional due diligence — October 15, 2026

The extra pass checked the implemented behavior again rather than relying on the historical approvals or an earlier end-to-end run.

Automated verification and targeted regression review

Passed locally on the completed implementation and cleanup tree:

  • task lint
  • task --concurrency 1 test — complete offline suite, including standalone module checks
  • task --concurrency 1 test:race — complete race suite
  • go run ./cmd/mecademo — observed tool call, permission ask, approval, and result
  • task docs; SDK typecheck and focused discovery tests; generated contract/API updates and checks; site build

The full suites were serialized and used a short temporary root plus isolated loopback networking to avoid occupied host OAuth ports and Unix-socket path limits. Namespace setup capabilities were dropped before the final gate run. Initial environment/timing failures were investigated; tests were not disabled to obtain the passing runs.

Verification ran on the integrated backend-plus-TUI working tree before the stack was separated. After committing the backend changes and updating the TUI stack, the final TUI head c704ae874 was checked to be tree-identical to that tested tree. This is not a claim that the backend-only SHA was separately put through every gate, or that a future merge with newer main has already been validated. CI remains an independent gate.

Agent-assisted spec, standards, test-adequacy, architecture, security, and reuse review was followed by fixes and focused closure checks. Useful committed evidence includes:

Boundary checked Regression evidence
Settings aliases, distinct root/child providers, actual model requests, owner isolation, remote/no-FS posture, runtime publication, effort/capabilities Root composition tests and healed-default test
Failed-create retry and failed-successor source/lease cleanup Real-composition lifecycle tests
Factory acknowledgment, tightened successors, fixed runtime configuration, ownership-safe enrollment, durable retry intent Factory acknowledgment, lifecycle persistence, and retry tests
Placement-specific project memory through real successor requests Memory integration tests
gRPC/HTTP mapping, rejection behavior, and effective identity Wire parity tests

The bloat review consolidated repeated catalog/broker setup and trimmed redundant integration permutations while retaining the unit matrices and distinct lifecycle/transport tests. It did not replace integration coverage with helper-only assertions.

Separate manual end-to-end smoke pass

The operator supplied an additional agent-assisted manual-run report from today's working tree. It used locally built mecated/mecatui, the offline --mock-script provider, temporary HOME/XDG directories and workspace, HTTP/SSE via curl, and a small gRPC client built from generated contracts. It exercised both in-memory and persistent stores, including killing and restarting the persistent server. Authentication was disabled on loopback. No operator credentials, paid providers, or operator state were used. This report was not pinned to an immutable final commit and should not be read as a rerun after every subsequent edit.

Journey Reported observation
Discovery /v1/compatibility advertised session_agent_definitions.
Create and echo HTTP returned 201 and gRPC succeeded with the bound-name echo; ordinary creation omitted the binding. Bound sessions reported no built-in shell; the ordinary session reported shell availability.
Mode and limit attenuation acceptEdits against reviewer's plan ceiling resolved to plan on both transports. A requested 100-turn limit resolved to 3, confirmed with gRPC GetSession.
Runtime budget A mock script requesting six Read calls stopped after exactly three turns with max_turns.
Tool scope and permission flow A Write call against Read-only reviewer returned unknown tool. The same call against writer (Read/Write) produced permission.ask; this observation alone does not claim that the write was approved and completed.
Validation Unknown definition: HTTP 400 / gRPC InvalidArgument. Camel-case agentDefinitionName: HTTP 400. Binding plus debug target: HTTP 400. SetMode on a bound session: InvalidArgument.
Client MCP rejection HTTP 501 / gRPC Unimplemented on the TCP deployment. This exercised the deployment guard, which runs before the agent-bound guard—not a manual proof of that inner guard.
Provider/model Alias fast resolved to mock/mock-fast; an unavailable openai definition pin returned InvalidArgument; a bound model-only request retained its literal model ID.
Fork/Clear Both returned HTTP 201; successors retained plan mode and no-shell capabilities.
Persistent restart Metadata retained plan mode and clamped limits. Prompting the old bound session failed closed on rehydration; SetMode remained rejected. Fork/Clear after restart worked and their successors retained plan/no-shell behavior. An ordinary session ran normally after restart.

The picker/filter and /agent smoke observations belong to #2055, not this backend diff.

Limits of that evidence and remaining merge work

Not exercised by the manual smoke pass: real LLM behavior with restricted catalogs; authenticated multi-owner transport/deployment behavior (covered at the isolated composition-test layer); the inner agent-bound client-MCP guard on a Unix-socket-only listener; deployed mecak8s/Redis/Kubernetes leases; remote store/definition drivers; TLS/mTLS deployment; microVM/execution-template deployment; or a live TypeScript SDK journey. Mock/fixture coverage and SDK type/discovery checks are not substitutes for those deployment runs. These checks can be done before merge with the appropriate environment; they are not inherently impossible until deployment.

Intentional v1 scope: no delegation from an agent-bound root, no new per-caller definition-name ACL, and no execution resume of a bound session after restart/client-MCP rehydration. Fail-closed restart was an explicit accepted decision; continuation support remains #1796.

Main integration completed after the review. The initial review fetched main at 7cd9bb0f9. This branch now includes main at ce0f816e0, merged in c4b5b1fe0. The only textual conflict was in engine/CHANGELOG.md; both the agent-authority and scheduling-contract entries were retained unchanged. Local task lint, task docs, the complete offline and race suites, and the offline demo passed again on the merged backend. The TUI branch was then updated, with its package race suite and scoped lint passing; its diff remains the original 15 TUI files. GitHub reports both branches conflict-free at this update. The earlier manual smoke report was not rerun as part of this conflict resolution. Latest CI and implementation review remain required; these results do not certify future main changes.

@kantord
kantord marked this pull request as ready for review October 1, 2026 11:27
@kantord kantord changed the title Implementation (draft, in progress): session-scoped agent identity Implement session-scoped Agent Definitions Oct 1, 2026
@kantord
kantord force-pushed the impl/session-scoped-agent-identity branch from 595be9f to e114ac1 Compare October 8, 2026 11:41
@kantord
kantord requested a review from JAORMX October 13, 2026 08:54
@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 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>
kantord added a commit that referenced this pull request Oct 15, 2026
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
kantord requested a review from jhrozek October 15, 2026 12:26
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>
@JAORMX

JAORMX commented Oct 15, 2026

Copy link
Copy Markdown
Contributor

So, I think the direction makes sense. Asking an agent to “be a reviewer” is easy. Making sure it stays within its permissions when someone forks the session or tries to attach more tools is the useful part here.

I’d keep a clear distinction between what this PR enforces inside Mecatl and what we’re proposing in #2121. This PR gives us a session bound to a definition, with an authority ceiling that cannot grow. The broker work would independently enforce the MCP portion of that authority and attribute calls to a particular agent acting for a particular user. I wouldn’t make this PR wait for that whole architecture.

My main question is how definition changes interact with session continuity. This PR rebuilds fork/clear successors using the current definition while retaining inherited authority. The identity proposal pins a definition revision to an instance. Those can fit together, but we should be explicit: a successor is a new instance; restoring an existing instance must not silently replace its definition or widen its authority. And an administrator still needs to be able to revoke access.

I’m fine with failing closed on restart for this first version. It’s a real usability limitation, especially during rolling upgrades, but I’d rather have an honest limitation than accidentally restore a broader session. We should make the recovery path clear and keep safe continuation as a separate follow-up.

For the identity integration, I’d propose treating agent_definition_name as a selector, rather than an identity. The user asks for reviewer; trusted server-side code resolves which definition that means in the applicable source scope. The external identity should come from registering a particular instance against that resolved definition, not from trusting the string the client supplied.

Concretely, I’d want the integration contract to separate:

  • The display/selection name, such as reviewer.
  • An admitted definition reference identifying its source and revision. Two definitions with the same name must not become interchangeable.
  • The durable instance identifier, associated with the owning user/session and its effective authority ceiling.

When we add the broker, Mecatl would register the instance using a definition reference the broker recognizes. The broker would validate that registration and establish the instance’s permitted MCP operations from admitted definition records. The client or model shouldn’t be able to establish authority by supplying a name, a tool list, or even a digest. A digest identifies content; it doesn’t prove we trust it.

That also leaves a separate admission question: is this caller allowed to start a session using this definition? We don’t need to add definition-specific ACLs here, but selecting a known name must not later become an implicit privilege grant.

For this PR, I’m suggesting we document that boundary and the successor/restoration rules, not add credentials or a registration API. Then the follow-up in 2121 has a concrete job: define the admitted definition reference, bind it to an instance and owner, and enforce the resulting ceiling at the broker.

@JAORMX JAORMX left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the overall approach makes sense, but there’s a definition-scope enforcement issue we should fix before merging: the root authority is minted from a catalog that can include explicitly disallowed tools. I’ve kept the inline comment focused on the implementation and repair, rather than an exploit walkthrough.

The other actionable findings are the child-only Shell trust gate leaking into root construction, explicit auto reasoning effort changing meaning during reconstruction, and a missing exactness assertion on the production-minted ceiling. Those have inline comments with the relevant paths and suggested regression coverage.

This was an eight-reviewer panel covering the agreed spec, Go conventions, test adequacy, security, lifecycle/composition, API compatibility, duplication, and library reuse. Disputed findings received a separate verification pass. No local tests or linters were run; this is static review backed by the existing CI results. I didn’t find a useful duplication or dependency refactor to ask for. The root/child differences mostly deserve to stay explicit.

A few nonblocking follow-ups, rather than requests to expand this PR:

  • Clarify that bound-session limit tightening covers turns and tool calls; the requested consecutive-failure limit currently keeps the inherited value. That matches the accepted scope, but the API documentation says “limits” more broadly.
  • Add high-level TypeScript binding support as a coordinated follow-up: the typed option, feature requirement, and exact response-echo check should arrive together. Accidental JavaScript forwarding of the currently undeclared option isn’t a supported safe workflow.
  • Clarify whether explicit-ID Service retries compare raw omitted mode or mode after deployment defaulting. The fingerprint currently uses the latter. This is the embedding seam, not a public HTTP/gRPC explicit-ID retry issue.
  • Consider making definition selection and governance-root inputs explicit in a future factory API cleanup. I don’t think a context-to-request refactor needs to block this PR.

CI is a separate concern: Go race, lint, build, API compatibility, and SDK checks passed. The Studio route-count failure is inherited from the base: the skills-files route added a sixteenth operation without updating the count or declaring the expected transport errors. The execution-provider production-profile check was still running when checked. I’m not requesting changes because of that inherited CI failure.

The intentional exclusions (bound-root delegation, new definition-name ACLs, and direct restart execution) aren’t review blockers, and I’m not pulling the broker or TUI follow-ups into this PR.

// deployment's build-time default closure (AC1.9). session.SessionKindMain is
// the only kind an agent-bound session can ever be (AC1.5 rejects the debug
// combination at the create boundary before this ever runs).
authority := mintRootAuthority(cat, resources, session.SessionKindMain)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Blocking: can we apply the definition’s exclusions to the final assembled catalog before minting authority here? scopedToolNamesMode filters core names, but resolvedAgentDefCatalog subsequently adds MCP tools at agentdefs.go:1142–1151 and automatically adds ShellStatus at 1130–1133, without applying DisallowedTools again. This root then copies those entries into both its grant and ceiling. The delegated path’s separate agentDefinitionAuthorityCeiling filtering does not protect this root path.

Please make the final advertised catalog and minted authority respect the same exclusions, including MCP and automatically added tools, and add negative regression coverage through the real root factory. Normal permission approval must not override an explicit definition exclusion.

}

noFS := profile == server.ProfileNoFS
base := baseSubagentTools(cfg)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This base set still carries the read-only child’s Shell trust gate. baseSubagentTools adds Shell through buildSandboxedCommandRunner, which requires TrustProject; the main runner created above does not have that same restriction. An explicitly admitted operator definition requesting Shell therefore loses it on an untrusted checkout, even when an ordinary root has Shell available through its normal permission flow. allowShell=true later cannot put a missing tool back into the base.

Could we derive root Shell availability from the main runner before applying the definition scope, while preserving no-FS and deployment restrictions? A regression using an explicit definition, TrustProject=false, and a configured Ask would pin the distinction between project-content trust and root tool permissions.

// agent-bound session's create path (AC1.10), so res.MountedClientMCPTools
// and broker are always empty here.
authority = res.Authority
sel = ProviderSelector{ProviderID: res.ProviderID, ModelID: res.ModelID, ReasoningEffort: res.ReasoningEffort}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Persisting effective reasoning effort here loses the distinction between explicit auto and an omitted selector when this configuration is reconstructed. With operator effort high, create-time auto correctly resolves to an empty effective value. The runtime-rebuild selector at service.go:6521 later passes that empty value back to resolveSessionEffort, where it means inherit the operator default. It becomes high, and the fixed-configuration check at 6815–6830 rejects the rebuild. This is reachable after a same-process MCP runtime revision change; it does not depend on the intentionally unsupported restart path.

Fork/Clear reconstruct the same empty selector at placement_successor.go:382, but accept and persist the changed effort instead.

When reconstructing an already-bound session’s effective configuration, could we encode stored empty effort as explicit auto, while retaining explicit successor overrides? Please cover both a runtime revision rebuild and Fork/Clear with operator default high and initial request auto.

t.Fatalf("minted Authority has a nil Ceiling — GrantToolAuthority/CompleteWorkspaceEnrollment would treat this agent-bound session as unrestricted")
}
if !authority.Ceiling.Contains(authority.CapabilitySet) {
t.Fatalf("minted Ceiling does not contain the minted CapabilitySet: ceiling=%v capability=%v", authority.Ceiling.Tools, authority.CapabilitySet.Tools)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we assert the production-minted ceiling’s exact contents against an independently specified fixture here? Checking expected tools are present and Ceiling.Contains(CapabilitySet) still passes if an undeclared tool is added only to the ceiling. The aggregate grant-rejection tests construct their own ceilings, so they wouldn’t catch that factory regression either.

An exact expected capability/ceiling assertion, followed by an out-of-definition grant attempt that must fail without mutation, would pin the nonwidenable-ceiling contract through the actual construction path. Explicitly denied MCP and automatically added tools would make useful negative controls alongside it.

This branch was successfully deployed

1 active deployment
Preview — c4b5b1fe 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.

2 participants