Skip to content

feat(runtime): register the managed local owner as an exact-only wrapper and add the neutral allocator port - #2259

Merged
thymikee merged 4 commits into
adr0021/u1-owner-kindfrom
adr0021/u3-managed-owner
Sep 3, 2026
Merged

feat(runtime): register the managed local owner as an exact-only wrapper and add the neutral allocator port#2259
thymikee merged 4 commits into
adr0021/u1-owner-kindfrom
adr0021/u3-managed-owner

Conversation

@thymikee

@thymikee thymikee commented Sep 3, 2026

Copy link
Copy Markdown
Member

Summary

ADR 0021 foundations, unit 3. Stacked on #2258 (adr0021/u1-owner-kind); U4 stacks on this.

Unit 1 added the managed-local owner kind and left the gateway's exact-owner arm for it failing
closed. This unit gives that arm a registry, the owner it selects, and agent-device's own allocator
port. Nothing in production registers a managed owner or calls an allocator, so every arm here
is reached from tests only; the point of the unit is that the arms exist and fail closed.

What changes for command authors and platform behavior:

  • A new gateway option, managedOwners, is the only place a managed local owner can be registered,
    and only the managed-local arm of selectExactOwner reads it. Ordinary selection cannot reach
    a managed owner by construction rather than by a check.
  • A managed binding is a local family binding republished under the managed owner, with
    twenty-seven cells withheld as owner-capability-missing and the operations filtered by those
    facts (R66). A command author sees the same narrowDeviceBinding refusal any unsupported cell
    produces.
  • The allocator port is a types-only contracts subpath with a scripted fake. No socket code, no
    allocator package dependency, no production call site.

12 files touched, no scope growth beyond the runtime-gateway/contracts surface the unit names;
src/platform-runtime.ts is deliberately not edited (no production producer of managed owners
exists yet, so createPlatformRuntimeGateway gains no pass-through).

The exact-only registry

createComposedPlatformRuntimeGateway gains a managedOwners list that only the managed-local
arm of selectExactOwner reads. selectOrdinaryProvider, inspectFacts and the ordinary bind
arm never see it, and providerModules pairs one ProviderDeviceRuntime with one
provider-runtime owner — so ordinary selection cannot be handed a managed owner. A
@ts-expect-error pins that: a managed ref is not assignable to a provider module's owner. A
duplicate instance is refused at composition, an unregistered instance still answers
owner-unavailable, and a registered one is composed once.

The wrapper

src/platform-runtime-managed-owner.ts (root zone, no platform imports) binds only under an
exact-owner intent naming itself, loads the device's own family owner through the gateway's loader,
delegates with { kind: 'ordinary' } — a family owner refuses a foreign exact owner — and
republishes the binding under the managed owner. It does not decode the fence: what a managed
binding fence proves is the device-claim gate's business, and a second reader would be a second
answer. ownsDevice returns false.

Twenty-seven cells are withheld, enumerated by mechanics rather than by catalog group:

  • device lifecycle: ensureReady, bootTarget, bootTargetHeadless, shutdownTarget, plus
    deployApp and deployMaterializedApp — both family deployment runtimes ensure readiness before
    installing, and deployAppUse requires deployApp alone, so nothing else would have refused it;
  • application cells that boot or shut the device down: prepareApplicationOpen,
    prepareAppleRunner, closeApplication, finalizeApplicationClose;
  • the twelve durable-capture cells (app log, screen recording, audio probe, native perf capture),
    which a managed binding could never reattach because the family runtime stamps envelopes with its
    own local owner;
  • captureScreenshot, setSetting, readClipboard, writeClipboard, openApplication — none is
    declared device lifecycle, but each Apple family-runtime implementation can boot the simulator
    lazily below cell-selection granularity (screenshot retries through a boot on a shutdown failure;
    the other three resolve a local interactor the same lazy way), which is exactly the implicit-boot
    fallback ADR-0021 §3 rules out for a managed device.

A managed session cannot be opened in the foundations. A managed binding loses every command
whose runtime use requires a withheld cell: install (and push-notification delivery), apps and
app-state, device boot and device shutdown, open and close, and the durable capture starts
(logs start, record start, audio probe capture, native perf capture). That is the intended shape
of the foundations — the allocator owns the device's lifecycle — not a gap to work around in daemon
code.

Named out of scope

  • Withholding cells is not a complete lifecycle exclusion, and the code says so. The four
    lazily-booting Apple cells named above are now withheld rather than left as a caveat — an earlier
    revision of this PR retained them and disclosed the gap instead of closing it; review caught that
    the disclosure preserved the exact bypass ADR-0021 §3 rules out, so the fix withholds them.
  • Pre-binding readiness is the same class, at the daemon level, and stays open.
    session-device-resolutiondevice-readyplatform-runtime-device-ready boots a device
    through direct simctl/adb before any binding exists, gated only by provider ownership — below
    where a managed binding's cell withholding can reach at all. The exclusion above covers binding
    cells only, and the exclusion test says so.
  • Closing the daemon-level gap is its own follow-up: pre-binding readiness moves under the binding
    once a managed owner is actually reachable from production (no unit before the allocation-flow
    unit can exercise it end to end).
  • Durable captures stay unavailable until family runtimes stamp the outward owner on their
    envelopes (adoption compares the envelope's owner key with the binding's). Same family-runtime
    change; not attempted here.
  • Delegating with an ordinary intent means local exact-owner recovery semantics never apply
    under a managed binding — consistent with durable captures being unavailable. The delegation test
    pins the choice, so forwarding a local-family exact intent later is a deliberate change.

The allocator port

@agent-device/contracts/managed-device-allocation is agent-device's own interface to a
managed-device allocator: lease request, lookup, supersession, cancellation, renewal, release,
activation confirmation, identity status, and removal acknowledgement. Types only, with no
dependency on any allocator package and no socket code. The member and field names match the
allocator's published contract (requestGeneration, identityIncarnationId,
expectedRequestGeneration, confirmLeaseActivation, getManagedIdentityStatus,
acknowledgeManagedIdentityRemoval, admission: 'fail-fast',
activation: 'direct' | 'external-fence', LEASE_ENVIRONMENT_INVALID { platform, key }, and the
{ platform: 'ios', deviceSetPath } | { platform: 'android', adbServerPort } projection) so the two
sides cannot drift.

Shaping notes:

  • Failures that would otherwise be invented error codes — a busy requester lane, an unknown attempt
    — are modelled where the contract already puts them: a refusal reason and a request state on
    LeaseRequestStatus. Protocol-version skew belongs to the unit that opens the socket.
  • ManagedLeaseEnvironment and LeaseEnvironmentError are declared but have no reader yet. The
    reader, and the mapping from agent-device's own device families onto the port's
    'ios' | 'android' vocabulary, land with the allocation-flow unit that first turns a grant into
    a DeviceInfo. Both carry the repo's one-line fallow-ignore-next-line unused-type idiom rather
    than a baseline entry.
  • The port's only implementation is a scripted fake under src/__tests__/test-utils/*.fixtures.ts.

Pins moved

  • Three-place pin for the new contracts subpath: packages/contracts/package.json#exports,
    CONTRACT_EXPORTS, and a FACADE_BUDGETS row of 1 (types only, so the entry evaluates itself).
  • HUB_BUDGETS['src/platform-runtime.ts'] 47 → 48, measured: the composed gateway now composes the
    wrapper, whose own value imports (the owner contract, kernel errors) were already in that closure.
  • CONTEXT.md: Managed device allocator port.

Validation

Evidence gathered at e2978a6637 (rebased onto #2258's blocking fixes; includes the withhold-cells
review fix below). Published and reported — CI on the head is the authority.

pnpm check:affected --run fail-opens to the full 57-check set (the diff touches tooling files), and
the whole set passed at this commit: format, lint, typecheck, layering, di-seams,
check:fallow --base origin/main (clean over 34 changed files), build, package, integration-node,
macOS command coverage, vitest-related, unit + smoke, provider-integration, integration-progress,
replay-compat, daemon-wire-compat, production-exports, bundle-owner-files, command-docs,
agent-guidance, xctest-selection, maestro-conformance, and the gate/mutation/coverage model suites.
The GitHub-authoritative lanes (device replay, Swift/Android helper builds, coverage, mutation, fuzz)
are skipped locally by design.

Two earlier full runs on this branch each aborted at vitest-related on a different test timing
out at the 5 s vitest limit under parallel load —
test/integration/provider-scenarios/ios-lifecycle.test.ts (4.9 s in isolation) and
src/daemon/__tests__/gesture-admission-parity.test.ts (3.3 s in isolation, already flagged by the
slow-test gate at a 2.5 s budget). Both pass in isolation, both are timeouts rather than assertion
failures, and neither is on a path this change touches. check:affected aborts at the first failed
lane, so those two runs did not reach the lanes after vitest-related; the clean third run did, and
CI on the head is what settles it.

Regression tests, each with the revert that makes it fail (all executed as mutations against the
real code; every one went red):

  • the withheld set — dropping deployApp/deployMaterializedApp, prepareAppleRunner, the
    durable-capture cells, or the five lazily-booting cells (captureScreenshot, setSetting,
    readClipboard, writeClipboard, openApplication) from the wrapper's list; returning
    binding.operations unfiltered;
  • narrowDeviceBinding refusals for appsRuntimeUse, appStateUse, bootTargetUse,
    shutdownTargetUse and deployAppUse;
  • the wrapper — forwarding the managed exact intent to the family owner (the family fixture throws
    on a foreign exact owner), dropping the owner rewrite (bindAndValidate then rejects), accepting
    an ordinary or foreign intent, or not forwarding disposal;
  • the gateway — restoring the fail-closed managed-local arm, dropping the composition-time
    duplicate check, or composing a fresh wrapper per bind (the second registration is then refused);
  • providerModeMatchesOwner's managed-local arm accepting transport-composed (fixed on feat(runtime): add managed-local ownership and the exact-only managed runtime #2258
    and rebased in here): accepts a managed binding whose local facts report a transport-composed device binds through the gateway with a transport-composed local family device and asserts
    success — replacing an earlier version of this test that asserted the opposite (rejection), which
    is what review caught: the arm rejected the mode the PR's own known-gap note said it should accept;
  • the scripted fake — consuming steps from one shared queue, or recording no calls.

No device-facing behavior changes, so no simulator/emulator evidence is owed: nothing in production
can select a managed owner yet.

@github-actions

github-actions Bot commented Sep 3, 2026

Copy link
Copy Markdown

Size Report

Metric Base Current Diff
JS raw 2.59 MB 2.59 MB +2.2 kB
JS gzip 872.2 kB 872.9 kB +711 B
npm bundled raw 2.59 MB 2.59 MB +2.2 kB
npm bundled gzip 872.2 kB 872.9 kB +711 B
npm tarball 1.02 MB 1.03 MB +638 B
npm unpacked 3.47 MB 3.47 MB +2.2 kB
npm clean-installed 3.47 MB 3.47 MB +2.2 kB

npm unpacked components

Component Base Current Diff
JS / dist source 2.75 MB 2.75 MB +2.2 kB
Apple runner source/project 544.4 kB 544.4 kB 0 B
Apple snapshot presentation source 33.8 kB 33.8 kB 0 B
macOS helper source 54.8 kB 54.8 kB 0 B
Android helper artifacts 43.3 kB 43.3 kB 0 B
Other package files 46.4 kB 46.4 kB 0 B

Startup median (7 runs, lower is better):

Scenario Base Current Diff
CLI --version 29.9 ms 30.3 ms +0.4 ms
CLI --help 80.2 ms 80.8 ms +0.6 ms

Top changed chunks: no changes in the largest emitted chunks.

Top changed packed files

Packed file Base Current Diff
dist/src/platform-runtime.js 12.4 kB 14.6 kB +2.2 kB

@thymikee

thymikee commented Sep 3, 2026

Copy link
Copy Markdown
Member Author

BLOCKED at 3d29f60eaaa48406e570c231d9aebf29c86071a6.

  1. The managed-owner wrapper retains captureScreenshot, setSetting, clipboard, and openApplication, whose Apple family-runtime cells can lazily boot the Simulator. That violates ADR-0021 §3's hard boundary: managed lifecycle/readiness belongs to Simlock and no handler path may fall back to direct lifecycle tooling. The current caveat/tests preserve rather than block the bypass; remove/own the implicit readiness path or withhold every such cell and add a managed-binding regression.

  2. The PR is CONFLICTING/DIRTY against adr0021/u1-owner-kind; iOS Smoke is still pending.

The exact-only registry behavior and types are otherwise coherent.

@thymikee

thymikee commented Sep 3, 2026

Copy link
Copy Markdown
Member Author

Fixed at e2978a6 (rebased onto #2258's blocking fix, then this fix on top).

1. Lazy-boot cells. Withheld captureScreenshot, setSetting, readClipboard, writeClipboard and openApplication from the managed binding instead of disclosing them as a retained caveat — they're now in WITHHELD_MANAGED_OPERATIONS's fourth group, twenty-seven cells total (was twenty-two). The pre-binding readiness gap you didn't flag (session-device-resolutiondevice-readyplatform-runtime-device-ready booting below where any binding-level withholding can reach) stays open and out of scope, as before — it's a daemon-level change, not a cell-selection one.

2. Rebase + providerModeMatchesOwner. Rebased onto #2258's fix (local || transport-composed for the managed-local arm). I flipped this PR's own rejects a managed binding whose local facts report a transport-composed device test, which was pinning the wrong outcome — it asserted rejection, while this PR's own body already said the arm should accept transport-composed. It's now accepts a managed binding whose local facts report a transport-composed device and binds through the real gateway (not just the wrapper in isolation) with a transport-composed local family device, asserting success.

Planted-red verified both: reverting the withheld-cells addition fails withholds every lifecycle-bearing and durable capture cell and keeps the rest; reverting the providerModeMatchesOwner arm reproduces the exact runtime-contract-invalid rejection the flipped test now catches.

pnpm check:affected --run green at e2978a6 (fail-opens to the full 57-check set, diff touches tooling files): format/lint/typecheck/layering/di-seams/fallow/build/package/integration-node/macOS coverage/vitest-related/integration-progress/replay-compat/daemon-wire-compat/gate-manifest/depgraph/tmpdir-leaks/coverage-model/production-exports/bundle-owner-files/fixture-cache/fixture-fallback/command-docs/agent-guidance/xctest-selection/maestro-conformance/mutation-model all clean.

@thymikee thymikee closed this Sep 3, 2026
@thymikee thymikee reopened this Sep 3, 2026
…per and add the neutral allocator port

ADR 0021 foundations, unit 3. Unit 1 added the `managed-local` owner kind and left the gateway's
exact-owner arm for it failing closed; this unit gives that arm a registry and the owner it selects.
Nothing in production registers a managed owner yet, so both are reached from tests only.

- `createComposedPlatformRuntimeGateway` gains a `managedOwners` list that only the `managed-local`
  arm of `selectExactOwner` reads. `selectOrdinaryProvider`, `inspectFacts` and the ordinary `bind`
  arm never see it, and `providerModules` pairs one provider-runtime owner with one
  `ProviderDeviceRuntime`, so ordinary selection cannot reach a managed owner by construction
  rather than by a check. A duplicate instance is refused at composition.

- The wrapper (src/platform-runtime-managed-owner.ts, root zone, no platform imports) binds only
  under an exact-owner intent naming itself, loads the device's own family owner through the
  gateway's loader, delegates with an ordinary intent — a family owner refuses a foreign exact
  owner — and republishes the binding under the managed owner. It does not read the fence: what a
  managed binding fence proves is the device-claim gate's business. `ownsDevice` returns false.

- Twenty cells are withheld as `owner-capability-missing`, enumerated by mechanics rather than by
  catalog group: the four device-lifecycle cells, the four application cells that boot or shut the
  device down (`prepareApplicationOpen`, `prepareAppleRunner`, `closeApplication`,
  `finalizeApplicationClose`), and the twelve durable-capture cells, which a managed binding could
  never reattach because the family runtime stamps envelopes with its own local owner. The
  operations are then filtered by those facts, so an operation cannot outlive its own fact.

- `@agent-device/contracts/managed-device-allocation` is agent-device's own allocator port: lease
  request, lookup, supersession, cancellation, renewal, release, activation confirmation, identity
  status, removal acknowledgement, and the typed environment projection. Types only, named to match
  the allocator's published contract so the two sides cannot drift, with no dependency on any
  allocator package. Its only implementation is a scripted fake under `*.fixtures.ts`.

- Budgets: the new contracts entry surface is a one-module closure; the `src/platform-runtime.ts`
  hub moves 47 -> 48 for the wrapper, whose own value imports were already in that closure.
…d trim the allocator port

Review findings on the managed local owner.

- `deployApp` and `deployMaterializedApp` join the lifecycle group. Both family deployment runtimes
  ensure device readiness before installing, and `deployAppUse` requires `deployApp` alone — so
  `install` on a managed binding would have booted the allocator's device with nothing to refuse
  it. Twenty withheld cells become twenty-two, and the refused-uses test covers `deployAppUse`.

- The wrapper's doc comment no longer implies that withholding cells is a complete lifecycle
  exclusion: several retained Apple cells (screenshot capture, settings, clipboard, application
  launch) boot the simulator lazily inside the family runtime, where cell selection cannot reach.
  That is the same class as the pre-binding readiness path, and closing it is a family-runtime
  change.

- `readLeaseEnvironment` leaves the allocator port. It was beyond the vocabulary the contract
  fixes, and it made the scripted fake carry a real parser whose only test passed with every
  production line reverted. `ManagedLeaseEnvironment`, `ManagedLeaseEnvironmentKey` and
  `LeaseEnvironmentError` stay as types; the reader that produces them lands with the unit that
  first turns a grant into a device.

- CONTEXT.md drops an operation enumeration that was already incomplete.
… cells

Screenshot capture, settings, clipboard and application launch were retained on a managed
binding even though their Apple family-runtime implementations can boot the simulator lazily
below cell-selection granularity (screenshot's shutdown-failure retry boot; settings, clipboard
and application launch each resolve a local interactor the same way). That preserves rather than
blocks the exact bypass ADR-0021 section 3's hard boundary names: managed lifecycle/readiness
belongs to the allocator, and no handler path may fall back to direct lifecycle tooling.

Withhold captureScreenshot, setSetting, readClipboard, writeClipboard and openApplication
alongside the existing withheld groups. The wrapper's doc comment now names the pre-binding
readiness gap explicitly as the same class of follow-up, rather than folding it into a retained-
cells caveat that no longer applies. MANAGED_RETAINED_OPERATION moves to tapPoint, the cell the
fixture-based regression tests now use to prove something survives the wrapper.
@thymikee
thymikee force-pushed the adr0021/u1-owner-kind branch from c17b4b5 to a8ecbd2 Compare September 3, 2026 15:30
@thymikee
thymikee force-pushed the adr0021/u3-managed-owner branch from a3fd02a to 165d5c7 Compare September 3, 2026 15:30
@thymikee

thymikee commented Sep 3, 2026

Copy link
Copy Markdown
Member Author

Code review CLEAN at this exact head. The lifecycle bypass is fixed across all lazy Apple cells, operation facts are filtered correctly, and the gateway regression now proves the transport-composed bind inherited from #2258. Exact-only registration, disposal, duplicate refusal, and the allocator port are coherent. Merge after #2258; two exact-head Smoke lanes are still running.

@thymikee thymikee added the ready-for-human Valid work that needs human implementation, judgment, or maintainer merge label Sep 3, 2026
@thymikee
thymikee merged commit b8f5fad into adr0021/u1-owner-kind Sep 3, 2026
14 of 15 checks passed
@thymikee
thymikee deleted the adr0021/u3-managed-owner branch September 3, 2026 16:02
@github-actions

github-actions Bot commented Sep 3, 2026

Copy link
Copy Markdown
PR Preview Action v1.8.1
Preview removed because the pull request was closed.
2026-09-03 16:02 UTC

thymikee added a commit that referenced this pull request Sep 3, 2026
…per and add the neutral allocator port (#2259)

* feat(runtime): register the managed local owner as an exact-only wrapper and add the neutral allocator port

ADR 0021 foundations, unit 3. Unit 1 added the `managed-local` owner kind and left the gateway's
exact-owner arm for it failing closed; this unit gives that arm a registry and the owner it selects.
Nothing in production registers a managed owner yet, so both are reached from tests only.

- `createComposedPlatformRuntimeGateway` gains a `managedOwners` list that only the `managed-local`
  arm of `selectExactOwner` reads. `selectOrdinaryProvider`, `inspectFacts` and the ordinary `bind`
  arm never see it, and `providerModules` pairs one provider-runtime owner with one
  `ProviderDeviceRuntime`, so ordinary selection cannot reach a managed owner by construction
  rather than by a check. A duplicate instance is refused at composition.

- The wrapper (src/platform-runtime-managed-owner.ts, root zone, no platform imports) binds only
  under an exact-owner intent naming itself, loads the device's own family owner through the
  gateway's loader, delegates with an ordinary intent — a family owner refuses a foreign exact
  owner — and republishes the binding under the managed owner. It does not read the fence: what a
  managed binding fence proves is the device-claim gate's business. `ownsDevice` returns false.

- Twenty cells are withheld as `owner-capability-missing`, enumerated by mechanics rather than by
  catalog group: the four device-lifecycle cells, the four application cells that boot or shut the
  device down (`prepareApplicationOpen`, `prepareAppleRunner`, `closeApplication`,
  `finalizeApplicationClose`), and the twelve durable-capture cells, which a managed binding could
  never reattach because the family runtime stamps envelopes with its own local owner. The
  operations are then filtered by those facts, so an operation cannot outlive its own fact.

- `@agent-device/contracts/managed-device-allocation` is agent-device's own allocator port: lease
  request, lookup, supersession, cancellation, renewal, release, activation confirmation, identity
  status, removal acknowledgement, and the typed environment projection. Types only, named to match
  the allocator's published contract so the two sides cannot drift, with no dependency on any
  allocator package. Its only implementation is a scripted fake under `*.fixtures.ts`.

- Budgets: the new contracts entry surface is a one-module closure; the `src/platform-runtime.ts`
  hub moves 47 -> 48 for the wrapper, whose own value imports were already in that closure.

* fix(runtime): withhold the deployment cells from a managed binding and trim the allocator port

Review findings on the managed local owner.

- `deployApp` and `deployMaterializedApp` join the lifecycle group. Both family deployment runtimes
  ensure device readiness before installing, and `deployAppUse` requires `deployApp` alone — so
  `install` on a managed binding would have booted the allocator's device with nothing to refuse
  it. Twenty withheld cells become twenty-two, and the refused-uses test covers `deployAppUse`.

- The wrapper's doc comment no longer implies that withholding cells is a complete lifecycle
  exclusion: several retained Apple cells (screenshot capture, settings, clipboard, application
  launch) boot the simulator lazily inside the family runtime, where cell selection cannot reach.
  That is the same class as the pre-binding readiness path, and closing it is a family-runtime
  change.

- `readLeaseEnvironment` leaves the allocator port. It was beyond the vocabulary the contract
  fixes, and it made the scripted fake carry a real parser whose only test passed with every
  production line reverted. `ManagedLeaseEnvironment`, `ManagedLeaseEnvironmentKey` and
  `LeaseEnvironmentError` stay as types; the reader that produces them lands with the unit that
  first turns a grant into a device.

- CONTEXT.md drops an operation enumeration that was already incomplete.

* fix(runtime): withhold the lazily-booting Apple system and screenshot cells

Screenshot capture, settings, clipboard and application launch were retained on a managed
binding even though their Apple family-runtime implementations can boot the simulator lazily
below cell-selection granularity (screenshot's shutdown-failure retry boot; settings, clipboard
and application launch each resolve a local interactor the same way). That preserves rather than
blocks the exact bypass ADR-0021 section 3's hard boundary names: managed lifecycle/readiness
belongs to the allocator, and no handler path may fall back to direct lifecycle tooling.

Withhold captureScreenshot, setSetting, readClipboard, writeClipboard and openApplication
alongside the existing withheld groups. The wrapper's doc comment now names the pre-binding
readiness gap explicitly as the same class of follow-up, rather than folding it into a retained-
cells caveat that no longer applies. MANAGED_RETAINED_OPERATION moves to tapPoint, the cell the
fixture-based regression tests now use to prove something survives the wrapper.

* chore: retrigger CI (stale synchronize event after rebase)
thymikee added a commit that referenced this pull request Sep 3, 2026
… runtime (#2258)

* docs: trim the CONTEXT.md glossary within the guidance byte budget

CONTEXT.md sat at 11,992 of its 12,000-byte guidance budget, so no new domain term could be added
without first paying for it.

- Condense eighteen definitions that had grown past one line (platform leaf, command surface,
  runtime use, runner command traits, interactor, coordinate-first resolved element activation,
  parent-owned touch point, guarantee cell, delegation-on-error, ref frame, snapshot producer,
  snapshot policy facet, capture hint, regular presented-depth frontier, clip fold,
  AX-unavailable target invalidation, Maestro program, Maestro observation generation). The
  definitions keep their meaning; only the elaboration is gone.
- Move the five test-harness terms of 'Providers and tests' (provider-backed integration
  scenario, provider transcript, scenario transcript, in-process provider scenario harness, HTTP
  contract test) to docs/agents/domain.md, which AGENTS.md already routes to for domain
  vocabulary. None of them names a concept a command or a wire shape carries, and none appears in
  a test name.

CONTEXT.md is 10,517 bytes after this pass.

* feat(runtime): add the managed-local owner kind, device-claim rule, and managed binding fence

ADR 0021 foundations, unit 1. Nothing registers a managed local owner yet, so every arm below is
reached from tests only; the point of the unit is that the arms exist and fail closed.

- `RuntimeOwnerRef` gains `{ kind: 'managed-local'; instance }` with `managedLocalRuntimeOwner`:
  one owner per allocator instance, family-agnostic because the device carries its family. Every
  owner-kind discrimination becomes an exhaustive switch, so a fourth kind is a type error at each
  site: the owner key, the unavailable-facts provider mode, the durable envelope decode, and the
  gateway's provider-mode acceptance and exact-owner selection.
- `deviceClaimRuleForOwner` ('ordinary' | 'allocator-held' | 'none') in the new leaf
  src/daemon/device-claim-rule.ts replaces the boolean `isLocalDeviceClaimTarget`. Both claim
  gates switch on it, and the admission gate now evaluates it under every device-claim policy: the
  `transient-exclusive` condition moved inside the ordinary arm, so a managed owner is verified
  where an ordinary owner would never have touched the store.
- `requireAllocatorHeldDeviceClaim` (src/daemon/device-claim-allocator.ts) is the one read-only
  verifier both gates consult. It never acquires, never locks and never clears; in this unit it
  can only answer `binding-invalid`, `missing`, or `conflict`, because no allocator-held claim
  kind exists until unit 2. `allocatorHeldAdmissionError` answers each outcome with its own
  refusal through an exhaustive switch, so an outcome the verifier learns to produce is a
  compile error until it is answered.
- A missing allocator-held claim refuses with COMMAND_FAILED / `allocator-claim-missing`,
  `retriable: false`. It is deliberately not a `DeviceClaimConflictReason`: replay retries every
  conflict reason as infrastructure, and a managed identity no allocator activated is permanent.
- `managedBindingFence` / `decodeManagedBindingFence` encode `[requesterId, identityIncarnationId]`
  as the fence token and the request generation as its generation, so two requesters on one
  identity incarnation never share a fence. The ids are fenced verbatim, and the decoder accepts a
  token only if it re-encodes to itself.
- Claim admission now receives the binding intent the gateway bound, so an exact-owner fence
  reaches the gate unchanged. Session open still binds ordinarily and passes an ordinary intent:
  a managed local owner is therefore refused there structurally, and the Host open route replaces
  that intent when it lands.
- CONTEXT.md: managed local owner, device-claim rule, managed binding fence, request generation,
  identity incarnation.

* fix(daemon): decide allocator-held admission totally instead of by an optional error

`allocatorHeldAdmissionError` returned `AppError | undefined`, so its switch without a default
was never exhaustiveness-checked: TS2366 fires only when the return type excludes `undefined`,
`noImplicitReturns` is off, and oxlint has no exhaustiveness rule. A verifier outcome nobody
answered would therefore fall out as `undefined`, which both gates read as an admission — claim
admission throws nothing and session open proceeds to open the session on a device it never
verified.

Replace it with `decideAllocatorHeldAdmission`, returning
`{ admitted: true } | { admitted: false; error }`. The return type excludes `undefined`, so
dropping an arm is now a compile error at the switch, and a gate asks whether the outcome was
admitted rather than whether an error happened to come back. `buildAllocatorHeldRefusal` and the
admission gate are projections of that one decision.

* docs: restore the meaning five CONTEXT.md definitions lost in the trim

The condensing pass shortened these five past the point where they still said what they meant:

- Capture hint said 'presented depth' where the term is 'regular presented depth', which is what
  Regular presented-depth frontier is measured against; the short form read as a different axis.
- Clip fold lost both that the interpreter runs inside presentation for every backend and that a
  platform difference may not enter as a backend exception. Those are the whole rule.
- Snapshot policy facet lost the process boundary that makes it host-side at all: runner-side
  Swift presentation stays separate.
- Runner command traits lost 'independently of the public command surface', which is what
  distinguishes them from the command surface.
- Delegation-on-error said 'settles', and Settled observation makes 'settle' a term of its own.

CONTEXT.md is 11,674 of its 12,000-byte budget.

* docs(daemon): correct the claim-gate and managed-owner comments

- The claim-gate docstring claimed there is no other way to obtain device operations. That is
  true of command handlers, but two daemon-owned recovery paths bind outside the seam:
  application-lifecycle-recovery.ts (ordinary intent, daemon shutdown) and
  durable-capture-runtime-recovery.ts (exact-owner intent read back from a durable envelope,
  which this unit makes able to carry a managed local owner). Name them instead of claiming
  coverage the seam does not have.
- The open path's comment described a session executing under an allocator-held claim, a state
  this route cannot produce. Say what the `{ kind: 'ordinary' }` literal actually is: the truth
  of a route that binds ordinarily, which the Host open route replaces with the request's exact
  intent when it lands.
- Name U3 as the unit that fills the exact-owner selection arm, rather than the whole ADR.

* fix(runtime): accept transport-composed facts for a managed owner

providerModeMatchesOwner's managed-local arm accepted mode === 'local' only, but
selectExactOwner's managed-local arm loads the device's local family owner through the same
loadLocal a local-family owner uses, so it inherits that owner's provider modes verbatim. A
managed binding over a transport-composed local device (e.g. a remote ADB or web-provider
transport) would fail bindingContractFailure's facts check and be rejected as an owner/facts
mismatch. Accept the same local-family modes the local-family arm already does; still
unreachable until U3 registers the exact-only owner, which is where the binding regression
test that pins this lives.

* feat(runtime): register the managed local owner as an exact-only wrapper and add the neutral allocator port (#2259)

* feat(runtime): register the managed local owner as an exact-only wrapper and add the neutral allocator port

ADR 0021 foundations, unit 3. Unit 1 added the `managed-local` owner kind and left the gateway's
exact-owner arm for it failing closed; this unit gives that arm a registry and the owner it selects.
Nothing in production registers a managed owner yet, so both are reached from tests only.

- `createComposedPlatformRuntimeGateway` gains a `managedOwners` list that only the `managed-local`
  arm of `selectExactOwner` reads. `selectOrdinaryProvider`, `inspectFacts` and the ordinary `bind`
  arm never see it, and `providerModules` pairs one provider-runtime owner with one
  `ProviderDeviceRuntime`, so ordinary selection cannot reach a managed owner by construction
  rather than by a check. A duplicate instance is refused at composition.

- The wrapper (src/platform-runtime-managed-owner.ts, root zone, no platform imports) binds only
  under an exact-owner intent naming itself, loads the device's own family owner through the
  gateway's loader, delegates with an ordinary intent — a family owner refuses a foreign exact
  owner — and republishes the binding under the managed owner. It does not read the fence: what a
  managed binding fence proves is the device-claim gate's business. `ownsDevice` returns false.

- Twenty cells are withheld as `owner-capability-missing`, enumerated by mechanics rather than by
  catalog group: the four device-lifecycle cells, the four application cells that boot or shut the
  device down (`prepareApplicationOpen`, `prepareAppleRunner`, `closeApplication`,
  `finalizeApplicationClose`), and the twelve durable-capture cells, which a managed binding could
  never reattach because the family runtime stamps envelopes with its own local owner. The
  operations are then filtered by those facts, so an operation cannot outlive its own fact.

- `@agent-device/contracts/managed-device-allocation` is agent-device's own allocator port: lease
  request, lookup, supersession, cancellation, renewal, release, activation confirmation, identity
  status, removal acknowledgement, and the typed environment projection. Types only, named to match
  the allocator's published contract so the two sides cannot drift, with no dependency on any
  allocator package. Its only implementation is a scripted fake under `*.fixtures.ts`.

- Budgets: the new contracts entry surface is a one-module closure; the `src/platform-runtime.ts`
  hub moves 47 -> 48 for the wrapper, whose own value imports were already in that closure.

* fix(runtime): withhold the deployment cells from a managed binding and trim the allocator port

Review findings on the managed local owner.

- `deployApp` and `deployMaterializedApp` join the lifecycle group. Both family deployment runtimes
  ensure device readiness before installing, and `deployAppUse` requires `deployApp` alone — so
  `install` on a managed binding would have booted the allocator's device with nothing to refuse
  it. Twenty withheld cells become twenty-two, and the refused-uses test covers `deployAppUse`.

- The wrapper's doc comment no longer implies that withholding cells is a complete lifecycle
  exclusion: several retained Apple cells (screenshot capture, settings, clipboard, application
  launch) boot the simulator lazily inside the family runtime, where cell selection cannot reach.
  That is the same class as the pre-binding readiness path, and closing it is a family-runtime
  change.

- `readLeaseEnvironment` leaves the allocator port. It was beyond the vocabulary the contract
  fixes, and it made the scripted fake carry a real parser whose only test passed with every
  production line reverted. `ManagedLeaseEnvironment`, `ManagedLeaseEnvironmentKey` and
  `LeaseEnvironmentError` stay as types; the reader that produces them lands with the unit that
  first turns a grant into a device.

- CONTEXT.md drops an operation enumeration that was already incomplete.

* fix(runtime): withhold the lazily-booting Apple system and screenshot cells

Screenshot capture, settings, clipboard and application launch were retained on a managed
binding even though their Apple family-runtime implementations can boot the simulator lazily
below cell-selection granularity (screenshot's shutdown-failure retry boot; settings, clipboard
and application launch each resolve a local interactor the same way). That preserves rather than
blocks the exact bypass ADR-0021 section 3's hard boundary names: managed lifecycle/readiness
belongs to the allocator, and no handler path may fall back to direct lifecycle tooling.

Withhold captureScreenshot, setSetting, readClipboard, writeClipboard and openApplication
alongside the existing withheld groups. The wrapper's doc comment now names the pre-binding
readiness gap explicitly as the same class of follow-up, rather than folding it into a retained-
cells caveat that no longer applies. MANAGED_RETAINED_OPERATION moves to tapPoint, the cell the
fixture-based regression tests now use to prove something survives the wrapper.

* chore: retrigger CI (stale synchronize event after rebase)

* fix(runtime): lazy-load the managed owner wrapper to satisfy the eager-closure no-growth gate

Main's eager-closure budget gate (the merge-base ratchet) replaced the hand-tracked
HUB_BUDGETS map with an automatic no-growth-vs-merge-base check: src/platform-runtime.ts
is a hub with no growth allowed at all, not a number bumped by hand with a justifying
comment. The static import of createManagedLocalRuntimeOwner in platform-runtime-gateway.ts
added one module to that hub's closure (47 -> 48), which now fails
scripts/__tests__/eager-closure-budgets.test.ts outright rather than needing a manual bump.

Move the value import into loadManaged's dynamic `await import`, matching how the rest of
this file's owner loaders defer their leaf modules. Only the managed-local arm reaches this
path, so an ordinary bind never pays for it, same as before -- the wrapper module itself was
simply the wrong side of the eager/lazy line.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ready-for-human Valid work that needs human implementation, judgment, or maintainer merge

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant