From cb15409512fa3f70f2dd4f902e9f5ddada5399f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pierzcha=C5=82a?= Date: Fri, 6 Nov 2026 06:01:32 +0100 Subject: [PATCH 1/5] docs(adr): propose platform-neutral snapshot source routing --- .../adr/0004-ios-snapshot-backend-strategy.md | 3 + .../0038-snapshot-capture-source-routing.md | 137 ++++++++++++++++++ docs/adr/README.md | 1 + 3 files changed, 141 insertions(+) create mode 100644 docs/adr/0038-snapshot-capture-source-routing.md diff --git a/docs/adr/0004-ios-snapshot-backend-strategy.md b/docs/adr/0004-ios-snapshot-backend-strategy.md index f91394d5c1..6a684286e0 100644 --- a/docs/adr/0004-ios-snapshot-backend-strategy.md +++ b/docs/adr/0004-ios-snapshot-backend-strategy.md @@ -3,6 +3,9 @@ ## Status Accepted. +Source-routing ownership and rules are proposed for replacement by [ADR 0038](0038-snapshot-capture-source-routing.md). +Until that proposal is accepted, this ADR's routing rules remain in force. +The iOS reader, runner capture plan, presentation, geometry, and in-place system-surface contracts remain here. This ADR was amended after local iOS Simulator acquisition moved to the host AX bridge, and again for provider iOS Simulators that the runner serves. The public surface remains two modes: regular interactive snapshots and raw diagnostic snapshots. The host AX bridge is an acquisition route, not a third public mode. diff --git a/docs/adr/0038-snapshot-capture-source-routing.md b/docs/adr/0038-snapshot-capture-source-routing.md new file mode 100644 index 0000000000..d1489358a3 --- /dev/null +++ b/docs/adr/0038-snapshot-capture-source-routing.md @@ -0,0 +1,137 @@ +# ADR 0038: Snapshot capture source routing + +## Status + +Proposed for [#4205](https://github.com/callstack/agent-device/issues/4205). +No production migration precedes acceptance. +On acceptance, this replaces ADR 0004's source-routing rules and ownership. +ADR 0004 retains iOS acquisition, presentation, geometry, and system-surface contracts. +Completed migration history stays in git history. + +## Rules at a glance + +- Route every supported capture through capture-kit channels. +- Permit one fallback; share refusal scope, circuits, and warnings. +- Stamp served identity; derive producer facts from contracts. + +## Boundary and channels + +`@agent-device/capture-kit` owns the router: planning, proof, fallback, circuits, identity, warnings. +Allow neutral contracts, kernel primitives, host-kit capabilities, and capture-kit policy imports. +Forbid direct, transitive, and dynamic dependencies on platform/provider code. +Bound runtime owners retain channels across requests and close only owned resources. +Keep lease/lock authority ([ADR 0007](0007-remote-device-leases.md)). + +Contracts defines `SourceChannel`: + +| Member | Contract | +| --- | --- | +| `id`, `producer` | Separate transport from producer; both bridge transports emit `simulator-ax-bridge`. | +| `acquire({ target, hint, proof, signal })` | Return acquired/presented evidence, served identity, residue, proof; or typed refusal. | +| `resolveTarget(device, app, signal)` | Return real target `T`, device key, target id, optional verified generation; or typed discovery-pending. | +| `refreshTarget?(device, app, signal)` | Verify cached identity; omit when resolution already verifies identity. | +| `awaitDiscovery?(device, signal)` | Await detached discovery; omit when none exists. | +| `proof` | `'none'` or `SurfaceProof`. | +| `supports` | Declare `customActions`, `observeOnly`, and supported `pinnedBackend` values. | + +Operations share remaining budget and cancellation. +Hints separate raw and presented depth; presentation owns scope. +Discovery-pending requires `awaitDiscovery` or an available alternate. +No constant callbacks, `targetFreshness`, or fabricated pids/generations are allowed. + +`SurfaceProof(device, signal)` returns `'clear'`, `{ occluded: surfaceId }`, or `'unproven'`. +`'none'` requires no separate absence proof and asserts none. +Occlusion selects a surface-capable channel; unproven acquisition must refuse occluded/unverified reads through integrated checks. +Never cache absence across captures. + +Both bridges probe registered hosts through runner foreground state. +Only provider probes may prepare their required runner. +If unproven, pass host executables into the bridge's pre/post-read process checks. +Forbid host-side process probes. +Serve registered hosts in place without activation or rebinding, per ADR 0004. + +## Planning, fallback, and refusals + +Pure `plan(input)` takes ordered owner channels, options, target/proof evidence, circuits, and a current-loop backend pin. +It returns selected channel, typed reason, and at most one eligible alternate. +It owns eligibility, observe-only, and pins; unsupported requests fail before acquisition. +Never relax pins or activate for observe-only. +Stable capture supplies its current-loop private-AX backend pin; previous snapshots cannot pin its first read. + +One settlement path handles resolution, acquisition, presentation, and alternates. +Attempt one alternate at most; its failure retains the first reason. +Cancellation neither falls back nor changes circuits. +Channels/presenters cannot route recursively. +XCTest tiers and Android helper recovery stay internal and cannot select another channel. + +Key circuits by owner, device key, channel id, target id, and verified generation. +Generation refusals disable that channel; new generations clear old circuits. +Refresh cached targets before honoring disabled circuits and after stale-target evidence. +If refresh fails, retain only circuit bookkeeping; publish no cached generation. +Without verified generation, create no generation circuit. + +Generalize `contracts/fixtures/ios-snapshot-bridge-refusals.json` into router input for all typed-refusal channels. +Entries key on producer/code and declare scope plus warning consequence; bridge transports share entries. +Preserve iOS scope: + +| Scope | Evidence | +| --- | --- | +| Capture | Preparation, read-in-flight, tree change, foreground ownership, unresolved coordinates, window placement, surface-host presence or uncertainty. | +| Generation | Remote-content boundary, abandoned read, malformed tree, presentation invariant failure. | + +Unknown codes have generation scope if verified. +Intercept cancellation before lookup, including `abort-signal`. +Unclassified provider errors remain terminal. + +## Producers and publication + +Define platform-neutral `SnapshotProducer` and one contracts registry with `satisfies Record`. +Rows declare backend channels, truncation, residue policy, quiet time, and ref-namespace owner. +Residue distinguishes source-measured and provider-derived facts. +Derive provenance pairs, iOS subsets, producer warning dispatch, quiet time, and ref behavior here. +First move provenance-dependent carriers/helpers from `packages/kernel/src/snapshot.ts` to contracts. +Keep node/geometry primitives in kernel; forbid kernel-to-contracts cycles. +Keep bridge quiet time at 300 ms, others at 500 ms; preserve agent-browser refs. + +One-channel owners have no alternate: Android, macOS helper/XCTest, web, WebDriver, Revyl, HarmonyOS, Linux, TV CDP, and composing providers. +Local and Limrun iOS Simulators offer bridge first, then `apple-runner`. +Physical iOS, tvOS, and visionOS retain one runner channel. +Unsupported owners such as Vega declare no channel. +WebDriver keeps `appium-source` on both backend channels. +Rename Revyl Android to `revyl-android-tree`; sharing a parser does not make it Appium. + +Stamp identity once after presentation: actual producer, intent, served target/generation, presentation key, residue. +Served surfaces override planned app lineage and discard app-generation residue. +Failed generation resolution carries per-capture unknown-generation residue. +Channels without generation support declare it unavailable; surfaces carry no app generation. +Preserve iOS producer names and fallback wording. +One-channel success adds no fallback warning; assembly retains quality/freshness disclosures. + +Android `systemSurfaceOnly` stays acquisition metadata, not `SurfaceProof`. +`packages/platform-android/src/snapshot-content-recovery.ts` accepts usable system-only trees. +This and system-bar labels prove no absence: use `proof: 'none'`, preserving disclosure/recovery. +A future app-only channel needs independent window-ownership evidence tied to its read. + +## Migration and enforcement + +After acceptance, each small PR must merge alone: + +1. Move provenance carriers to contracts; migrate registry consumers atomically. +2. Add router/refusal input and layering enforcement; port guest/runner tests with unchanged behavior assertions. +3. Bind both iOS channels; remove old routing, placeholder ports, duplicate eligibility, and fallback settlers. +4. Bind single-source owners separately; rename Revyl Android with its emitter/consumers. +5. Slim ADR 0004 below about 15 KB after relocating unique contracts; retain iOS rules. + +Add router layering enforcement with a failing dependency control. +Producer-emitter/refusal guards in `test/repo-guards/` reject missing reachable production emitters, unlisted codes, and scope disagreement. +Add an ADR-citation guard for nonexistent cited paths/test names. +Select guards through affected checks with docs-prose, docs-links, and agent-guidance. +Require live producer/fallback parity on local iOS Simulator, Limrun iOS, and local Android. + +## Rationale and alternatives + +Readers, decoding, discovery, process checks, native budgets, geometry, and lifecycle stay platform-specific. +Presentation stays in capture-kit and runner-side Swift. + +Rejected: Apple placeholder ports (absent capabilities), provider planners (duplicated policy), and annotations as absence proof (no pre-read evidence). +The cost is provenance migration and explicit channels. diff --git a/docs/adr/README.md b/docs/adr/README.md index ce44dc5cd5..8050da8df8 100644 --- a/docs/adr/README.md +++ b/docs/adr/README.md @@ -39,6 +39,7 @@ | [0035 Host-Scoped Devices](0035-macos-app-devices.md) | native macOS apps as devices of their own, the host/scoped-device overlap and claim rule, concurrent app sessions, and `macos-app` lease claims | | [0036 Bounded Composite Resource Admission](0036-bounded-composite-resource-admission.md) | resource operations, nested tests, session disposal, and claim recovery inside bounded batch/replay steps | | [0037 Web Session Devices](0037-web-session-devices.md) | web sessions as devices of their own, the session name rule, the `p_`/`t_` backend key and tenant isolation, and first-open locking for web | +| [0038 Snapshot capture source routing (Proposed)](0038-snapshot-capture-source-routing.md) | platform-neutral source channels, proof, one fallback, generation circuits, refusal scope, and the producer registry | ADRs record rationale. The registries and gates they describe are the current source of truth. From fd031ee762ae5bca8d0aec193a0668c21ffb332b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pierzcha=C5=82a?= Date: Fri, 6 Nov 2026 08:19:27 +0100 Subject: [PATCH 2/5] docs(adr): narrow 0038 routing to multi-channel owners and move the plan to #4205 - Route only owners with more than one channel through the router. Every producer stamps identity through capture-kit and declares a registry row. - Give unknown refusal codes generation scope without a verification condition. - Add the runner-session scope from #4241. - Derive quiet time per capture from the served producer. - Move the migration plan and the guard inventory to #4205. --- .../0038-snapshot-capture-source-routing.md | 37 ++++++++----------- 1 file changed, 15 insertions(+), 22 deletions(-) diff --git a/docs/adr/0038-snapshot-capture-source-routing.md b/docs/adr/0038-snapshot-capture-source-routing.md index d1489358a3..7e429dcc17 100644 --- a/docs/adr/0038-snapshot-capture-source-routing.md +++ b/docs/adr/0038-snapshot-capture-source-routing.md @@ -10,9 +10,9 @@ Completed migration history stays in git history. ## Rules at a glance -- Route every supported capture through capture-kit channels. +- Route owners with more than one channel through the capture-kit router. - Permit one fallback; share refusal scope, circuits, and warnings. -- Stamp served identity; derive producer facts from contracts. +- Every producer stamps served identity through capture-kit and declares a contracts registry row. ## Boundary and channels @@ -78,8 +78,10 @@ Preserve iOS scope: | --- | --- | | Capture | Preparation, read-in-flight, tree change, foreground ownership, unresolved coordinates, window placement, surface-host presence or uncertainty. | | Generation | Remote-content boundary, abandoned read, malformed tree, presentation invariant failure. | +| Runner session | Wedged runner-side bridge reader; a new runner session id starts a fresh circuit. | -Unknown codes have generation scope if verified. +Unknown codes have generation scope. +Without a verified generation, an unknown code disables the channel for the current capture and creates no circuit. Intercept cancellation before lookup, including `abort-signal`. Unclassified provider errors remain terminal. @@ -91,11 +93,13 @@ Residue distinguishes source-measured and provider-derived facts. Derive provenance pairs, iOS subsets, producer warning dispatch, quiet time, and ref behavior here. First move provenance-dependent carriers/helpers from `packages/kernel/src/snapshot.ts` to contracts. Keep node/geometry primitives in kernel; forbid kernel-to-contracts cycles. -Keep bridge quiet time at 300 ms, others at 500 ms; preserve agent-browser refs. +Keep bridge quiet time at 300 ms, others at 500 ms; derive it per capture from the served producer. +Preserve agent-browser refs. -One-channel owners have no alternate: Android, macOS helper/XCTest, web, WebDriver, Revyl, HarmonyOS, Linux, TV CDP, and composing providers. -Local and Limrun iOS Simulators offer bridge first, then `apple-runner`. -Physical iOS, tvOS, and visionOS retain one runner channel. +Local and Limrun iOS Simulators offer bridge first, then `apple-runner`, and route through the router. +Every other owner has one channel and does not route through the router: Android, macOS helper/XCTest, web, WebDriver, Revyl, HarmonyOS, Linux, TV CDP, composing providers, and physical iOS, tvOS, and visionOS. +A one-channel owner calls the capture-kit identity-stamp helper and declares its registry row. +An owner that gains a second channel moves to the router. Unsupported owners such as Vega declare no channel. WebDriver keeps `appium-source` on both backend channels. Rename Revyl Android to `revyl-android-tree`; sharing a parser does not make it Appium. @@ -112,26 +116,15 @@ Android `systemSurfaceOnly` stays acquisition metadata, not `SurfaceProof`. This and system-bar labels prove no absence: use `proof: 'none'`, preserving disclosure/recovery. A future app-only channel needs independent window-ownership evidence tied to its read. -## Migration and enforcement +## Consequences -After acceptance, each small PR must merge alone: - -1. Move provenance carriers to contracts; migrate registry consumers atomically. -2. Add router/refusal input and layering enforcement; port guest/runner tests with unchanged behavior assertions. -3. Bind both iOS channels; remove old routing, placeholder ports, duplicate eligibility, and fallback settlers. -4. Bind single-source owners separately; rename Revyl Android with its emitter/consumers. -5. Slim ADR 0004 below about 15 KB after relocating unique contracts; retain iOS rules. - -Add router layering enforcement with a failing dependency control. -Producer-emitter/refusal guards in `test/repo-guards/` reject missing reachable production emitters, unlisted codes, and scope disagreement. -Add an ADR-citation guard for nonexistent cited paths/test names. -Select guards through affected checks with docs-prose, docs-links, and agent-guidance. -Require live producer/fallback parity on local iOS Simulator, Limrun iOS, and local Android. +The migration plan, its guard inventory, and the live parity checks live in [#4205](https://github.com/callstack/agent-device/issues/4205). +Repo guards enforce the router's layering, the producer emitters, and the refusal table scope. ## Rationale and alternatives Readers, decoding, discovery, process checks, native budgets, geometry, and lifecycle stay platform-specific. Presentation stays in capture-kit and runner-side Swift. -Rejected: Apple placeholder ports (absent capabilities), provider planners (duplicated policy), and annotations as absence proof (no pre-read evidence). +Rejected: Apple placeholder ports (absent capabilities), provider planners (duplicated policy), annotations as absence proof (no pre-read evidence), and routing one-channel owners through the router (the planner never chooses for them). The cost is provenance migration and explicit channels. From 7ef3490351bcaf7ecd967f7768e1e39e5189355b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pierzcha=C5=82a?= Date: Fri, 6 Nov 2026 08:22:41 +0100 Subject: [PATCH 3/5] docs(adr): accept 0038 as a one-page decision record Keep only the decisions, reasons, rejected alternatives, and consequences. Move the source interface, planner mechanics, and per-owner inventory to #4205. Keep the constraints that the code cannot show: unproven is not clear, cancellation is terminal, no circuit without a verified generation, and a served surface owns its identity. Amend the ADR 0004 status to point at 0038. --- .../adr/0004-ios-snapshot-backend-strategy.md | 4 +- .../0038-snapshot-capture-source-routing.md | 177 +++++++----------- docs/adr/README.md | 2 +- 3 files changed, 66 insertions(+), 117 deletions(-) diff --git a/docs/adr/0004-ios-snapshot-backend-strategy.md b/docs/adr/0004-ios-snapshot-backend-strategy.md index 6a684286e0..cfd5478f82 100644 --- a/docs/adr/0004-ios-snapshot-backend-strategy.md +++ b/docs/adr/0004-ios-snapshot-backend-strategy.md @@ -3,8 +3,8 @@ ## Status Accepted. -Source-routing ownership and rules are proposed for replacement by [ADR 0038](0038-snapshot-capture-source-routing.md). -Until that proposal is accepted, this ADR's routing rules remain in force. +[ADR 0038](0038-snapshot-capture-source-routing.md) owns snapshot source routing for every platform. +Until the router from [#4205](https://github.com/callstack/agent-device/issues/4205) replaces the Apple route, the routing rules here describe the current iOS code. The iOS reader, runner capture plan, presentation, geometry, and in-place system-surface contracts remain here. This ADR was amended after local iOS Simulator acquisition moved to the host AX bridge, and again for provider iOS Simulators that the runner serves. The public surface remains two modes: regular interactive snapshots and raw diagnostic snapshots. diff --git a/docs/adr/0038-snapshot-capture-source-routing.md b/docs/adr/0038-snapshot-capture-source-routing.md index 7e429dcc17..759260f318 100644 --- a/docs/adr/0038-snapshot-capture-source-routing.md +++ b/docs/adr/0038-snapshot-capture-source-routing.md @@ -2,129 +2,78 @@ ## Status -Proposed for [#4205](https://github.com/callstack/agent-device/issues/4205). -No production migration precedes acceptance. -On acceptance, this replaces ADR 0004's source-routing rules and ownership. -ADR 0004 retains iOS acquisition, presentation, geometry, and system-surface contracts. -Completed migration history stays in git history. +Accepted. +This record owns snapshot source routing for every platform and provider. +It amends [ADR 0004](0004-ios-snapshot-backend-strategy.md), which keeps the iOS acquisition, presentation, geometry, and system-surface contracts. +The router does not exist yet. [#4205](https://github.com/callstack/agent-device/issues/4205) holds the migration plan and its guards. -## Rules at a glance +## Context -- Route owners with more than one channel through the capture-kit router. -- Permit one fallback; share refusal scope, circuits, and warnings. -- Every producer stamps served identity through capture-kit and declares a contracts registry row. +Only iOS has a source-selection rule, in the Apple platform runtime. +Its runner source fills the route's ports with constants. +Eligibility has three owners, fallback has three settlers, and producer facts have about six tables. +Each other platform has one hard-wired source with its own refusal and identity vocabulary. -## Boundary and channels +## Decision -`@agent-device/capture-kit` owns the router: planning, proof, fallback, circuits, identity, warnings. -Allow neutral contracts, kernel primitives, host-kit capabilities, and capture-kit policy imports. -Forbid direct, transitive, and dynamic dependencies on platform/provider code. -Bound runtime owners retain channels across requests and close only owned resources. -Keep lease/lock authority ([ADR 0007](0007-remote-device-leases.md)). +**Router.** One platform-neutral router in `@agent-device/capture-kit` owns planning, fallback, refusal scope, circuits, surface proof, and warnings. +Owners with more than one source use the router. +Today these owners are the local and Limrun iOS Simulators: the simulator AX bridge first, then the XCTest runner. +The router depends on no platform or provider code. +Lease and lock authority stays with [ADR 0007](0007-remote-device-leases.md). -Contracts defines `SourceChannel`: +**One-source owners.** An owner with one source does not use the router. +It stamps identity through the capture-kit helper. +When an owner gets a second source, it moves to the router. -| Member | Contract | -| --- | --- | -| `id`, `producer` | Separate transport from producer; both bridge transports emit `simulator-ax-bridge`. | -| `acquire({ target, hint, proof, signal })` | Return acquired/presented evidence, served identity, residue, proof; or typed refusal. | -| `resolveTarget(device, app, signal)` | Return real target `T`, device key, target id, optional verified generation; or typed discovery-pending. | -| `refreshTarget?(device, app, signal)` | Verify cached identity; omit when resolution already verifies identity. | -| `awaitDiscovery?(device, signal)` | Await detached discovery; omit when none exists. | -| `proof` | `'none'` or `SurfaceProof`. | -| `supports` | Declare `customActions`, `observeOnly`, and supported `pinnedBackend` values. | - -Operations share remaining budget and cancellation. -Hints separate raw and presented depth; presentation owns scope. -Discovery-pending requires `awaitDiscovery` or an available alternate. -No constant callbacks, `targetFreshness`, or fabricated pids/generations are allowed. - -`SurfaceProof(device, signal)` returns `'clear'`, `{ occluded: surfaceId }`, or `'unproven'`. -`'none'` requires no separate absence proof and asserts none. -Occlusion selects a surface-capable channel; unproven acquisition must refuse occluded/unverified reads through integrated checks. -Never cache absence across captures. - -Both bridges probe registered hosts through runner foreground state. -Only provider probes may prepare their required runner. -If unproven, pass host executables into the bridge's pre/post-read process checks. -Forbid host-side process probes. -Serve registered hosts in place without activation or rebinding, per ADR 0004. - -## Planning, fallback, and refusals - -Pure `plan(input)` takes ordered owner channels, options, target/proof evidence, circuits, and a current-loop backend pin. -It returns selected channel, typed reason, and at most one eligible alternate. -It owns eligibility, observe-only, and pins; unsupported requests fail before acquisition. -Never relax pins or activate for observe-only. -Stable capture supplies its current-loop private-AX backend pin; previous snapshots cannot pin its first read. - -One settlement path handles resolution, acquisition, presentation, and alternates. -Attempt one alternate at most; its failure retains the first reason. -Cancellation neither falls back nor changes circuits. -Channels/presenters cannot route recursively. -XCTest tiers and Android helper recovery stay internal and cannot select another channel. - -Key circuits by owner, device key, channel id, target id, and verified generation. -Generation refusals disable that channel; new generations clear old circuits. -Refresh cached targets before honoring disabled circuits and after stale-target evidence. -If refresh fails, retain only circuit bookkeeping; publish no cached generation. -Without verified generation, create no generation circuit. - -Generalize `contracts/fixtures/ios-snapshot-bridge-refusals.json` into router input for all typed-refusal channels. -Entries key on producer/code and declare scope plus warning consequence; bridge transports share entries. -Preserve iOS scope: - -| Scope | Evidence | +**Producers.** One contracts registry keys every producer with a platform-neutral name. +All other producer facts derive from that registry. + +**Identity.** Stamp identity once, after presentation, from the source that served the capture. +When a system surface serves the capture, the surface owns the published identity, not the app generation below it. + +**Fallback.** A capture tries one alternate source at most. +If the alternate fails, the capture keeps the first reason. +Cancellation is terminal: it neither falls back nor changes a circuit. +Never relax a backend pin or an observe-only request to make a fallback possible. +A source never routes to another source. + +**Refusal scope.** Each typed refusal has one scope: + +| Scope | Effect | | --- | --- | -| Capture | Preparation, read-in-flight, tree change, foreground ownership, unresolved coordinates, window placement, surface-host presence or uncertainty. | -| Generation | Remote-content boundary, abandoned read, malformed tree, presentation invariant failure. | -| Runner session | Wedged runner-side bridge reader; a new runner session id starts a fresh circuit. | - -Unknown codes have generation scope. -Without a verified generation, an unknown code disables the channel for the current capture and creates no circuit. -Intercept cancellation before lookup, including `abort-signal`. -Unclassified provider errors remain terminal. - -## Producers and publication - -Define platform-neutral `SnapshotProducer` and one contracts registry with `satisfies Record`. -Rows declare backend channels, truncation, residue policy, quiet time, and ref-namespace owner. -Residue distinguishes source-measured and provider-derived facts. -Derive provenance pairs, iOS subsets, producer warning dispatch, quiet time, and ref behavior here. -First move provenance-dependent carriers/helpers from `packages/kernel/src/snapshot.ts` to contracts. -Keep node/geometry primitives in kernel; forbid kernel-to-contracts cycles. -Keep bridge quiet time at 300 ms, others at 500 ms; derive it per capture from the served producer. -Preserve agent-browser refs. - -Local and Limrun iOS Simulators offer bridge first, then `apple-runner`, and route through the router. -Every other owner has one channel and does not route through the router: Android, macOS helper/XCTest, web, WebDriver, Revyl, HarmonyOS, Linux, TV CDP, composing providers, and physical iOS, tvOS, and visionOS. -A one-channel owner calls the capture-kit identity-stamp helper and declares its registry row. -An owner that gains a second channel moves to the router. -Unsupported owners such as Vega declare no channel. -WebDriver keeps `appium-source` on both backend channels. -Rename Revyl Android to `revyl-android-tree`; sharing a parser does not make it Appium. - -Stamp identity once after presentation: actual producer, intent, served target/generation, presentation key, residue. -Served surfaces override planned app lineage and discard app-generation residue. -Failed generation resolution carries per-capture unknown-generation residue. -Channels without generation support declare it unavailable; surfaces carry no app generation. -Preserve iOS producer names and fallback wording. -One-channel success adds no fallback warning; assembly retains quality/freshness disclosures. - -Android `systemSurfaceOnly` stays acquisition metadata, not `SurfaceProof`. -`packages/platform-android/src/snapshot-content-recovery.ts` accepts usable system-only trees. -This and system-bar labels prove no absence: use `proof: 'none'`, preserving disclosure/recovery. -A future app-only channel needs independent window-ownership evidence tied to its read. +| Capture | The source stays eligible for the next capture. | +| App generation | The source stays disabled until the app has a new generation. | +| Runner session | The source stays disabled until the runner has a new session id. | -## Consequences +An unknown refusal code has app-generation scope. +Without a verified generation, the refusal disables the source for the current capture only and creates no circuit. +The refusal fixture is the one table of codes and scopes. +The router handles cancellation before it classifies a refusal. + +**Surface proof.** Prove surface absence for each capture. Never cache it. +An unproven state is not a clear state: an app-only read must prove absence or refuse. +An occluded read selects a source that can read the surface. +A source that declares no proof asserts no absence. +Acquisition metadata, such as an Android system-only tree, is not a proof. +A future app-only source needs window-ownership evidence that is tied to its read. + +## Rationale -The migration plan, its guard inventory, and the live parity checks live in [#4205](https://github.com/callstack/agent-device/issues/4205). -Repo guards enforce the router's layering, the producer emitters, and the refusal table scope. +- One rule makes fallback, refusals, and warnings the same on every platform. A new provider declares its sources and does not copy policy. +- The router applies only where a choice exists. A planner that never chooses adds migration cost and no value. +- An unknown refusal fails closed, so a failed source cannot stay eligible. +- A surface can appear between captures. A cached absence would publish an occluded read as app content. -## Rationale and alternatives +## Rejected alternatives -Readers, decoding, discovery, process checks, native budgets, geometry, and lifecycle stay platform-specific. -Presentation stays in capture-kit and runner-side Swift. +- Placeholder ports on the Apple route: they claim capabilities that a source does not have. +- A planner for each provider: it duplicates policy, and the copies drift. +- Annotations as absence proof: they give no evidence from before the read. +- The router for one-source owners: the planner never chooses for them. + +## Consequences -Rejected: Apple placeholder ports (absent capabilities), provider planners (duplicated policy), annotations as absence proof (no pre-read evidence), and routing one-channel owners through the router (the planner never chooses for them). -The cost is provenance migration and explicit channels. +- Provenance types move from kernel to contracts, and both iOS sources bind to the router. +- Readers, decoding, discovery, process checks, native budgets, geometry, and lifecycle stay platform-specific. +- Open gaps: the router and the producer registry do not exist yet. The runner-session scope arrives with [#4256](https://github.com/callstack/agent-device/pull/4256). ADR 0004 must shrink below about 15 KB. diff --git a/docs/adr/README.md b/docs/adr/README.md index 8050da8df8..2206bf983a 100644 --- a/docs/adr/README.md +++ b/docs/adr/README.md @@ -39,7 +39,7 @@ | [0035 Host-Scoped Devices](0035-macos-app-devices.md) | native macOS apps as devices of their own, the host/scoped-device overlap and claim rule, concurrent app sessions, and `macos-app` lease claims | | [0036 Bounded Composite Resource Admission](0036-bounded-composite-resource-admission.md) | resource operations, nested tests, session disposal, and claim recovery inside bounded batch/replay steps | | [0037 Web Session Devices](0037-web-session-devices.md) | web sessions as devices of their own, the session name rule, the `p_`/`t_` backend key and tenant isolation, and first-open locking for web | -| [0038 Snapshot capture source routing (Proposed)](0038-snapshot-capture-source-routing.md) | platform-neutral source channels, proof, one fallback, generation circuits, refusal scope, and the producer registry | +| [0038 Snapshot capture source routing](0038-snapshot-capture-source-routing.md) | the source router for multi-source owners, one fallback, refusal scopes, surface proof, and the producer registry | ADRs record rationale. The registries and gates they describe are the current source of truth. From fcf318e6e00a179c6fdf96538746dd5383151bf7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pierzcha=C5=82a?= Date: Fri, 6 Nov 2026 08:23:45 +0100 Subject: [PATCH 4/5] docs(adr): fix meaning drift in the 0038 rewrite Limit the no-generation rule to app-generation refusals, scope surface proof to captures that need it, keep integrated checks for unproven absence, and scope the rationale to multi-source owners. --- docs/adr/0038-snapshot-capture-source-routing.md | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/docs/adr/0038-snapshot-capture-source-routing.md b/docs/adr/0038-snapshot-capture-source-routing.md index 759260f318..dd21841adb 100644 --- a/docs/adr/0038-snapshot-capture-source-routing.md +++ b/docs/adr/0038-snapshot-capture-source-routing.md @@ -47,12 +47,13 @@ A source never routes to another source. | Runner session | The source stays disabled until the runner has a new session id. | An unknown refusal code has app-generation scope. -Without a verified generation, the refusal disables the source for the current capture only and creates no circuit. +Without a verified app generation, an app-generation refusal disables the source for the current capture only and creates no app-generation circuit. The refusal fixture is the one table of codes and scopes. The router handles cancellation before it classifies a refusal. -**Surface proof.** Prove surface absence for each capture. Never cache it. -An unproven state is not a clear state: an app-only read must prove absence or refuse. +**Surface proof.** When a capture needs surface-absence proof, prove absence for that capture. Never cache it. +An unproven state is not a clear state. +When surface absence is unproven, an app-only source must refuse an occluded or unverified read through integrated checks. An occluded read selects a source that can read the surface. A source that declares no proof asserts no absence. Acquisition metadata, such as an Android system-only tree, is not a proof. @@ -60,9 +61,9 @@ A future app-only source needs window-ownership evidence that is tied to its rea ## Rationale -- One rule makes fallback, refusals, and warnings the same on every platform. A new provider declares its sources and does not copy policy. +- The router gives multi-source owners one rule for fallback, refusals, and warnings. A new provider declares its sources and does not copy policy. - The router applies only where a choice exists. A planner that never chooses adds migration cost and no value. -- An unknown refusal fails closed, so a failed source cannot stay eligible. +- An unknown refusal fails closed for the verified app generation, or for the current capture when the generation is unverified. - A surface can appear between captures. A cached absence would publish an occluded read as app content. ## Rejected alternatives @@ -76,4 +77,4 @@ A future app-only source needs window-ownership evidence that is tied to its rea - Provenance types move from kernel to contracts, and both iOS sources bind to the router. - Readers, decoding, discovery, process checks, native budgets, geometry, and lifecycle stay platform-specific. -- Open gaps: the router and the producer registry do not exist yet. The runner-session scope arrives with [#4256](https://github.com/callstack/agent-device/pull/4256). ADR 0004 must shrink below about 15 KB. +- Open gaps: the router and the producer registry do not exist yet. The runner-session scope is pending [#4256](https://github.com/callstack/agent-device/pull/4256). From 74e1a6e654d974abc58eeb0b48f38322af6c716a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pierzcha=C5=82a?= Date: Fri, 6 Nov 2026 08:48:32 +0100 Subject: [PATCH 5/5] docs(adr): list the identity-stamp helper as an open gap in 0038 --- docs/adr/0038-snapshot-capture-source-routing.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/adr/0038-snapshot-capture-source-routing.md b/docs/adr/0038-snapshot-capture-source-routing.md index dd21841adb..fc3d215174 100644 --- a/docs/adr/0038-snapshot-capture-source-routing.md +++ b/docs/adr/0038-snapshot-capture-source-routing.md @@ -77,4 +77,4 @@ A future app-only source needs window-ownership evidence that is tied to its rea - Provenance types move from kernel to contracts, and both iOS sources bind to the router. - Readers, decoding, discovery, process checks, native budgets, geometry, and lifecycle stay platform-specific. -- Open gaps: the router and the producer registry do not exist yet. The runner-session scope is pending [#4256](https://github.com/callstack/agent-device/pull/4256). +- Open gaps: the router, the identity-stamp helper, and the producer registry do not exist yet. The runner-session scope is pending [#4256](https://github.com/callstack/agent-device/pull/4256).