Skip to content

docs: plan workspace enrollment authority repair - #1741

Merged
jhrozek merged 6 commits into
mainfrom
plan/workspace-enrollment-authority
Sep 24, 2026
Merged

jhrozek merged 6 commits into
mainfrom
plan/workspace-enrollment-authority

Conversation

@jbeda

@jbeda jbeda commented Sep 21, 2026 •

Copy link
Copy Markdown
Contributor

Plan / Interface

Relates to #1740.

This plan records the authority repair contract before implementation:

  • persist a bounded, presence-bearing private ledger of dynamically installed broker registration keys;
  • replace only that bundle while retaining all other carried authority;
  • fail a broker/non-broker registration-key collision as one terminal, no-partial enrollment;
  • retain already-corrupted legacy snapshots without heuristic repair and require a fresh session for corrected behavior.

It proposes ADR 0358, which supersedes only ADR 0335's no-refresh-specific-persisted-state clause.

Verification

  • bash .claude/skills/to-acceptance-plan/scripts/check-acceptance-plan.sh docs/acceptance/workspace-enrollment-authority.md
  • bash .claude/skills/to-acceptance-plan/scripts/check-acceptance-plan-test.sh
  • task docs

The plan requires human merge before /plan-orchestrate implements it.

Revisions

  • Rebased onto main after feat(mcp): reconcile sources and refresh session tools #1673 (direct MCP source reconciliation, ADR 0355). The ADR is renumbered from 0350 to 0358 because main now has its own ADRs 0350-0357.
  • The plan names direct MCP refresh (RefreshMcpSources / Session.GrantToolAuthority) as a separate add-only path that refuses broker-bound sessions, adds AC1.5 pinning that refusal, and the ADR records why ADR 0355's union-and-availability model is rejected for broker keys.
  • 2026-09-23 review amendments (recorded under Human decisions): authority and ledger set together with absent-by-default provenance, excluded keys retained in the ledger, collision refusal on every engine build, and pending enrollment cleared on collision.
  • Implementation is stacked in fix(enrollment): preserve workspace enrollment authority #1809.
  • Review feedback (JAORMX): AC3.1, the collision-cleanup decision, and ADR decision 5 now separate clearing the pending enrollment from the session being usable (an incoming-key collision leaves it usable; a recorded-key collision keeps engine builds refused until the configuration is corrected). The older-binary re-save caveat is removed: no upgrade compatibility work is required, and sessions without provenance are refused with a fresh-session error.

@jbeda

jbeda commented Sep 22, 2026

Copy link
Copy Markdown
Contributor Author

Open design question: runtime-only vs durable broker provenance

The implementation does not restore tool implementations from a session snapshot. On run entry it reconstructs an engine from current server composition. For ordinary tools that is sufficient.

Broker tools are intentionally different after a process restart: rehydrateSession rebuilds the per-session engine in exact-tools mode with no broker tools. It does not attach, rebind, rediscover, or enroll ToolHive on an ordinary prompt. The persisted binding and authority names are inert until the owner explicitly invokes workspace enrollment again. See internal/adapter/server/service.go (rehydrateSession) and internal/adapter/server/workspace_enrollment_test.go (post-restart ordinary run vs explicit refresh).

That creates two possible scopes for #1740:

  1. In-process / initial enrollment fix only. Preserve the pre-enrollment authority and track the installed broker bundle only in the live attachment or session-engine state. This fixes the clobbering regression without a new snapshot field. After a process restart, a later explicit enrollment can install fresh broker wrappers, but the system cannot precisely remove old broker capability names while preserving unrelated authority and deliberate attenuation.

  2. Exact replacement across restart. Persist a narrow, bounded, presence-bearing set of dynamic broker registration keys. On explicit re-enrollment after restart, remove only that old broker set and add the newly registered one. This supports exact replacement but adds durable provenance and requires an ADR-level decision.

The existing plan/ADR currently assumes option 2. That should not be merged until we decide whether same-session exact replacement after a broker/process restart is required. If it is not, the plan should be narrowed to option 1 and the durable provenance ADR removed.

Related edge case: a broker/non-broker duplicate registration key must either be rejected as a whole-bundle enrollment failure or receive a specified precedence rule. Namespacing makes this unusual, but it is possible when two sources expose the same mcp__<server>__<tool> key.

@jhrozek
jhrozek force-pushed the plan/workspace-enrollment-authority branch from bd51e49 to 9bacc04 Compare September 23, 2026 15:11
@jhrozek
jhrozek force-pushed the plan/workspace-enrollment-authority branch from 9bacc04 to 943bc9a Compare September 23, 2026 20:43
jhrozek added a commit that referenced this pull request Sep 23, 2026
Workspace enrollment replaced the session's whole tool capability set with
the latest broker catalogue, discarding unrelated authority. Restart and
successor paths had no durable way to tell stale broker keys from
deliberately retained capabilities, and a cross-source registration
collision could make a durable key resolve to a different implementation.

Persist a bounded broker-key ledger with a presence bit on the Session
aggregate, through sessnap and eventsource. The server sets authority and
ledger together on every root-creation and authority-carrying path (clear,
fork, and CreateSession with a source session); session.New starts with
absent provenance, so a path that forgets the ledger refuses enrollment.
Completion replaces only the prior broker bundle and keeps excluded keys in
the ledger, so an exclusion survives a refresh that omits the key.

Composition registers broker tools last against frozen registration keys
and reports a collision as a typed, presentation-safe error that unwraps to
failed precondition. A recorded broker key still held in authority and now
owned by a non-broker tool fails every engine build. An enrollment-time
collision or ledger overflow clears the pending enrollment; mecatui shows
it as a terminal failure without sending a retained prompt.

Offline tests cover every acceptance criterion in
docs/acceptance/workspace-enrollment-authority.md: aggregate, persistence,
restart, successors, composition (the real Build with a configured deny),
server end to end, and TUI.

Implements the plan in #1741.

Relates to #1740.

Co-Authored-By: mecatl <noreply@mecatl.dev>

@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.

The approach looks good: track the broker-installed keys, replace only that bundle, and preserve unrelated authority and existing exclusions.

Two non-blocking notes:

  • Please distinguish clearing the pending enrollment from making the session usable again. An incoming-tool collision can leave the prior session usable, but a conflict involving a recorded broker key still held in authority blocks engine construction until the configuration is corrected. AC3.1's “session accepts prompts again” wording should reflect that distinction.
  • We’re moving fast and don’t need upgrade compatibility or migration work here. Refusing enrollment for sessions without provenance and requiring a fresh session is acceptable; a basic actionable error is enough.

Approving the plan. This review does not cover the implementation in #1809.

@jhrozek
jhrozek force-pushed the plan/workspace-enrollment-authority branch from 943bc9a to 4e7142f Compare September 24, 2026 10:05
jhrozek added a commit that referenced this pull request Sep 24, 2026
Main now has its own ADR 0356, so the workspace enrollment authority
decision record becomes ADR 0357.

Review feedback on #1741:

- Clearing the pending enrollment is not the same as the session being
  usable again. AC3.1, the collision-cleanup decision, and ADR decision 5
  now say that an incoming-key collision leaves the session usable, while a
  collision on a recorded broker key still held in authority keeps engine
  builds refused until the operator corrects the configuration.
- No upgrade compatibility work is required. Drop the older-binary
  re-save caveat; sessions without provenance are refused and a fresh
  session is the recovery path.

Relates to #1740.

Co-Authored-By: mecatl <noreply@mecatl.dev>
jhrozek added a commit that referenced this pull request Sep 24, 2026
Workspace enrollment replaced the session's whole tool capability set with
the latest broker catalogue, discarding unrelated authority. Restart and
successor paths had no durable way to tell stale broker keys from
deliberately retained capabilities, and a cross-source registration
collision could make a durable key resolve to a different implementation.

Persist a bounded broker-key ledger with a presence bit on the Session
aggregate, through sessnap and eventsource. The server sets authority and
ledger together on every root-creation and authority-carrying path (clear,
fork, and CreateSession with a source session); session.New starts with
absent provenance, so a path that forgets the ledger refuses enrollment.
Completion replaces only the prior broker bundle and keeps excluded keys in
the ledger, so an exclusion survives a refresh that omits the key.

Composition registers broker tools last against frozen registration keys
and reports a collision as a typed, presentation-safe error that unwraps to
failed precondition. A recorded broker key still held in authority and now
owned by a non-broker tool fails every engine build. An enrollment-time
collision or ledger overflow clears the pending enrollment; mecatui shows
it as a terminal failure without sending a retained prompt.

Offline tests cover every acceptance criterion in
docs/acceptance/workspace-enrollment-authority.md: aggregate, persistence,
restart, successors, composition (the real Build with a configured deny),
server end to end, and TUI.

Implements the plan in #1741.

Relates to #1740.

Co-Authored-By: mecatl <noreply@mecatl.dev>
@jhrozek

jhrozek commented Sep 24, 2026 •

Copy link
Copy Markdown
Contributor

Thanks, Joe. We went with option 2: exact replacement across restart, using durable provenance.

Option 1 fixes the clobbering, but only until the process restarts. After a restart, a new enrollment has no record of which authority entries were broker tools. It can then either keep the old broker names (they go stale and may later match a different tool) or drop authority it can't identify. Clear and fork have the same gap. Their successors carry the authority but not the knowledge of which names came from the broker.

The plan (now ADR 0358 after renumbering on rebase) keeps the durable part small:

  • The session stores only broker registration keys, capped at 256 keys and 32 KiB, with a presence bit. No endpoints, OAuth state, or runtime handles.
  • A restart still rebuilds without broker tools. As you described, an ordinary prompt never attaches, rebinds, or enrolls. The ledger only lets a later explicit enrollment remove exactly the old broker keys.
  • Sessions without a recorded ledger are refused at enrollment with "create a fresh session". There is no migration or repair.

On the duplicate-key edge case: a collision between a broker tool and a non-broker tool rejects the whole enrollment. There is no precedence rule. The error names only the conflicting key, and the pending enrollment is cleared.

JAORMX approved the plan with that scope. The implementation is in #1809, stacked on this PR.

jbeda and others added 6 commits September 24, 2026 13:08
Co-Authored-By: Mecatl <noreply@mecatl.dev>
Co-Authored-By: Mecatl <noreply@mecatl.dev>
Revise the proposed plan and ADR 0350 after tracing the implementation:

- Name the engine API: add Session.WorkspaceEnrollmentBrokerKeys and
  Session.RestoreWorkspaceEnrollmentBrokerKeys, and classify the changed
  CompleteWorkspaceEnrollment semantics as a breaking change.
- Specify how the ledger and its presence bit are encoded in sessnap and
  eventsource, the key bounds, and validation of raw JSON before decoding.
- Add an authority-boundary section: no supported journey lets a child
  session enroll, so enrollment cannot widen narrowed child authority.
- Keep a previously recorded broker key excluded when carried authority no
  longer holds it; admit keys the ledger has never seen.
- Separate restart from successors: a restart keeps the ledger but no broker
  runtime; clear and fork copy the ledger with authority and start a fresh
  attachment.
- Refuse legacy provenance before any reopen, persist, reset, discovery, or
  consent side effect.

Relates to #1740.

Co-Authored-By: mecatl <noreply@mecatl.dev>
A review of the spike implementation found gaps in the contract itself.
Record the human decisions of 2026-09-23:

- Authority and the broker-key ledger are always set together. Root
  creation records exactly the broker keys it grants; clear, fork, and
  CreateSession with a source session copy the source ledger. session.New
  starts with absent provenance, so a path that forgets the ledger refuses
  enrollment instead of keeping stale broker keys. The restore method is
  write-once.
- Excluded broker keys stay in the ledger (new ledger = completed bundle
  plus prior excluded keys), so an exclusion survives a refresh that omits
  the key. Exceeding the ledger bounds fails enrollment without mutation.
- A recorded broker key still held in authority that a non-broker tool now
  owns fails every engine build (rehydrate, successor, enrollment) with a
  failed-precondition error naming only the key. Keys that are only
  excluded never trigger this.
- An enrollment-time collision or ledger overflow clears and persists the
  pending enrollment, so the session accepts prompts again.

The acceptance criteria follow: AC1.1 covers creation-time keys, new AC1.4
covers exclusion memory, AC2.1 gains the collision exception, AC2.3 covers
carryover, and AC3.1 requires an end-to-end proof that pending is cleared.
The compatibility section notes that an older binary re-saving a snapshot
drops the ledger.

Relates to #1740.

Co-Authored-By: mecatl <noreply@mecatl.dev>
Main now has its own ADRs 0350-0355, so the workspace enrollment authority
decision record becomes ADR 0356. The plan, the ADR index, and ADR 0335's
supersession note follow.

#1673 added direct MCP refresh (ADR 0355), a separate add-only authority
path through Session.GrantToolAuthority. Name it in the plan's authority
boundary: it refuses broker-bound sessions and broker mode clears the
global MCP inputs, so the two paths never act on the same session. Add
AC1.5 pinning that refusal. In the ADR, link ADR 0355 and record why its
union-and-availability model is rejected for broker keys.

Relates to #1740.

Co-Authored-By: mecatl <noreply@mecatl.dev>
Main now has its own ADRs 0356 and 0357, so the workspace enrollment authority
decision record becomes ADR 0358.

Review feedback on #1741:

- Clearing the pending enrollment is not the same as the session being
  usable again. AC3.1, the collision-cleanup decision, and ADR decision 5
  now say that an incoming-key collision leaves the session usable, while a
  collision on a recorded broker key still held in authority keeps engine
  builds refused until the operator corrects the configuration.
- No upgrade compatibility work is required. Drop the older-binary
  re-save caveat; sessions without provenance are refused and a fresh
  session is the recovery path.

Relates to #1740.

Co-Authored-By: mecatl <noreply@mecatl.dev>
@jhrozek
jhrozek force-pushed the plan/workspace-enrollment-authority branch from 4e7142f to 68be96d Compare September 24, 2026 11:16
jhrozek added a commit that referenced this pull request Sep 24, 2026
Workspace enrollment replaced the session's whole tool capability set with
the latest broker catalogue, discarding unrelated authority. Restart and
successor paths had no durable way to tell stale broker keys from
deliberately retained capabilities, and a cross-source registration
collision could make a durable key resolve to a different implementation.

Persist a bounded broker-key ledger with a presence bit on the Session
aggregate, through sessnap and eventsource. The server sets authority and
ledger together on every root-creation and authority-carrying path (clear,
fork, and CreateSession with a source session); session.New starts with
absent provenance, so a path that forgets the ledger refuses enrollment.
Completion replaces only the prior broker bundle and keeps excluded keys in
the ledger, so an exclusion survives a refresh that omits the key.

Composition registers broker tools last against frozen registration keys
and reports a collision as a typed, presentation-safe error that unwraps to
failed precondition. A recorded broker key still held in authority and now
owned by a non-broker tool fails every engine build. An enrollment-time
collision or ledger overflow clears the pending enrollment; mecatui shows
it as a terminal failure without sending a retained prompt.

Offline tests cover every acceptance criterion in
docs/acceptance/workspace-enrollment-authority.md: aggregate, persistence,
restart, successors, composition (the real Build with a configured deny),
server end to end, and TUI.

Implements the plan in #1741.

Relates to #1740.

Co-Authored-By: mecatl <noreply@mecatl.dev>
@jhrozek
jhrozek merged commit 3e80d41 into main Sep 24, 2026
31 checks passed
@jhrozek
jhrozek deleted the plan/workspace-enrollment-authority branch September 24, 2026 11:29
jhrozek added a commit that referenced this pull request Sep 24, 2026
Workspace enrollment replaced the session's whole tool capability set with
the latest broker catalogue, discarding unrelated authority. Restart and
successor paths had no durable way to tell stale broker keys from
deliberately retained capabilities, and a cross-source registration
collision could make a durable key resolve to a different implementation.

Persist a bounded broker-key ledger with a presence bit on the Session
aggregate, through sessnap and eventsource. The server sets authority and
ledger together on every root-creation and authority-carrying path (clear,
fork, and CreateSession with a source session); session.New starts with
absent provenance, so a path that forgets the ledger refuses enrollment.
Completion replaces only the prior broker bundle and keeps excluded keys in
the ledger, so an exclusion survives a refresh that omits the key.

Composition registers broker tools last against frozen registration keys
and reports a collision as a typed, presentation-safe error that unwraps to
failed precondition. A recorded broker key still held in authority and now
owned by a non-broker tool fails every engine build. An enrollment-time
collision or ledger overflow clears the pending enrollment; mecatui shows
it as a terminal failure without sending a retained prompt.

Offline tests cover every acceptance criterion in
docs/acceptance/workspace-enrollment-authority.md: aggregate, persistence,
restart, successors, composition (the real Build with a configured deny),
server end to end, and TUI.

Implements the plan in #1741.

Relates to #1740.

Co-Authored-By: mecatl <noreply@mecatl.dev>
jhrozek added a commit that referenced this pull request Sep 25, 2026
Workspace enrollment replaced the session's whole tool capability set with
the latest broker catalogue, discarding unrelated authority. Restart and
successor paths had no durable way to tell stale broker keys from
deliberately retained capabilities, and a cross-source registration
collision could make a durable key resolve to a different implementation.

Persist a bounded broker-key ledger with a presence bit on the Session
aggregate, through sessnap and eventsource. The server sets authority and
ledger together on every root-creation and authority-carrying path (clear,
fork, and CreateSession with a source session); session.New starts with
absent provenance, so a path that forgets the ledger refuses enrollment.
Completion replaces only the prior broker bundle and keeps excluded keys in
the ledger, so an exclusion survives a refresh that omits the key.

Composition registers broker tools last against frozen registration keys
and reports a collision as a typed, presentation-safe error that unwraps to
failed precondition. A recorded broker key still held in authority and now
owned by a non-broker tool fails every engine build. An enrollment-time
collision or ledger overflow clears the pending enrollment; mecatui shows
it as a terminal failure without sending a retained prompt.

Offline tests cover every acceptance criterion in
docs/acceptance/workspace-enrollment-authority.md: aggregate, persistence,
restart, successors, composition (the real Build with a configured deny),
server end to end, and TUI.

Implements the plan in #1741.

Relates to #1740.

Co-Authored-By: mecatl <noreply@mecatl.dev>
@toolhive-release-app toolhive-release-app Bot mentioned this pull request Sep 25, 2026
2 tasks
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.

3 participants