feat(runtime): add managed-local ownership and the exact-only managed runtime - #2258
Conversation
Size Report
npm unpacked components
Startup median (7 runs, lower is better):
Top changed chunks:
Top changed packed files
|
|
BLOCKED at
Exact-head iOS Smoke is still pending; other checks, including coverage and integration, are green. No conflict. Do not label ready. |
|
Fixed at c17b4b5.
The arm is still unreachable from this PR alone (no
|
c17b4b5 to
a8ecbd2
Compare
|
CLEAN at this exact head. The rebase is patch-identical to the reviewed six-commit series; the owner-kind/claim-gate design, fail-closed decision, canonical fence, and regressions remain sound. All completed exact-head checks are green. iOS Smoke is still running; once it succeeds, this is first in merge order. |
|
READY at unchanged exact head |
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.
…nd 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.
… 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.
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.
- 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.
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.
…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)
…r-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.
b8f5fad to
83d5522
Compare
|
BLOCKED at |
|
Scope blocker resolved at exact head #2258 now explicitly owns the combined U1+U3 foundation created when #2259 merged into this branch. The title/body, dependency order, 33-file scope, +1,943/-180 accounting, +5.6 kB size evidence, lazy-import rationale, deferred production boundaries, and exact-head validation all match the code. Preserving the merged unit avoids orphaning it or recreating a second implementation path; #2263 remains the independent claim-store layer. All exact-head checks are green and GitHub reports CLEAN. |
|
Summary
ADR 0021 foundations for managed local runtimes. This branch now intentionally contains the two units that GitHub has already combined: #2258's owner/claim-admission foundation and merged #2259's exact-only managed-owner wrapper plus neutral allocator port. #2263 remains the focused allocator-held claim-store layer on top.
RuntimeOwnerRefgains the exhaustivemanaged-localkind. The existing claim gate maps owners to one totalordinary | allocator-held | nonerule, and managed admission returns an explicit admitted/refused result; there is no parallel command path.managedBindingFenceis the canonical requester/incarnation fence. Its decoder accepts only the canonical encoding and preserves allocator-issued identifiers verbatim.managedOwnersregistry. Ordinary selection cannot reach it. The wrapper delegates device mechanics to the existing local-family owner, republishes the binding under the managed owner, and removes lifecycle, implicit-boot, and non-adoptable durable-capture cells through the existing capability/narrowing machinery.@agent-device/contracts/managed-device-allocationis a types-only allocator port with a scripted test fake. It adds no allocator dependency, socket code, or production call site.Nothing in production registers a managed owner or calls the allocator yet, so this foundation does not make managed sessions reachable and changes no device-facing behavior. Pre-binding readiness and durable-capture adoption remain explicitly deferred to the allocation-flow/lifecycle units, where they can be exercised end to end.
Combining these units is deliberate now that #2259 was merged into this branch: splitting them again would discard an already-merged unit and create a second implementation path. They share the owner/gateway contract and remain one coherent runtime foundation. Claim persistence and clearing stay separate in #2263 because the claim store is an independent owner with different failure and recovery rules.
Scope: 33 files, +1,943/-180 against
main. The npm unpacked increase is 5.6 kB: approximately 2.1 kB is the managed-owner wrapper, 1.1 kB is the batch-runner projection, and the apparent 56.3 kBdevice-claim-rule.jsaddition is predominantly the renamed 54.9 kBdevice-claim-conflict.jschunk. The remaining growth is contracts/projections; startup stayed flat within measurement noise.Validation
Exact head:
83d5522d3fd4f16afb5de4cb208ef3f8c190476f.pnpm check:affected --run: green across format, lint, typecheck, layering, fallow, build, unit, integration, package, and repository guards.