diff --git a/AGENTS.md b/AGENTS.md index 3ff131c658..b46d4d2d98 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -110,7 +110,7 @@ under `contracts/fixtures/`. ## Runtime and diagnostics seams Diagnostics use `@agent-device/host-kit/diagnostics`. Request diagnostics belong in the session request log; -session artifact paths come from `packages/daemon/src/core/session-artifact-paths.ts`. App/device logs remain in `app.log`; +session artifact paths come from `packages/daemon-contracts/src/session-artifact-paths.ts`. App/device logs remain in `app.log`; Apple runner and xcodebuild output remains in `runner.log`. Normalize failures with `normalizeError` and preserve `hint`, `diagnosticId`, `logPath`, and typed diff --git a/docs/adr/0032-layering-graph-engine.md b/docs/adr/0032-layering-graph-engine.md index 5a83dd20cf..170c1fe457 100644 --- a/docs/adr/0032-layering-graph-engine.md +++ b/docs/adr/0032-layering-graph-engine.md @@ -85,6 +85,8 @@ finding. Clean tree: custom 0, depcruise 10 (3 R6 survivors, 7 recorded R78 edge | R14 non-TS file under `src/utils` | flag | ✓ | ✗ | ✗ | | R71 file under `src/replay` | flag | ✓ | ✓ | ✓ generic | +_R78 no longer records edges: the client's direct imports are a flat ban in `LOGICAL_MODULE_POLICIES` (R10), and R78 now walks reachability from the CLI entries, so the "unrecorded" and "stale" rows describe the spike at its date, not the rules as shipped._ + Custom prints `file:line`, rule id and hint (`report()` in `scripts/layering/check.ts`); depcruise the rule name and an authored comment, without a line. "Generic" means a fallow zone pair (`core → cli`) and a docs link, with no rule id or hint; R14 and R71 surface as "matches no zone". diff --git a/docs/adr/0033-collocation-decision-table.md b/docs/adr/0033-collocation-decision-table.md index adaf8bb0fe..64496a5e68 100644 --- a/docs/adr/0033-collocation-decision-table.md +++ b/docs/adr/0033-collocation-decision-table.md @@ -79,7 +79,7 @@ already owns the operations behind the vocabulary. | `src/diagnostics.ts` | keep | The diagnostics seam every zone imports (AGENTS.md mandates it); highest fan-in in the repo is what a substrate looks like, not misplacement. | | `src/internal/request-cancel.ts` | keep | Request-cancellation mechanics AGENTS.md routes to `host-kit/request`; internal beside its facade is already the collocation. | | `src/request.ts` | keep | Cross-layer request contracts AGENTS.md names as a host-kit seam; consumers span every zone. | -| `src/session-paths.ts` | keep | Session-artifact path grammar the daemon, cli, and platforms all resolve; AGENTS.md keeps the path *policy* in `packages/daemon/src/core/session-artifact-paths.ts`, which consumes this. | +| `src/session-paths.ts` | keep | Session-artifact path grammar the daemon, cli, and platforms all resolve; AGENTS.md keeps the path *policy* in `packages/daemon-contracts/src/session-artifact-paths.ts`, which consumes this. | ### kernel @@ -118,7 +118,7 @@ already owns the operations behind the vocabulary. | File | Decision | Reason | | --- | --- | --- | -| `src/cli/commands/device-release.ts` | keep | Command surface belongs in `cli` by the folder spine; the daemon device-claim modules it reuses are already extracted, so what remains is thin daemonless-CLI composition. | +| `src/cli/commands/device-release.ts` | moved | Now `src/platform-runtime-claim-recovery.ts`: the daemonless stale release composes the shared reconciler with the root gateway, and the CLI process may reach no daemon module (R78), so it sits beside the claim-recovery gateway in the platform-runtime composition. | ### (root) @@ -133,8 +133,9 @@ the whole `daemon-contracts` zone has moved into `packages/daemon-contracts` — package derive the zone, so every moved file's `ROOT_MODULE_ZONES` row is gone. The daemon ⇄ client shared files that remain here (`provider-credential-fingerprint.ts`) trace to #2559, which relocated the shared contracts **to the process root** so the client stops -importing `packages/daemon/src/` at all; the `daemon-client` logical-module policy bans every edge from `src/daemon-client/` into -`packages/daemon/src/`, and #3288's `daemon-contracts` +importing `packages/daemon/src/` at all; the `client-side` logical-module policy bans every edge from the CLI, typed client, remote transports and +`src/daemon-client/` into `packages/daemon/src/`, R78 bans the hop through any root module the CLI +process reaches, and #3288's `daemon-contracts` zone was the declaration of that shared-below-both ownership — the extraction makes it physical, with R11 and the package's dependency edges holding the same boundary. Rows on path-keyed evidence alone (the `platform-runtime-*` seams) say "retain for this batch": a guard's @@ -144,6 +145,7 @@ optimal. | File | Decision | Reason | | --- | --- | --- | | `src/daemon-diagnostics-scope.ts` | moved → `packages/daemon-contracts/src/daemon-diagnostics-scope.ts` ([#3297](https://github.com/callstack/agent-device/pull/3297) into `src/daemon-contracts/`, then the package extraction) | The whole `daemon-contracts` zone now lives as `packages/daemon-contracts`, so the package directory derives the zone and the per-file `ROOT_MODULE_ZONES` row stays deleted; an internal rename needs no ownership-table edit. | +| `packages/daemon-contracts/src/device-claim-recovery.ts` | keep (shared-below-both) | The owner-scoped claim reconciler runs durable-resource recovery for a dead owner's state dir; the daemon runs it at `open` and startup and the daemonless `device release --stale` runs it too, so it lives below both. Runtime recovery may live in `daemon-contracts` only when both sides run it and root composition supplies just the gateway (ADR 0019 section 1); the package depends on `capture-kit`, which cannot depend back on it (a project-reference cycle through `selectors`). | | `src/daemon-policy-file.ts` | moved → `packages/daemon-contracts/src/daemon-policy-file.ts` | Daemon ⇄ client shared contract from #2559, declared `daemon-contracts` by #3288 — the shared-below-both ownership the spine needs, since daemon-client (rank 5) dynamically imports it. The extraction makes that placement physical: the package sits below both sides and its manifest exports own the boundary. | | `src/daemon.ts` | keep | Entry point (`internal/daemon` bundle entry); composition roots stay in root by umbrella §4. | | `src/platform-runtime-apple-runner-owner.ts` | retain for this batch | `platform-runtime-*` composition seam: R13's exact-importer rule and the ADR 0022 R76 inventory key on its path, so relocation requires coordinated ownership changes to both declarations — the rules constrain the move, they do not independently prove the placement. | diff --git a/packages/capture-kit/package.json b/packages/capture-kit/package.json index 1575ec8ea4..49a974cfd9 100644 --- a/packages/capture-kit/package.json +++ b/packages/capture-kit/package.json @@ -18,6 +18,18 @@ "types": "./src/snapshot/android-replacement-surface-occlusion.ts", "default": "./src/snapshot/android-replacement-surface-occlusion.ts" }, + "./app-log-durable-resource": { + "types": "./src/capture-admission/app-log-durable-resource.ts", + "default": "./src/capture-admission/app-log-durable-resource.ts" + }, + "./app-log-files": { + "types": "./src/capture-admission/app-log-files.ts", + "default": "./src/capture-admission/app-log-files.ts" + }, + "./app-log-resource-recovery": { + "types": "./src/capture-admission/app-log-resource-recovery.ts", + "default": "./src/capture-admission/app-log-resource-recovery.ts" + }, "./audio-probe-admission-ledger": { "types": "./src/capture-admission/audio-probe-admission-ledger.ts", "default": "./src/capture-admission/audio-probe-admission-ledger.ts" @@ -38,6 +50,10 @@ "types": "./src/capture-admission/audio-probe-session-resource.ts", "default": "./src/capture-admission/audio-probe-session-resource.ts" }, + "./durable-capture": { + "types": "./src/durable-capture/index.ts", + "default": "./src/durable-capture/index.ts" + }, "./durable-capture-admission-ledger": { "types": "./src/capture-admission/durable-capture-admission-ledger.ts", "default": "./src/capture-admission/durable-capture-admission-ledger.ts" @@ -50,66 +66,14 @@ "types": "./src/capture-admission/durable-capture-runtime-recovery.ts", "default": "./src/capture-admission/durable-capture-runtime-recovery.ts" }, - "./observe-until": { - "types": "./src/observe-until.ts", - "default": "./src/observe-until.ts" - }, - "./perf-capture-admission-ledger": { - "types": "./src/capture-admission/perf-capture-admission-ledger.ts", - "default": "./src/capture-admission/perf-capture-admission-ledger.ts" - }, - "./perf-capture-recovery": { - "types": "./src/capture-admission/perf-capture-resource-recovery.ts", - "default": "./src/capture-admission/perf-capture-resource-recovery.ts" - }, - "./perf-capture-resource-store": { - "types": "./src/capture-admission/perf-capture-resource-store.ts", - "default": "./src/capture-admission/perf-capture-resource-store.ts" - }, - "./perf-capture-session-resource": { - "types": "./src/capture-admission/perf-capture-session-resource.ts", - "default": "./src/capture-admission/perf-capture-session-resource.ts" - }, - "./perf-runtime-plan": { - "types": "./src/capture-admission/perf-runtime-plan.ts", - "default": "./src/capture-admission/perf-runtime-plan.ts" - }, - "./screen-recording-admission-ledger": { - "types": "./src/capture-admission/screen-recording-admission-ledger.ts", - "default": "./src/capture-admission/screen-recording-admission-ledger.ts" - }, - "./screen-recording-recovery": { - "types": "./src/capture-admission/screen-recording-resource-recovery.ts", - "default": "./src/capture-admission/screen-recording-resource-recovery.ts" - }, - "./screen-recording-resource-store": { - "types": "./src/capture-admission/screen-recording-resource-store.ts", - "default": "./src/capture-admission/screen-recording-resource-store.ts" - }, - "./screen-recording-session-resource": { - "types": "./src/capture-admission/screen-recording-session-resource.ts", - "default": "./src/capture-admission/screen-recording-session-resource.ts" - }, - "./screen-recording-stop-recovery": { - "types": "./src/capture-admission/screen-recording-stop-recovery.ts", - "default": "./src/capture-admission/screen-recording-stop-recovery.ts" - }, - "./durable-capture": { - "types": "./src/durable-capture/index.ts", - "default": "./src/durable-capture/index.ts" + "./durable-capture/session-binding": { + "types": "./src/durable-capture/session-binding.ts", + "default": "./src/durable-capture/session-binding.ts" }, "./durable-json": { "types": "./src/durable-json.ts", "default": "./src/durable-json.ts" }, - "./recording-stop-sequence": { - "types": "./src/recording/stop-sequence.ts", - "default": "./src/recording/stop-sequence.ts" - }, - "./screen-recording-transport": { - "types": "./src/screen-recording-transport.ts", - "default": "./src/screen-recording-transport.ts" - }, "./ios-node-interpretation": { "types": "./src/ios-node-interpretation.ts", "default": "./src/ios-node-interpretation.ts" @@ -122,10 +86,6 @@ "types": "./src/ios-snapshot-engine/index.ts", "default": "./src/ios-snapshot-engine/index.ts" }, - "./ios-snapshot-tree": { - "types": "./src/ios-snapshot-engine/tree.ts", - "default": "./src/ios-snapshot-engine/tree.ts" - }, "./ios-snapshot-planning": { "types": "./src/ios-snapshot-planning.ts", "default": "./src/ios-snapshot-planning.ts" @@ -134,10 +94,38 @@ "types": "./src/snapshot/ios-snapshot-runtime.ts", "default": "./src/snapshot/ios-snapshot-runtime.ts" }, + "./ios-snapshot-tree": { + "types": "./src/ios-snapshot-engine/tree.ts", + "default": "./src/ios-snapshot-engine/tree.ts" + }, "./mobile-snapshot-semantics": { "types": "./src/mobile-snapshot-semantics.ts", "default": "./src/mobile-snapshot-semantics.ts" }, + "./observe-until": { + "types": "./src/observe-until.ts", + "default": "./src/observe-until.ts" + }, + "./perf-capture-admission-ledger": { + "types": "./src/capture-admission/perf-capture-admission-ledger.ts", + "default": "./src/capture-admission/perf-capture-admission-ledger.ts" + }, + "./perf-capture-recovery": { + "types": "./src/capture-admission/perf-capture-resource-recovery.ts", + "default": "./src/capture-admission/perf-capture-resource-recovery.ts" + }, + "./perf-capture-resource-store": { + "types": "./src/capture-admission/perf-capture-resource-store.ts", + "default": "./src/capture-admission/perf-capture-resource-store.ts" + }, + "./perf-capture-session-resource": { + "types": "./src/capture-admission/perf-capture-session-resource.ts", + "default": "./src/capture-admission/perf-capture-session-resource.ts" + }, + "./perf-runtime-plan": { + "types": "./src/capture-admission/perf-runtime-plan.ts", + "default": "./src/capture-admission/perf-runtime-plan.ts" + }, "./png": { "types": "./src/png.ts", "default": "./src/png.ts" @@ -190,14 +178,14 @@ "types": "./src/recording/recording-facts.ts", "default": "./src/recording/recording-facts.ts" }, - "./recording-mp4-fixtures": { - "types": "./src/recording/mp4.fixtures.ts", - "default": "./src/recording/mp4.fixtures.ts" - }, "./recording-mp4-duration": { "types": "./src/recording/mp4-duration.ts", "default": "./src/recording/mp4-duration.ts" }, + "./recording-mp4-fixtures": { + "types": "./src/recording/mp4.fixtures.ts", + "default": "./src/recording/mp4.fixtures.ts" + }, "./recording-output-path": { "types": "./src/recording/output-path.ts", "default": "./src/recording/output-path.ts" @@ -206,6 +194,10 @@ "types": "./src/recording/overlay.ts", "default": "./src/recording/overlay.ts" }, + "./recording-stop-sequence": { + "types": "./src/recording/stop-sequence.ts", + "default": "./src/recording/stop-sequence.ts" + }, "./recording-telemetry": { "types": "./src/recording/telemetry.ts", "default": "./src/recording/telemetry.ts" @@ -218,6 +210,30 @@ "types": "./src/snapshot/snapshot-presentation/repeated-nav-subtree.ts", "default": "./src/snapshot/snapshot-presentation/repeated-nav-subtree.ts" }, + "./screen-recording-admission-ledger": { + "types": "./src/capture-admission/screen-recording-admission-ledger.ts", + "default": "./src/capture-admission/screen-recording-admission-ledger.ts" + }, + "./screen-recording-recovery": { + "types": "./src/capture-admission/screen-recording-resource-recovery.ts", + "default": "./src/capture-admission/screen-recording-resource-recovery.ts" + }, + "./screen-recording-resource-store": { + "types": "./src/capture-admission/screen-recording-resource-store.ts", + "default": "./src/capture-admission/screen-recording-resource-store.ts" + }, + "./screen-recording-session-resource": { + "types": "./src/capture-admission/screen-recording-session-resource.ts", + "default": "./src/capture-admission/screen-recording-session-resource.ts" + }, + "./screen-recording-stop-recovery": { + "types": "./src/capture-admission/screen-recording-stop-recovery.ts", + "default": "./src/capture-admission/screen-recording-stop-recovery.ts" + }, + "./screen-recording-transport": { + "types": "./src/screen-recording-transport.ts", + "default": "./src/screen-recording-transport.ts" + }, "./screenshot-density": { "types": "./src/screenshot-density.ts", "default": "./src/screenshot-density.ts" @@ -325,10 +341,6 @@ "./touch-reference-frame": { "types": "./src/snapshot/touch-reference-frame.ts", "default": "./src/snapshot/touch-reference-frame.ts" - }, - "./durable-capture/session-binding": { - "types": "./src/durable-capture/session-binding.ts", - "default": "./src/durable-capture/session-binding.ts" } }, "devDependencies": { diff --git a/packages/capture-kit/src/capture-admission/app-log-durable-resource.ts b/packages/capture-kit/src/capture-admission/app-log-durable-resource.ts new file mode 100644 index 0000000000..6383f886a6 --- /dev/null +++ b/packages/capture-kit/src/capture-admission/app-log-durable-resource.ts @@ -0,0 +1,26 @@ +import type { AppLogCompletion, AppLogLiveHandle } from '@agent-device/contracts/app-log-runtime'; +import { createDurableCaptureResource } from './durable-capture-resource.ts'; +import { appLogResourceStore } from './app-log-resource-store.ts'; + +export const appLogDurableResource = createDurableCaptureResource< + 'app-log', + AppLogLiveHandle, + AppLogCompletion +>({ + resourceKind: 'app-log', + displayName: 'app-log', + store: appLogResourceStore, + completionMetadata: (completion) => ({ + backend: completion.backend, + outputPath: completion.outputPath, + completedAt: completion.completedAt, + }), + failedFinishPolicy: 'dispose-on-failed-finish', + messages: { + noActive: 'no app log stream active', + cleanupPendingHint: + 'Keep app-log.resource.json and retry cleanup through its exact runtime owner.', + }, +}); + +export { appLogResourceStore }; diff --git a/packages/daemon/src/resources/app-log-files.ts b/packages/capture-kit/src/capture-admission/app-log-files.ts similarity index 100% rename from packages/daemon/src/resources/app-log-files.ts rename to packages/capture-kit/src/capture-admission/app-log-files.ts diff --git a/packages/daemon/src/resources/app-log-resource-recovery.ts b/packages/capture-kit/src/capture-admission/app-log-resource-recovery.ts similarity index 93% rename from packages/daemon/src/resources/app-log-resource-recovery.ts rename to packages/capture-kit/src/capture-admission/app-log-resource-recovery.ts index aea6a5d548..905347ac19 100644 --- a/packages/daemon/src/resources/app-log-resource-recovery.ts +++ b/packages/capture-kit/src/capture-admission/app-log-resource-recovery.ts @@ -12,10 +12,10 @@ import { import type { DurableCaptureRecoveryControl, DurableCaptureRecoverySummary, -} from '@agent-device/capture-kit/durable-capture'; +} from '../durable-capture/index.ts'; import type { ResourceDiagnostic } from '@agent-device/host-kit/diagnostics'; -import { appLogDurableResource } from './app-log-session-resource.ts'; -import { acquireExactDurableCaptureRecoveryControl } from '@agent-device/capture-kit/durable-capture-runtime-recovery'; +import { appLogDurableResource } from './app-log-durable-resource.ts'; +import { acquireExactDurableCaptureRecoveryControl } from './durable-capture-runtime-recovery.ts'; const appLogRecoveryUse = defineUse({ required: ['appLogReattach', 'appLogCleanup'], diff --git a/packages/daemon/src/resources/__tests__/app-log-resource-store.test.ts b/packages/capture-kit/src/capture-admission/app-log-resource-store.test.ts similarity index 94% rename from packages/daemon/src/resources/__tests__/app-log-resource-store.test.ts rename to packages/capture-kit/src/capture-admission/app-log-resource-store.test.ts index f2e433db18..2dfedf0f5d 100644 --- a/packages/daemon/src/resources/__tests__/app-log-resource-store.test.ts +++ b/packages/capture-kit/src/capture-admission/app-log-resource-store.test.ts @@ -2,9 +2,9 @@ import fs from 'node:fs'; import path from 'node:path'; import { expect, test } from 'vitest'; import { localRuntimeOwner } from '@agent-device/contracts/platform-runtime'; -import { createDurableResourceEnvelope } from '@agent-device/capture-kit'; -import { mkdtempForTestSync } from '@agent-device/test-kit/tmp-dir'; -import { appLogResourceStore } from '../app-log-resource-store.ts'; +import { createDurableResourceEnvelope } from '../durable-resource-envelope.ts'; +import { mkdtempForTestSync } from '../tmp-dir.fixtures.ts'; +import { appLogResourceStore } from './app-log-resource-store.ts'; test('app-log resource records publish atomically with owner-only permissions', () => { const resourcePath = appLogResourceStore.resolvePath( diff --git a/packages/daemon/src/resources/app-log-resource-store.ts b/packages/capture-kit/src/capture-admission/app-log-resource-store.ts similarity index 63% rename from packages/daemon/src/resources/app-log-resource-store.ts rename to packages/capture-kit/src/capture-admission/app-log-resource-store.ts index 5f930281b1..534c7267da 100644 --- a/packages/daemon/src/resources/app-log-resource-store.ts +++ b/packages/capture-kit/src/capture-admission/app-log-resource-store.ts @@ -1,4 +1,4 @@ -import { createDurableCaptureResourceStore } from '@agent-device/capture-kit/durable-capture'; +import { createDurableCaptureResourceStore } from '../durable-capture/index.ts'; export const appLogResourceStore = createDurableCaptureResourceStore({ resourceKind: 'app-log', diff --git a/packages/capture-kit/src/durable-resource-envelope.ts b/packages/capture-kit/src/durable-resource-envelope.ts index 979672d6b1..297299251a 100644 --- a/packages/capture-kit/src/durable-resource-envelope.ts +++ b/packages/capture-kit/src/durable-resource-envelope.ts @@ -1,14 +1,4 @@ -import { - isAppleOs, - isDeviceTarget, - isPlatform, - type AppleOS, - type DeviceIdentity, - type DeviceInfo, - type DeviceKind, - type DeviceTarget, - type Platform, -} from '@agent-device/kernel/device'; +import { decodeDeviceIdentity, isPlatform, type DeviceIdentity } from '@agent-device/kernel/device'; import type { JsonObject } from '@agent-device/contracts/client'; import type { DurableDescriptorCodec, @@ -132,78 +122,6 @@ function decodeRuntimeOwnerRef(value: unknown): RuntimeOwnerRef | null { return null; } -export function decodeDeviceIdentity(value: unknown): DeviceIdentity | null { - if (!isObject(value)) return null; - const id = readDeviceIdentityId(value.id); - const family = readDeviceIdentityFamily(value.family); - const kind = readDeviceIdentityKind(value.kind); - const target = readDeviceIdentityTarget(value.target); - if (!id || !family || !kind || target === null) return null; - if (!hasCoherentAppleIdentity(value, family, kind)) return null; - return buildDeviceIdentity(value, id, family, kind, target); -} - -function buildDeviceIdentity( - value: Record & { - appleOs?: AppleOS; - iosPhysicalDeviceBackend?: DeviceInfo['iosPhysicalDeviceBackend']; - }, - id: string, - family: Platform, - kind: DeviceKind, - target: DeviceTarget | undefined, -): DeviceIdentity { - return Object.freeze({ - id, - family, - ...(value.appleOs === undefined ? {} : { appleOs: value.appleOs }), - kind, - ...(target === undefined ? {} : { target }), - ...(value.iosPhysicalDeviceBackend === undefined - ? {} - : { iosPhysicalDeviceBackend: value.iosPhysicalDeviceBackend }), - }); -} - -function readDeviceIdentityId(value: unknown): string | null { - return isNonEmptyString(value) ? value : null; -} - -function readDeviceIdentityFamily(value: unknown): Platform | null { - return isPlatform(value) ? value : null; -} - -function readDeviceIdentityKind(value: unknown): DeviceKind | null { - return isDeviceKind(value) ? value : null; -} - -function readDeviceIdentityTarget(value: unknown): DeviceTarget | undefined | null { - if (value === undefined) return undefined; - return isDeviceTarget(value) ? value : null; -} - -function hasCoherentAppleIdentity( - value: Record, - family: Platform, - kind: DeviceKind, -): value is Record & { - appleOs?: AppleOS; - iosPhysicalDeviceBackend?: DeviceInfo['iosPhysicalDeviceBackend']; -} { - if (family === 'apple') { - if (!isAppleOs(value.appleOs)) return false; - } else if (value.appleOs !== undefined || value.iosPhysicalDeviceBackend !== undefined) { - return false; - } - return hasValidPhysicalAppleBackend(value.iosPhysicalDeviceBackend, kind); -} - -function hasValidPhysicalAppleBackend(value: unknown, kind: DeviceKind): boolean { - if (value === undefined) return true; - if (kind !== 'device') return false; - return value === 'coredevice' || value === 'xctest'; -} - function decodeFence(value: unknown): ResourceOwnershipFence | null { if ( !isObject(value) || @@ -230,10 +148,6 @@ function isLifecycleState(value: unknown): value is DurableResourceLifecycleStat return value === 'open' || value === 'completed'; } -function isDeviceKind(value: unknown): value is DeviceKind { - return value === 'simulator' || value === 'emulator' || value === 'device'; -} - function isNonEmptyString(value: unknown): value is string { return typeof value === 'string' && value.trim().length > 0; } diff --git a/packages/capture-kit/src/index.ts b/packages/capture-kit/src/index.ts index e5cacd3ab2..5c83dd4f4e 100644 --- a/packages/capture-kit/src/index.ts +++ b/packages/capture-kit/src/index.ts @@ -1,6 +1,5 @@ export { createDurableResourceEnvelope, - decodeDeviceIdentity, encodeDurableDescriptor, } from './durable-resource-envelope.ts'; export { diff --git a/packages/daemon-contracts/package.json b/packages/daemon-contracts/package.json index a915e7da02..aef1c885c6 100644 --- a/packages/daemon-contracts/package.json +++ b/packages/daemon-contracts/package.json @@ -3,8 +3,9 @@ "version": "0.0.0", "private": true, "type": "module", - "description": "On-disk and wire contracts a daemon process shares with its clients (#2559): state-dir and transport resolution, registration and ownership, process liveness, shutdown reports, the repair tombstone, progress-envelope framing, the diagnostics scope, and owner-cleanup vocabulary. Internal workspace package bundled into the published agent-device artifact.", + "description": "On-disk and wire contracts a daemon process shares with its clients (#2559): state-dir and transport resolution, registration and ownership, process liveness, shutdown reports, the repair tombstone, progress-envelope framing, the diagnostics scope, owner-cleanup vocabulary, the stop path, session artifact paths, and the on-disk device-claim record, store and inspection a CLI shares with the daemon. Internal workspace package bundled into the published agent-device artifact.", "dependencies": { + "@agent-device/capture-kit": "workspace:*", "@agent-device/command-registry": "workspace:*", "@agent-device/contracts": "workspace:*", "@agent-device/host-kit": "workspace:*", @@ -47,6 +48,22 @@ "types": "./src/daemon-shutdown-report.ts", "default": "./src/daemon-shutdown-report.ts" }, + "./daemon-stop": { + "types": "./src/daemon-stop.ts", + "default": "./src/daemon-stop.ts" + }, + "./device-claim-recovery": { + "types": "./src/device-claim-recovery.ts", + "default": "./src/device-claim-recovery.ts" + }, + "./device-claims": { + "types": "./src/device-claims.ts", + "default": "./src/device-claims.ts" + }, + "./device-claims-fixtures": { + "types": "./src/device-claims.fixtures.ts", + "default": "./src/device-claims.fixtures.ts" + }, "./provider-credentials": { "types": "./src/provider-credentials.ts", "default": "./src/provider-credentials.ts" @@ -55,6 +72,10 @@ "types": "./src/request-progress-protocol.ts", "default": "./src/request-progress-protocol.ts" }, + "./session-artifact-paths": { + "types": "./src/session-artifact-paths.ts", + "default": "./src/session-artifact-paths.ts" + }, "./session-repair-tombstone": { "types": "./src/session-repair-tombstone.ts", "default": "./src/session-repair-tombstone.ts" diff --git a/packages/daemon/src/resources/device/claim-recovery-gateway.ts b/packages/daemon-contracts/src/claim-recovery-gateway.ts similarity index 100% rename from packages/daemon/src/resources/device/claim-recovery-gateway.ts rename to packages/daemon-contracts/src/claim-recovery-gateway.ts diff --git a/packages/daemon/src/server/__tests__/daemon-stop.test.ts b/packages/daemon-contracts/src/daemon-stop.test.ts similarity index 95% rename from packages/daemon/src/server/__tests__/daemon-stop.test.ts rename to packages/daemon-contracts/src/daemon-stop.test.ts index 926c258fe3..0c886a8fd3 100644 --- a/packages/daemon/src/server/__tests__/daemon-stop.test.ts +++ b/packages/daemon-contracts/src/daemon-stop.test.ts @@ -1,14 +1,14 @@ import fs from 'node:fs'; import { afterEach, expect, test, vi } from 'vitest'; -import { mkdtempForTestSync } from '@agent-device/test-kit/tmp-dir'; +import { mkdtempForTestSync } from './tmp-dir.fixtures.ts'; const mocks = vi.hoisted(() => ({ stopAndRetireDaemon: vi.fn() })); -vi.mock('@agent-device/daemon-contracts/daemon-registration-owner', () => ({ +vi.mock('./daemon-registration-owner.ts', () => ({ stopAndRetireDaemon: mocks.stopAndRetireDaemon, })); -import { resolveDaemonPaths } from '@agent-device/daemon-contracts/daemon-resolution'; -import { stopDaemon } from '../daemon-stop.ts'; +import { resolveDaemonPaths } from './daemon-resolution.ts'; +import { stopDaemon } from './daemon-stop.ts'; afterEach(() => { vi.resetAllMocks(); diff --git a/packages/daemon/src/server/daemon-stop.ts b/packages/daemon-contracts/src/daemon-stop.ts similarity index 89% rename from packages/daemon/src/server/daemon-stop.ts rename to packages/daemon-contracts/src/daemon-stop.ts index f9482dc2a7..f4a6d67639 100644 --- a/packages/daemon/src/server/daemon-stop.ts +++ b/packages/daemon-contracts/src/daemon-stop.ts @@ -1,13 +1,10 @@ import { AppError } from '@agent-device/kernel/errors'; -import type { DaemonRetirementResult } from '@agent-device/daemon-contracts/daemon-registration-owner'; +import type { DaemonRetirementResult } from './daemon-registration-owner.ts'; import type { OwnerIdentity } from '@agent-device/host-kit/process'; -import type { DaemonPaths } from '@agent-device/daemon-contracts/daemon-resolution'; -import { readRegisteredDaemonIdentity } from '@agent-device/daemon-contracts/daemon-registration'; -import type { - DeviceClaimRecord, - ProviderReleaseRecord, -} from '@agent-device/daemon-contracts/daemon-shutdown-report'; +import type { DaemonPaths } from './daemon-resolution.ts'; +import { readRegisteredDaemonIdentity } from './daemon-registration.ts'; +import type { DeviceClaimRecord, ProviderReleaseRecord } from './daemon-shutdown-report.ts'; const DAEMON_STOP_GRACE_TIMEOUT_MS = 10_000; const DAEMON_STOP_KILL_TIMEOUT_MS = 2_000; @@ -46,8 +43,7 @@ export async function stopDaemon(params: { }): Promise { const info = readRegisteredDaemonIdentity(params.paths.infoPath); if (!info) return notRunningResult(); - const { stopAndRetireDaemon } = - await import('@agent-device/daemon-contracts/daemon-registration-owner'); + const { stopAndRetireDaemon } = await import('./daemon-registration-owner.ts'); const retirement = await stopAndRetireDaemon({ paths: params.paths, observed: info, diff --git a/packages/daemon/src/resources/device/device-claim-inspection.ts b/packages/daemon-contracts/src/device-claim-inspection.ts similarity index 98% rename from packages/daemon/src/resources/device/device-claim-inspection.ts rename to packages/daemon-contracts/src/device-claim-inspection.ts index fdecfb89c7..00e83dc551 100644 --- a/packages/daemon/src/resources/device/device-claim-inspection.ts +++ b/packages/daemon-contracts/src/device-claim-inspection.ts @@ -7,7 +7,7 @@ import { readHostProcessIdentityObservations, } from '@agent-device/host-kit/process'; -import { isSupersededDaemonOwner } from '@agent-device/daemon-contracts/daemon-registration'; +import { isSupersededDaemonOwner } from './daemon-registration.ts'; import { resolveDeviceClaimRoot } from './device-claim-paths.ts'; import { decodeStoredDeviceClaim, type DeviceClaim } from './device-claim-record.ts'; diff --git a/packages/daemon/src/server/device/device-claim-owner-recovery.ts b/packages/daemon-contracts/src/device-claim-owner-recovery.ts similarity index 88% rename from packages/daemon/src/server/device/device-claim-owner-recovery.ts rename to packages/daemon-contracts/src/device-claim-owner-recovery.ts index a9f6895dd5..dca3086fcb 100644 --- a/packages/daemon/src/server/device/device-claim-owner-recovery.ts +++ b/packages/daemon-contracts/src/device-claim-owner-recovery.ts @@ -1,14 +1,14 @@ -import type { PlatformRequestScope } from '@agent-device/contracts/platform-runtime-host'; -import { createOwnedProcessRecordStore } from '@agent-device/host-kit/process'; -import { resolveDaemonPaths } from '@agent-device/daemon-contracts/daemon-resolution'; -import { createDeviceClaimReconciler } from '../../sessions/device/device-claim-reconciliation.ts'; -import type { DeviceClaimReconciler } from '../../resources/device/device-claims.ts'; -import type { ClaimRecoveryGatewayFactory } from '../../resources/device/claim-recovery-gateway.ts'; import { resolveSessionDir, resolveSessionAppLogPath, resolveSessionAppLogPidPath, -} from '../../core/session-artifact-paths.ts'; +} from './session-artifact-paths.ts'; +import type { PlatformRequestScope } from '@agent-device/contracts/platform-runtime-host'; +import { createOwnedProcessRecordStore } from '@agent-device/host-kit/process'; +import { resolveDaemonPaths } from './daemon-resolution.ts'; +import { createDeviceClaimReconciler } from './device-claim-reconciliation.ts'; +import type { DeviceClaimReconciler } from './device-claims.ts'; +import type { ClaimRecoveryGatewayFactory } from './claim-recovery-gateway.ts'; export type OwnerScopedClaimRecovery = { reconcile: DeviceClaimReconciler; diff --git a/packages/daemon/src/resources/device/device-claim-paths.ts b/packages/daemon-contracts/src/device-claim-paths.ts similarity index 100% rename from packages/daemon/src/resources/device/device-claim-paths.ts rename to packages/daemon-contracts/src/device-claim-paths.ts diff --git a/packages/daemon/src/resources/device/__tests__/device-claim-reboot.test.ts b/packages/daemon-contracts/src/device-claim-reboot.test.ts similarity index 95% rename from packages/daemon/src/resources/device/__tests__/device-claim-reboot.test.ts rename to packages/daemon-contracts/src/device-claim-reboot.test.ts index d803d18dd4..a41ab90309 100644 --- a/packages/daemon/src/resources/device/__tests__/device-claim-reboot.test.ts +++ b/packages/daemon-contracts/src/device-claim-reboot.test.ts @@ -5,8 +5,8 @@ import type { DeviceBootObservationService, } from '@agent-device/contracts/device-boot'; import type { DeviceInfo } from '@agent-device/kernel/device'; -import { rebootedDeviceClaim } from '../device-claim-reboot.ts'; -import type { DeviceClaim } from '../device-claim-record.ts'; +import { rebootedDeviceClaim } from './device-claim-reboot.ts'; +import type { DeviceClaim } from './device-claim-record.ts'; const device: DeviceInfo = { platform: 'apple', diff --git a/packages/daemon/src/resources/device/device-claim-reboot.ts b/packages/daemon-contracts/src/device-claim-reboot.ts similarity index 100% rename from packages/daemon/src/resources/device/device-claim-reboot.ts rename to packages/daemon-contracts/src/device-claim-reboot.ts diff --git a/packages/daemon/src/sessions/device/device-claim-reconciliation.ts b/packages/daemon-contracts/src/device-claim-reconciliation.ts similarity index 92% rename from packages/daemon/src/sessions/device/device-claim-reconciliation.ts rename to packages/daemon-contracts/src/device-claim-reconciliation.ts index eb55c86401..bbc4e193e0 100644 --- a/packages/daemon/src/sessions/device/device-claim-reconciliation.ts +++ b/packages/daemon-contracts/src/device-claim-reconciliation.ts @@ -1,14 +1,13 @@ +import { appLogDurableResource } from '@agent-device/capture-kit/app-log-durable-resource'; +import { recoverAppLogResourceAfterDaemonLock } from '@agent-device/capture-kit/app-log-resource-recovery'; import path from 'node:path'; import type { DeviceRuntimeGateway } from '@agent-device/contracts/platform-runtime'; import type { PlatformRequestScope } from '@agent-device/contracts/platform-runtime-host'; import type { PlatformRuntimeOperations } from '@agent-device/contracts/platform-runtime-operations'; import { sameDeviceIdentity } from '@agent-device/kernel/device'; import { emitDiagnostic } from '@agent-device/host-kit/diagnostics'; -import { resolveDaemonPaths } from '@agent-device/daemon-contracts/daemon-resolution'; -import { appLogDurableResource } from '../../resources/app-log-session-resource.ts'; -import { recoverAppLogResourceAfterDaemonLock } from '../../resources/app-log-resource-recovery.ts'; -import type { DeviceClaim } from '../../resources/device/device-claim-record.ts'; -import type { DeviceClaimReconciler } from '../../resources/device/device-claims.ts'; +import { resolveDaemonPaths } from './daemon-resolution.ts'; +import type { DeviceClaim, DeviceClaimReconciler } from './device-claims.ts'; import type { DurableCaptureRecoveryOutcome } from '@agent-device/capture-kit/durable-capture'; import { recoverAudioProbeResourceAfterDaemonLock } from '@agent-device/capture-kit/audio-probe-recovery'; import { audioProbeDurableResource } from '@agent-device/capture-kit/audio-probe-session-resource'; diff --git a/packages/daemon/src/resources/device/__tests__/device-claim-record.test.ts b/packages/daemon-contracts/src/device-claim-record.test.ts similarity index 96% rename from packages/daemon/src/resources/device/__tests__/device-claim-record.test.ts rename to packages/daemon-contracts/src/device-claim-record.test.ts index 0a1ba78d6b..d56ff0962e 100644 --- a/packages/daemon/src/resources/device/__tests__/device-claim-record.test.ts +++ b/packages/daemon-contracts/src/device-claim-record.test.ts @@ -1,5 +1,5 @@ import { expect, test } from 'vitest'; -import { decodeStoredDeviceClaim } from '../device-claim-record.ts'; +import { decodeStoredDeviceClaim } from './device-claim-record.ts'; const DEVICE_KEY = 'local:android:none:emulator-5554'; diff --git a/packages/daemon/src/resources/device/device-claim-record.ts b/packages/daemon-contracts/src/device-claim-record.ts similarity index 98% rename from packages/daemon/src/resources/device/device-claim-record.ts rename to packages/daemon-contracts/src/device-claim-record.ts index af39de18b8..974e1400fc 100644 --- a/packages/daemon/src/resources/device/device-claim-record.ts +++ b/packages/daemon-contracts/src/device-claim-record.ts @@ -1,9 +1,9 @@ import { deviceFieldsFromPublicPlatform, + decodeDeviceIdentity, isPublicPlatform, type DeviceIdentity, } from '@agent-device/kernel/device'; -import { decodeDeviceIdentity } from '@agent-device/capture-kit'; import { isRecord } from '@agent-device/kernel/record'; import { canonicalLocalDeviceKey } from './device-claim-paths.ts'; diff --git a/packages/daemon-contracts/src/device-claim-recovery.ts b/packages/daemon-contracts/src/device-claim-recovery.ts new file mode 100644 index 0000000000..0b68939168 --- /dev/null +++ b/packages/daemon-contracts/src/device-claim-recovery.ts @@ -0,0 +1,6 @@ +export type { ClaimRecoveryGatewayFactory } from './claim-recovery-gateway.ts'; +export { + createOwnerScopedDeviceClaimReconciler, + type OwnerScopedClaimRecoveryComposer, +} from './device-claim-owner-recovery.ts'; +export { createRecoveryPlatformScope } from './recovery-platform-scope.ts'; diff --git a/packages/daemon/src/resources/device/device-claim-settlement.ts b/packages/daemon-contracts/src/device-claim-settlement.ts similarity index 100% rename from packages/daemon/src/resources/device/device-claim-settlement.ts rename to packages/daemon-contracts/src/device-claim-settlement.ts diff --git a/packages/daemon/src/resources/device/device-claim-store.ts b/packages/daemon-contracts/src/device-claim-store.ts similarity index 100% rename from packages/daemon/src/resources/device/device-claim-store.ts rename to packages/daemon-contracts/src/device-claim-store.ts diff --git a/packages/daemon-contracts/src/device-claims.fixtures.ts b/packages/daemon-contracts/src/device-claims.fixtures.ts new file mode 100644 index 0000000000..8fbf1d6f35 --- /dev/null +++ b/packages/daemon-contracts/src/device-claims.fixtures.ts @@ -0,0 +1,2 @@ +export { resolveDeviceClaimPath } from './device-claim-paths.ts'; +export { createDeviceClaimReconciler } from './device-claim-reconciliation.ts'; diff --git a/packages/daemon/src/resources/device/device-claims.ts b/packages/daemon-contracts/src/device-claims.ts similarity index 97% rename from packages/daemon/src/resources/device/device-claims.ts rename to packages/daemon-contracts/src/device-claims.ts index 8ee603df55..2fbd71f0db 100644 --- a/packages/daemon/src/resources/device/device-claims.ts +++ b/packages/daemon-contracts/src/device-claims.ts @@ -41,6 +41,19 @@ import { } from './device-claim-settlement.ts'; import { withDeviceClaimLock, writeDeviceClaim } from './device-claim-store.ts'; +export { canonicalLocalDeviceKey } from './device-claim-paths.ts'; +export { + deviceClaimOwnerCannotRelease, + deviceClaimRequiresStaleInspection, + inspectDeviceClaims, +} from './device-claim-inspection.ts'; +export type { + DeviceClaimClassification, + DeviceClaimSelectors, + InspectedDeviceClaim, +} from './device-claim-inspection.ts'; +export type { DeviceClaim } from './device-claim-record.ts'; +export type { TakenOverDeviceClaim } from './device-claim-reboot.ts'; export type { DeviceClaimReconciler } from './device-claim-settlement.ts'; export type { DeviceClaimSessionOwnership } from './device-claim-record.ts'; diff --git a/packages/daemon-contracts/src/recovery-platform-scope.ts b/packages/daemon-contracts/src/recovery-platform-scope.ts new file mode 100644 index 0000000000..4b3e4f92fb --- /dev/null +++ b/packages/daemon-contracts/src/recovery-platform-scope.ts @@ -0,0 +1,20 @@ +import type { + PlatformDiagnosticEvent, + PlatformRequestScope, +} from '@agent-device/contracts/platform-runtime-host'; +import { emitDiagnostic } from '@agent-device/host-kit/diagnostics'; + +const processLifetimeSignal = new AbortController().signal; + +/** Process-owned scope for post-lock durable-resource recovery before request admission. */ +export function createRecoveryPlatformScope(): PlatformRequestScope { + return Object.freeze({ + signal: processLifetimeSignal, + diagnostics: Object.freeze({ + emit: (event: PlatformDiagnosticEvent) => emitDiagnostic(event), + }), + progress: Object.freeze({ + report: () => {}, + }), + }); +} diff --git a/packages/daemon/src/core/__tests__/managed-session-artifact-path.test.ts b/packages/daemon-contracts/src/session-artifact-paths.test.ts similarity index 70% rename from packages/daemon/src/core/__tests__/managed-session-artifact-path.test.ts rename to packages/daemon-contracts/src/session-artifact-paths.test.ts index 7e818990e4..f5831fe17d 100644 --- a/packages/daemon/src/core/__tests__/managed-session-artifact-path.test.ts +++ b/packages/daemon-contracts/src/session-artifact-paths.test.ts @@ -1,9 +1,33 @@ +import { afterEach, expect, test } from 'vitest'; +import assert from 'node:assert/strict'; import fs from 'node:fs'; - import path from 'node:path'; -import { afterEach, expect, test } from 'vitest'; -import { requireManagedSessionArtifactPath } from '../managed-session-artifact-path.ts'; -import { mkdtempForTestSync } from '@agent-device/test-kit/tmp-dir'; +import { AppError } from '@agent-device/kernel/errors'; +import { requireManagedSessionArtifactPath, resolveSessionDir } from './session-artifact-paths.ts'; +import { mkdtempForTestSync } from './tmp-dir.fixtures.ts'; + +test('resolveSessionDir keeps every session dir beneath the sessions dir', () => { + const sessionsDir = path.join( + mkdtempForTestSync('agent-device-tests'), + 'agent-device-tests', + 'sessions', + ); + assert.equal(resolveSessionDir(sessionsDir, 'a/b:c d'), path.join(sessionsDir, 'a_b_c_d')); + // `.` and `..` survive `safeSessionName` unchanged, so without an explicit + // refusal `path.join` resolves them to the sessions dir itself and its parent + // (the daemon state dir): a remote caller's `--session ..` would then land + // app.log / runner.log / requests/*.ndjson outside the sessions tree. + for (const name of ['.', '..', '']) { + assert.throws( + () => resolveSessionDir(sessionsDir, name), + (error: unknown) => + error instanceof AppError && + error.code === 'INVALID_ARGS' && + /session name/i.test(error.message), + `expected resolveSessionDir(${JSON.stringify(name)}) to reject`, + ); + } +}); const roots: string[] = []; diff --git a/packages/daemon/src/core/session-artifact-paths.ts b/packages/daemon-contracts/src/session-artifact-paths.ts similarity index 72% rename from packages/daemon/src/core/session-artifact-paths.ts rename to packages/daemon-contracts/src/session-artifact-paths.ts index b445ed1ec1..05694566a8 100644 --- a/packages/daemon/src/core/session-artifact-paths.ts +++ b/packages/daemon-contracts/src/session-artifact-paths.ts @@ -1,3 +1,4 @@ +import fs from 'node:fs'; import path from 'node:path'; import type { DiagnosticsRecordRef } from '@agent-device/kernel/errors'; import { AppError } from '@agent-device/kernel/errors'; @@ -74,3 +75,30 @@ export function resolveSessionAppLogPath(sessionsDir: string, address: string): export function resolveSessionAppLogPidPath(sessionsDir: string, address: string): string { return path.join(resolveSessionDir(sessionsDir, address), 'app-log.pid'); } + +type ManagedSessionArtifactPath = Readonly<{ + sessionsDir: string; + pathname: string; + basename: string; + label: string; +}>; + +export function requireManagedSessionArtifactPath(input: ManagedSessionArtifactPath): string { + const root = path.resolve(input.sessionsDir); + const resolved = path.resolve(input.pathname); + if (path.basename(resolved) !== input.basename) { + throw new Error(`${input.label} is outside the daemon-owned sessions directory`); + } + if (fs.existsSync(root) && fs.existsSync(path.dirname(resolved))) { + const realRoot = fs.realpathSync.native(root); + const realParent = fs.realpathSync.native(path.dirname(resolved)); + if (realParent !== realRoot && !realParent.startsWith(`${realRoot}${path.sep}`)) { + throw new Error(`${input.label} resolves outside the daemon-owned sessions directory`); + } + return path.join(realParent, input.basename); + } + if (resolved !== root && !resolved.startsWith(`${root}${path.sep}`)) { + throw new Error(`${input.label} is outside the daemon-owned sessions directory`); + } + return resolved; +} diff --git a/packages/daemon-contracts/tsconfig.json b/packages/daemon-contracts/tsconfig.json index e6eb415c43..ab0246c7e8 100644 --- a/packages/daemon-contracts/tsconfig.json +++ b/packages/daemon-contracts/tsconfig.json @@ -11,6 +11,7 @@ }, "include": ["src"], "references": [ + { "path": "../capture-kit" }, { "path": "../command-registry" }, { "path": "../contracts" }, { "path": "../host-kit" }, diff --git a/packages/daemon/package.json b/packages/daemon/package.json index c1741c8c15..e42d31d98c 100644 --- a/packages/daemon/package.json +++ b/packages/daemon/package.json @@ -31,10 +31,6 @@ "yaml": "^2.9.0" }, "exports": { - "./app-log-files": { - "types": "./src/resources/app-log-files.ts", - "default": "./src/resources/app-log-files.ts" - }, "./daemon-command-registry": { "types": "./src/core/daemon-command-registry.ts", "default": "./src/core/daemon-command-registry.ts" @@ -43,38 +39,10 @@ "types": "./src/core/daemon-request.ts", "default": "./src/core/daemon-request.ts" }, - "./daemon-stop": { - "types": "./src/server/daemon-stop.ts", - "default": "./src/server/daemon-stop.ts" - }, - "./device/claim-recovery-gateway": { - "types": "./src/resources/device/claim-recovery-gateway.ts", - "default": "./src/resources/device/claim-recovery-gateway.ts" - }, - "./device/device-claim-inspection": { - "types": "./src/resources/device/device-claim-inspection.ts", - "default": "./src/resources/device/device-claim-inspection.ts" - }, - "./device/device-claim-owner-recovery": { - "types": "./src/server/device/device-claim-owner-recovery.ts", - "default": "./src/server/device/device-claim-owner-recovery.ts" - }, - "./device/device-claims": { - "types": "./src/resources/device/device-claims.ts", - "default": "./src/resources/device/device-claims.ts" - }, - "./managed-session-artifact-path": { - "types": "./src/core/managed-session-artifact-path.ts", - "default": "./src/core/managed-session-artifact-path.ts" - }, "./platform-owner-lifecycle": { "types": "./src/core/platform-owner-lifecycle.ts", "default": "./src/core/platform-owner-lifecycle.ts" }, - "./platform-request-scope": { - "types": "./src/server/platform-request-scope.ts", - "default": "./src/server/platform-request-scope.ts" - }, "./platform-resource-cleanup": { "types": "./src/core/platform-resource-cleanup.ts", "default": "./src/core/platform-resource-cleanup.ts" @@ -91,10 +59,6 @@ "types": "./src/server/http/daemon-runtime.ts", "default": "./src/server/http/daemon-runtime.ts" }, - "./session-artifact-paths": { - "types": "./src/core/session-artifact-paths.ts", - "default": "./src/core/session-artifact-paths.ts" - }, "./testing/__tests__/android-observation-fixture": { "types": "./src/__tests__/support/android-observation-fixture.ts", "default": "./src/__tests__/support/android-observation-fixture.ts" @@ -207,10 +171,6 @@ "types": "./src/execution/app-events.ts", "default": "./src/execution/app-events.ts" }, - "./testing/app-log-resource-store": { - "types": "./src/resources/app-log-resource-store.ts", - "default": "./src/resources/app-log-resource-store.ts" - }, "./testing/application-lifecycle-recovery": { "types": "./src/server/application-lifecycle-recovery.ts", "default": "./src/server/application-lifecycle-recovery.ts" @@ -235,10 +195,6 @@ "types": "./src/resources/device/device-claim-conflict.ts", "default": "./src/resources/device/device-claim-conflict.ts" }, - "./testing/device/device-claim-paths": { - "types": "./src/resources/device/device-claim-paths.ts", - "default": "./src/resources/device/device-claim-paths.ts" - }, "./testing/device/device-ready": { "types": "./src/core/device/device-ready.ts", "default": "./src/core/device/device-ready.ts" diff --git a/packages/daemon/src/__tests__/support/device-claim-store.ts b/packages/daemon/src/__tests__/support/device-claim-store.ts index ff8291153e..bc59364841 100644 --- a/packages/daemon/src/__tests__/support/device-claim-store.ts +++ b/packages/daemon/src/__tests__/support/device-claim-store.ts @@ -2,7 +2,7 @@ import fs from 'node:fs'; import path from 'node:path'; import { afterEach } from 'vitest'; import { mkdtempForTestSync } from '@agent-device/test-kit/tmp-dir'; -import type { DeviceClaimReconciler } from '../../resources/device/device-claims.ts'; +import type { DeviceClaimReconciler } from '@agent-device/daemon-contracts/device-claims'; export type IsolatedDeviceClaimStore = { /** Temporary root holding both the claim store and the daemon state dir. */ diff --git a/packages/daemon/src/__tests__/support/platform-services-fixture.ts b/packages/daemon/src/__tests__/support/platform-services-fixture.ts index 8a1aad0e95..cf27bae977 100644 --- a/packages/daemon/src/__tests__/support/platform-services-fixture.ts +++ b/packages/daemon/src/__tests__/support/platform-services-fixture.ts @@ -1,8 +1,8 @@ +import { type ClaimRecoveryGatewayFactory } from '@agent-device/daemon-contracts/device-claim-recovery'; import { vi } from 'vitest'; import type { DeviceInfo } from '@agent-device/kernel/device'; import type { SessionSurface } from '@agent-device/contracts/session'; import type { DeviceBootObservation } from '@agent-device/contracts/device-boot'; -import type { ClaimRecoveryGatewayFactory } from '../../resources/device/claim-recovery-gateway.ts'; import type { DaemonPlatformServices } from '../../core/platform-services.ts'; import { unavailableDeviceRuntimeGateway } from './unavailable-device-runtime-gateway.ts'; diff --git a/packages/daemon/src/core/__tests__/session-artifact-paths.test.ts b/packages/daemon/src/core/__tests__/session-artifact-paths.test.ts deleted file mode 100644 index 6afb9fb55e..0000000000 --- a/packages/daemon/src/core/__tests__/session-artifact-paths.test.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { test } from 'vitest'; -import assert from 'node:assert/strict'; -import path from 'node:path'; -import { AppError } from '@agent-device/kernel/errors'; -import { resolveSessionDir } from '../session-artifact-paths.ts'; -import { mkdtempForTestSync } from '@agent-device/test-kit/tmp-dir'; - -test('resolveSessionDir keeps every session dir beneath the sessions dir', () => { - const sessionsDir = path.join( - mkdtempForTestSync('agent-device-tests'), - 'agent-device-tests', - 'sessions', - ); - assert.equal(resolveSessionDir(sessionsDir, 'a/b:c d'), path.join(sessionsDir, 'a_b_c_d')); - // `.` and `..` survive `safeSessionName` unchanged, so without an explicit - // refusal `path.join` resolves them to the sessions dir itself and its parent - // (the daemon state dir): a remote caller's `--session ..` would then land - // app.log / runner.log / requests/*.ndjson outside the sessions tree. - for (const name of ['.', '..', '']) { - assert.throws( - () => resolveSessionDir(sessionsDir, name), - (error: unknown) => - error instanceof AppError && - error.code === 'INVALID_ARGS' && - /session name/i.test(error.message), - `expected resolveSessionDir(${JSON.stringify(name)}) to reject`, - ); - } -}); diff --git a/packages/daemon/src/core/managed-session-artifact-path.ts b/packages/daemon/src/core/managed-session-artifact-path.ts deleted file mode 100644 index d58b1daed3..0000000000 --- a/packages/daemon/src/core/managed-session-artifact-path.ts +++ /dev/null @@ -1,29 +0,0 @@ -import fs from 'node:fs'; -import path from 'node:path'; - -type ManagedSessionArtifactPath = Readonly<{ - sessionsDir: string; - pathname: string; - basename: string; - label: string; -}>; - -export function requireManagedSessionArtifactPath(input: ManagedSessionArtifactPath): string { - const root = path.resolve(input.sessionsDir); - const resolved = path.resolve(input.pathname); - if (path.basename(resolved) !== input.basename) { - throw new Error(`${input.label} is outside the daemon-owned sessions directory`); - } - if (fs.existsSync(root) && fs.existsSync(path.dirname(resolved))) { - const realRoot = fs.realpathSync.native(root); - const realParent = fs.realpathSync.native(path.dirname(resolved)); - if (realParent !== realRoot && !realParent.startsWith(`${realRoot}${path.sep}`)) { - throw new Error(`${input.label} resolves outside the daemon-owned sessions directory`); - } - return path.join(realParent, input.basename); - } - if (resolved !== root && !resolved.startsWith(`${root}${path.sep}`)) { - throw new Error(`${input.label} is outside the daemon-owned sessions directory`); - } - return resolved; -} diff --git a/packages/daemon/src/core/session-store.ts b/packages/daemon/src/core/session-store.ts index 62a4cba8bf..d8e752acde 100644 --- a/packages/daemon/src/core/session-store.ts +++ b/packages/daemon/src/core/session-store.ts @@ -1,3 +1,8 @@ +import { + resolveSessionDir, + resolveSessionAppLogPath, + resolveSessionAppLogPidPath, +} from '@agent-device/daemon-contracts/session-artifact-paths'; import path from 'node:path'; import fs from 'node:fs'; import { AppError } from '@agent-device/kernel/errors'; @@ -5,11 +10,6 @@ import { emitDiagnostic } from '@agent-device/host-kit/diagnostics'; import type { SessionRef, SessionRuntimeHints, SessionState } from './session-state.ts'; import { recordActionEntry, type RecordActionEntry } from './session-action-recorder.ts'; import { isSafeSessionSegment, safeSessionName } from '@agent-device/host-kit/session-paths'; -import { - resolveSessionDir, - resolveSessionAppLogPath, - resolveSessionAppLogPidPath, -} from './session-artifact-paths.ts'; import { readRepairTombstoneFile, clearRepairTombstoneFile, diff --git a/packages/daemon/src/resources/__tests__/app-log-admission-ledger.test.ts b/packages/daemon/src/resources/__tests__/app-log-admission-ledger.test.ts index 4ee855cb82..42a79d89b1 100644 --- a/packages/daemon/src/resources/__tests__/app-log-admission-ledger.test.ts +++ b/packages/daemon/src/resources/__tests__/app-log-admission-ledger.test.ts @@ -1,8 +1,8 @@ +import { appLogResourceStore } from '@agent-device/capture-kit/app-log-durable-resource'; import { expect, test } from 'vitest'; import { deviceIdentity, type DeviceInfo } from '@agent-device/kernel/device'; import { createAppLogAdmissionLedger } from '../app-log-admission-ledger.ts'; import { createNextAppLogFence } from '../app-log-start-preflight.ts'; -import { appLogResourceStore } from '../app-log-resource-store.ts'; import { makeSessionStore } from '../../__tests__/support/store-factory.ts'; const DEVICE: DeviceInfo = { diff --git a/packages/daemon/src/resources/__tests__/app-log-files.test.ts b/packages/daemon/src/resources/__tests__/app-log-files.test.ts index b8ddf0be25..04903865af 100644 --- a/packages/daemon/src/resources/__tests__/app-log-files.test.ts +++ b/packages/daemon/src/resources/__tests__/app-log-files.test.ts @@ -1,9 +1,9 @@ +import { ensureAppLogPath, rotateAppLogIfNeeded } from '@agent-device/capture-kit/app-log-files'; import fs from 'node:fs'; import path from 'node:path'; import { expect, test } from 'vitest'; import { assertThrowsAppError } from '@agent-device/test-kit/app-error'; import { mkdtempForTestSync } from '@agent-device/test-kit/tmp-dir'; -import { ensureAppLogPath, rotateAppLogIfNeeded } from '../app-log-files.ts'; // Pinned as a literal on purpose — see the note in packages/host-kit/src/internal/verified-file.test.ts. const NOT_REGULAR_FILE_HINT = diff --git a/packages/daemon/src/resources/__tests__/app-log-resource-recovery.test.ts b/packages/daemon/src/resources/__tests__/app-log-resource-recovery.test.ts index bb2c6fd6cb..ab3632bfbd 100644 --- a/packages/daemon/src/resources/__tests__/app-log-resource-recovery.test.ts +++ b/packages/daemon/src/resources/__tests__/app-log-resource-recovery.test.ts @@ -1,3 +1,5 @@ +import { appLogResourceStore } from '@agent-device/capture-kit/app-log-durable-resource'; +import { recoverAppLogResourcesAfterDaemonLock } from '@agent-device/capture-kit/app-log-resource-recovery'; import fs from 'node:fs'; import path from 'node:path'; import { expect, test, vi } from 'vitest'; @@ -13,8 +15,6 @@ import { createDurableResourceEnvelope } from '@agent-device/capture-kit'; import { createTestAppLogLiveHandle } from '@agent-device/test-kit/app-log-live-handle'; import { unavailableDeploymentSnapshotAndShutdownOperationFacts } from '@agent-device/test-kit/runtime-operation-facts'; import { mkdtempForTestSync } from '@agent-device/test-kit/tmp-dir'; -import { recoverAppLogResourcesAfterDaemonLock } from '../app-log-resource-recovery.ts'; -import { appLogResourceStore } from '../app-log-resource-store.ts'; const scope = { signal: new AbortController().signal, diff --git a/packages/daemon/src/resources/__tests__/app-log-session-resource.test.ts b/packages/daemon/src/resources/__tests__/app-log-session-resource.test.ts index 895a6510f0..40edca81f0 100644 --- a/packages/daemon/src/resources/__tests__/app-log-session-resource.test.ts +++ b/packages/daemon/src/resources/__tests__/app-log-session-resource.test.ts @@ -1,3 +1,4 @@ +import { appLogResourceStore } from '@agent-device/capture-kit/app-log-durable-resource'; import fs from 'node:fs'; import path from 'node:path'; import { expect, test, vi } from 'vitest'; @@ -17,7 +18,6 @@ import { makeSessionStore } from '../../__tests__/support/store-factory.ts'; import { createAppLogAdmissionLedger } from '../app-log-admission-ledger.ts'; import { adoptStartedSessionAppLog, finishSessionAppLog } from '../app-log-session-resource.ts'; import { createNextAppLogFence } from '../app-log-start-preflight.ts'; -import { appLogResourceStore } from '../app-log-resource-store.ts'; import type { SessionState } from '../../core/session-state.ts'; import { stopSessionAppLog } from '../../sessions/session-teardown.ts'; diff --git a/packages/daemon/src/resources/__tests__/app-log-start-preflight.test.ts b/packages/daemon/src/resources/__tests__/app-log-start-preflight.test.ts index 0dcb81df42..53c4119413 100644 --- a/packages/daemon/src/resources/__tests__/app-log-start-preflight.test.ts +++ b/packages/daemon/src/resources/__tests__/app-log-start-preflight.test.ts @@ -1,3 +1,4 @@ +import { appLogResourceStore } from '@agent-device/capture-kit/app-log-durable-resource'; import fs from 'node:fs'; import path from 'node:path'; import { expect, test } from 'vitest'; @@ -7,7 +8,6 @@ import { deviceIdentity, type DeviceInfo } from '@agent-device/kernel/device'; import { makeSessionStore } from '../../__tests__/support/store-factory.ts'; import { createAppLogAdmissionLedger } from '../app-log-admission-ledger.ts'; import { createNextAppLogFence } from '../app-log-start-preflight.ts'; -import { appLogResourceStore } from '../app-log-resource-store.ts'; const device: DeviceInfo = { platform: 'android', diff --git a/packages/daemon/src/resources/__tests__/filesystem-boundary-faults.test.ts b/packages/daemon/src/resources/__tests__/filesystem-boundary-faults.test.ts index 658463e17c..b7d3db2175 100644 --- a/packages/daemon/src/resources/__tests__/filesystem-boundary-faults.test.ts +++ b/packages/daemon/src/resources/__tests__/filesystem-boundary-faults.test.ts @@ -6,8 +6,10 @@ import { afterEach, vi } from 'vitest'; import { localRuntimeOwner } from '@agent-device/contracts/platform-runtime'; import { createDurableResourceEnvelope } from '@agent-device/capture-kit'; import type { DeviceInfo } from '@agent-device/kernel/device'; -import { acquireDeviceClaim } from '../device/device-claims.ts'; -import { canonicalLocalDeviceKey } from '../device/device-claim-paths.ts'; +import { + acquireDeviceClaim, + canonicalLocalDeviceKey, +} from '@agent-device/daemon-contracts/device-claims'; import { createDurableCaptureResourceStore } from '@agent-device/capture-kit/durable-capture'; import { tryAcquireDaemonRegistration } from '@agent-device/daemon-contracts/daemon-registration-owner'; import { resolveDaemonPaths } from '@agent-device/daemon-contracts/daemon-resolution'; diff --git a/packages/daemon/src/resources/app-log-session-resource.ts b/packages/daemon/src/resources/app-log-session-resource.ts index c0d44354f5..e4d9264962 100644 --- a/packages/daemon/src/resources/app-log-session-resource.ts +++ b/packages/daemon/src/resources/app-log-session-resource.ts @@ -1,3 +1,4 @@ +import { appLogDurableResource } from '@agent-device/capture-kit/app-log-durable-resource'; import type { LogBackend } from '@agent-device/contracts/observability'; import type { AppLogCompletion, AppLogLiveHandle } from '@agent-device/contracts/app-log-runtime'; import type { DurableResourceEnvelope } from '@agent-device/contracts/durable-resource-envelope'; @@ -9,11 +10,7 @@ import type { import type { DeviceInfo } from '@agent-device/kernel/device'; import { AppError, normalizeError } from '@agent-device/kernel/errors'; import type { AppLogAdmissionLedger } from './app-log-admission-ledger.ts'; -import { - createDurableCaptureResource, - type DurableCaptureFinishIntent, -} from '@agent-device/capture-kit/durable-capture-resource'; -import { appLogResourceStore } from './app-log-resource-store.ts'; +import type { DurableCaptureFinishIntent } from '@agent-device/capture-kit/durable-capture-resource'; import type { SessionStore } from '../core/session-store.ts'; import type { SessionRef, SessionState } from '../core/session-state.ts'; import { clearAppLogFiles } from './app-log.ts'; @@ -29,27 +26,6 @@ export type AppLogSessionSnapshot = Readonly<{ hint?: string; }>; -export const appLogDurableResource = createDurableCaptureResource< - 'app-log', - AppLogLiveHandle, - AppLogCompletion ->({ - resourceKind: 'app-log', - displayName: 'app-log', - store: appLogResourceStore, - completionMetadata: (completion) => ({ - backend: completion.backend, - outputPath: completion.outputPath, - completedAt: completion.completedAt, - }), - failedFinishPolicy: 'dispose-on-failed-finish', - messages: { - noActive: 'no app log stream active', - cleanupPendingHint: - 'Keep app-log.resource.json and retry cleanup through its exact runtime owner.', - }, -}); - export function inspectSessionAppLog(session: SessionState): AppLogSessionSnapshot { if (session.appLog) { const snapshot = session.appLog.handle.inspect(); diff --git a/packages/daemon/src/resources/app-log-start-preflight.ts b/packages/daemon/src/resources/app-log-start-preflight.ts index ab6a5ee339..065ff3c110 100644 --- a/packages/daemon/src/resources/app-log-start-preflight.ts +++ b/packages/daemon/src/resources/app-log-start-preflight.ts @@ -1,7 +1,7 @@ +import { appLogDurableResource } from '@agent-device/capture-kit/app-log-durable-resource'; import type { ResourceOwnershipFence } from '@agent-device/contracts/platform-runtime'; import type { DeviceInfo } from '@agent-device/kernel/device'; import type { AppLogAdmissionLedger } from './app-log-admission-ledger.ts'; -import { appLogDurableResource } from './app-log-session-resource.ts'; export function createNextAppLogFence(params: { ledger: AppLogAdmissionLedger; diff --git a/packages/daemon/src/resources/app-log.ts b/packages/daemon/src/resources/app-log.ts index 52867428f4..31ef5a3fbf 100644 --- a/packages/daemon/src/resources/app-log.ts +++ b/packages/daemon/src/resources/app-log.ts @@ -1,3 +1,4 @@ +import { ensureAppLogPath } from '@agent-device/capture-kit/app-log-files'; import fs from 'node:fs'; import path from 'node:path'; import { @@ -5,7 +6,6 @@ import { openVerifiedFileForRead, openVerifiedFileForTruncate, } from '@agent-device/host-kit/file'; -import { ensureAppLogPath } from './app-log-files.ts'; export function getAppLogPathMetadata(outPath: string): { exists: boolean; diff --git a/packages/daemon/src/resources/device/__tests__/device-claim-admission.test.ts b/packages/daemon/src/resources/device/__tests__/device-claim-admission.test.ts index 400e2e5180..d832c4a792 100644 --- a/packages/daemon/src/resources/device/__tests__/device-claim-admission.test.ts +++ b/packages/daemon/src/resources/device/__tests__/device-claim-admission.test.ts @@ -1,3 +1,4 @@ +import { resolveDeviceClaimPath } from '@agent-device/daemon-contracts/device-claims-fixtures'; import { expect, test } from 'vitest'; import { daemonPlatformServicesFixture, @@ -13,9 +14,12 @@ import { retainOrphanedDeviceClaims, } from '../../../__tests__/support/device-claim-store.ts'; import { createDeviceClaimAdmission } from '../device-claim-admission.ts'; -import { abandonDeviceClaim, acquireDeviceClaim } from '../device-claims.ts'; -import { inspectDeviceClaims } from '../device-claim-inspection.ts'; -import { canonicalLocalDeviceKey, resolveDeviceClaimPath } from '../device-claim-paths.ts'; +import { + abandonDeviceClaim, + acquireDeviceClaim, + inspectDeviceClaims, + canonicalLocalDeviceKey, +} from '@agent-device/daemon-contracts/device-claims'; import { createRequestExecutionScope } from '../../../server/request-execution-scope.ts'; import { LeaseRegistry } from '../../lease-registry.ts'; import { SessionStore } from '../../../core/session-store.ts'; diff --git a/packages/daemon/src/resources/device/__tests__/device-claim-conflict.test.ts b/packages/daemon/src/resources/device/__tests__/device-claim-conflict.test.ts index fb8567ca6a..037847ae0c 100644 --- a/packages/daemon/src/resources/device/__tests__/device-claim-conflict.test.ts +++ b/packages/daemon/src/resources/device/__tests__/device-claim-conflict.test.ts @@ -5,7 +5,7 @@ import { buildDeviceClaimConflictError, buildDeviceClaimInspectionCommand, } from '../device-claim-conflict.ts'; -import type { InspectedDeviceClaim } from '../device-claim-inspection.ts'; +import type { InspectedDeviceClaim } from '@agent-device/daemon-contracts/device-claims'; const device: DeviceInfo = { platform: 'android', diff --git a/packages/daemon/src/resources/device/__tests__/device-claim-prune.test.ts b/packages/daemon/src/resources/device/__tests__/device-claim-prune.test.ts index 03c4dab794..afd2dff3ee 100644 --- a/packages/daemon/src/resources/device/__tests__/device-claim-prune.test.ts +++ b/packages/daemon/src/resources/device/__tests__/device-claim-prune.test.ts @@ -1,10 +1,10 @@ +import { resolveDeviceClaimPath } from '@agent-device/daemon-contracts/device-claims-fixtures'; import assert from 'node:assert/strict'; import fs from 'node:fs'; import path from 'node:path'; import { afterEach, test, vi } from 'vitest'; -import { reconcileOrphanedDeviceClaims } from '../device-claims.ts'; -import { resolveDeviceClaimPath } from '../device-claim-paths.ts'; +import { reconcileOrphanedDeviceClaims } from '@agent-device/daemon-contracts/device-claims'; import { acquireProcessLock } from '@agent-device/host-kit/file'; import { readCurrentOwnerIdentity } from '@agent-device/host-kit/process'; import { publishDaemonRegistration } from '@agent-device/daemon-contracts/daemon-registration-fixtures'; diff --git a/packages/daemon/src/resources/device/__tests__/device-claim-settlement.test.ts b/packages/daemon/src/resources/device/__tests__/device-claim-settlement.test.ts index e193127037..305f3b02f8 100644 --- a/packages/daemon/src/resources/device/__tests__/device-claim-settlement.test.ts +++ b/packages/daemon/src/resources/device/__tests__/device-claim-settlement.test.ts @@ -3,9 +3,11 @@ import crypto from 'node:crypto'; import fs from 'node:fs'; import path from 'node:path'; import { afterEach, test, vi } from 'vitest'; -import { acquireDeviceClaim as acquireProductionDeviceClaim } from '../device-claims.ts'; -import { canonicalLocalDeviceKey } from '../device-claim-paths.ts'; -import { inspectDeviceClaims } from '../device-claim-inspection.ts'; +import { + acquireDeviceClaim as acquireProductionDeviceClaim, + canonicalLocalDeviceKey, + inspectDeviceClaims, +} from '@agent-device/daemon-contracts/device-claims'; import type { DeviceBootObservationService } from '@agent-device/contracts/device-boot'; import type { DeviceInfo } from '@agent-device/kernel/device'; import { mkdtempForTestSync } from '@agent-device/test-kit/tmp-dir'; diff --git a/packages/daemon/src/resources/device/__tests__/device-claims.test.ts b/packages/daemon/src/resources/device/__tests__/device-claims.test.ts index 6f741c644c..83fa800619 100644 --- a/packages/daemon/src/resources/device/__tests__/device-claims.test.ts +++ b/packages/daemon/src/resources/device/__tests__/device-claims.test.ts @@ -9,9 +9,9 @@ import { clearDeviceClaim, processOwnsActiveDeviceClaim, releaseProvenStaleDeviceClaims, -} from '../device-claims.ts'; -import { canonicalLocalDeviceKey } from '../device-claim-paths.ts'; -import { inspectDeviceClaims } from '../device-claim-inspection.ts'; + canonicalLocalDeviceKey, + inspectDeviceClaims, +} from '@agent-device/daemon-contracts/device-claims'; import type { DeviceInfo } from '@agent-device/kernel/device'; import { mkdtempForTestSync } from '@agent-device/test-kit/tmp-dir'; import { publishDaemonRegistration } from '@agent-device/daemon-contracts/daemon-registration-fixtures'; diff --git a/packages/daemon/src/resources/device/device-claim-admission.ts b/packages/daemon/src/resources/device/device-claim-admission.ts index 189a503dd1..3dd98c5728 100644 --- a/packages/daemon/src/resources/device/device-claim-admission.ts +++ b/packages/daemon/src/resources/device/device-claim-admission.ts @@ -9,7 +9,7 @@ import { clearDeviceClaim, type DeviceClaimReconciler, type DeviceClaimSessionOwnership, -} from './device-claims.ts'; +} from '@agent-device/daemon-contracts/device-claims'; /** * The #1320 claim gate a request passes on its way from a device binding to diff --git a/packages/daemon/src/resources/device/device-claim-conflict.ts b/packages/daemon/src/resources/device/device-claim-conflict.ts index c4f0c6cb55..58cc4095fe 100644 --- a/packages/daemon/src/resources/device/device-claim-conflict.ts +++ b/packages/daemon/src/resources/device/device-claim-conflict.ts @@ -11,7 +11,7 @@ import { deviceClaimRequiresStaleInspection, type DeviceClaimClassification, type InspectedDeviceClaim, -} from './device-claim-inspection.ts'; +} from '@agent-device/daemon-contracts/device-claims'; import { errorResponse } from '@agent-device/kernel/contracts'; import type { DeviceClaimConflictReason } from '@agent-device/contracts/device'; diff --git a/packages/daemon/src/server/__tests__/atomic-publish-ownership.test.ts b/packages/daemon/src/server/__tests__/atomic-publish-ownership.test.ts index e186c4fc19..1258c6e0c0 100644 --- a/packages/daemon/src/server/__tests__/atomic-publish-ownership.test.ts +++ b/packages/daemon/src/server/__tests__/atomic-publish-ownership.test.ts @@ -4,7 +4,7 @@ import { test } from 'vitest'; const SIMPLE_PUBLISHERS = [ // device-claims.ts delegates every write to device-claim-store.ts's writeDeviceClaim. - new URL('../../resources/device/device-claim-store.ts', import.meta.url), + new URL('../../../../../packages/daemon-contracts/src/device-claim-store.ts', import.meta.url), new URL( '../../../../../packages/daemon-contracts/src/daemon-registration-owner.ts', import.meta.url, diff --git a/packages/daemon/src/server/__tests__/daemon-runtime-app-log.test.ts b/packages/daemon/src/server/__tests__/daemon-runtime-app-log.test.ts index b5aaa2a231..3abdd04830 100644 --- a/packages/daemon/src/server/__tests__/daemon-runtime-app-log.test.ts +++ b/packages/daemon/src/server/__tests__/daemon-runtime-app-log.test.ts @@ -1,3 +1,5 @@ +import { appLogResourceStore } from '@agent-device/capture-kit/app-log-durable-resource'; +import { recoverAppLogResourcesAfterDaemonLock } from '@agent-device/capture-kit/app-log-resource-recovery'; import { platformResourceCleanupFixture } from '../../__tests__/support/platform-resource-cleanup-fixture.ts'; import fs from 'node:fs'; import path from 'node:path'; @@ -8,8 +10,6 @@ import { createDurableResourceEnvelope } from '@agent-device/capture-kit'; import { createTestAppLogLiveHandle } from '@agent-device/test-kit/app-log-live-handle'; import { makeSessionStore } from '../../__tests__/support/store-factory.ts'; import { mkdtempForTestSync } from '@agent-device/test-kit/tmp-dir'; -import { recoverAppLogResourcesAfterDaemonLock } from '../../resources/app-log-resource-recovery.ts'; -import { appLogResourceStore } from '../../resources/app-log-resource-store.ts'; import { flushDaemonStartupDiagnostics, teardownDaemonSessionForShutdown, diff --git a/packages/daemon/src/server/__tests__/http-server-tenant-trust.test.ts b/packages/daemon/src/server/__tests__/http-server-tenant-trust.test.ts index 5518dd867a..4afa11b7f7 100644 --- a/packages/daemon/src/server/__tests__/http-server-tenant-trust.test.ts +++ b/packages/daemon/src/server/__tests__/http-server-tenant-trust.test.ts @@ -1,10 +1,10 @@ +import { resolveSessionRequestLogPath } from '@agent-device/daemon-contracts/session-artifact-paths'; import type { DaemonResponse } from '@agent-device/kernel/contracts'; import { test } from 'vitest'; import assert from 'node:assert/strict'; import fs from 'node:fs'; import path from 'node:path'; import { createDaemonHttpServer } from '../http/http-server.ts'; -import { resolveSessionRequestLogPath } from '../../core/session-artifact-paths.ts'; import { safeSessionName } from '@agent-device/host-kit/session-paths'; import { DAEMON_HTTP_TENANT_HEADER } from '@agent-device/contracts/daemon-http'; import type { DaemonRequest } from '../../core/daemon-request.ts'; diff --git a/packages/daemon/src/server/__tests__/request-diagnostics-http.test.ts b/packages/daemon/src/server/__tests__/request-diagnostics-http.test.ts index ce24826e5d..fc1cbe050a 100644 --- a/packages/daemon/src/server/__tests__/request-diagnostics-http.test.ts +++ b/packages/daemon/src/server/__tests__/request-diagnostics-http.test.ts @@ -1,3 +1,4 @@ +import { resolveSessionRequestLogPath } from '@agent-device/daemon-contracts/session-artifact-paths'; import type { DaemonResponse } from '@agent-device/kernel/contracts'; import { test } from 'vitest'; import assert from 'node:assert/strict'; @@ -5,7 +6,6 @@ import fs from 'node:fs'; import http from 'node:http'; import path from 'node:path'; import { createDaemonHttpServer } from '../http/http-server.ts'; -import { resolveSessionRequestLogPath } from '../../core/session-artifact-paths.ts'; import { safeSessionName } from '@agent-device/host-kit/session-paths'; import { LeaseRegistry } from '../../resources/lease-registry.ts'; import { diff --git a/packages/daemon/src/server/__tests__/request-execution-scope-lease-session-end.test.ts b/packages/daemon/src/server/__tests__/request-execution-scope-lease-session-end.test.ts index b8d365d545..91c95d3688 100644 --- a/packages/daemon/src/server/__tests__/request-execution-scope-lease-session-end.test.ts +++ b/packages/daemon/src/server/__tests__/request-execution-scope-lease-session-end.test.ts @@ -1,3 +1,4 @@ +import { resolveDeviceClaimPath } from '@agent-device/daemon-contracts/device-claims-fixtures'; import fs from 'node:fs'; import path from 'node:path'; import { expect, test, vi } from 'vitest'; @@ -8,8 +9,7 @@ import { isolatedDeviceClaimStores, retainOrphanedDeviceClaims, } from '../../__tests__/support/device-claim-store.ts'; -import { acquireDeviceClaim } from '../../resources/device/device-claims.ts'; -import { resolveDeviceClaimPath } from '../../resources/device/device-claim-paths.ts'; +import { acquireDeviceClaim } from '@agent-device/daemon-contracts/device-claims'; import { existingSessionExecutionLockKeys } from '../request-binding.ts'; import { withRequestExecutionLockKeys } from '../request-execution-locks.ts'; import type { SessionStore } from '../../core/session-store.ts'; diff --git a/packages/daemon/src/server/__tests__/request-execution-scope.test.ts b/packages/daemon/src/server/__tests__/request-execution-scope.test.ts index 4938824813..4d12e57193 100644 --- a/packages/daemon/src/server/__tests__/request-execution-scope.test.ts +++ b/packages/daemon/src/server/__tests__/request-execution-scope.test.ts @@ -1,3 +1,4 @@ +import { resolveSessionRequestLogPath } from '@agent-device/daemon-contracts/session-artifact-paths'; import { daemonPlatformServicesFixture, daemonClaimRecoveryGatewayFixture, @@ -18,7 +19,6 @@ import { createRequestExecutionScope, prepareLockedRequestScope, } from '../request-execution-scope.ts'; -import { resolveSessionRequestLogPath } from '../../core/session-artifact-paths.ts'; import { resolveSessionScope } from '../../sessions/session-routing.ts'; import type { DaemonRequest } from '../../core/daemon-request.ts'; import { mkdtempForTestSync } from '@agent-device/test-kit/tmp-dir'; diff --git a/packages/daemon/src/server/__tests__/request-router-open-claim.test.ts b/packages/daemon/src/server/__tests__/request-router-open-claim.test.ts index aa5b1fafed..7cf30ae176 100644 --- a/packages/daemon/src/server/__tests__/request-router-open-claim.test.ts +++ b/packages/daemon/src/server/__tests__/request-router-open-claim.test.ts @@ -57,7 +57,7 @@ import { awaitFixtureReadiness } from '../../__tests__/support/application-lifec import { ensureDeviceReady } from '../../core/device/device-ready.ts'; import type { DeviceInfo } from '@agent-device/kernel/device'; import { makeSessionStore } from '../../__tests__/support/store-factory.ts'; -import { inspectDeviceClaims } from '../../resources/device/device-claim-inspection.ts'; +import { inspectDeviceClaims } from '@agent-device/daemon-contracts/device-claims'; import { makeIosDevice, openRequest, storedClaimUpdatedAt } from './request-router-open-harness.ts'; import { mkdtempForTestSync } from '@agent-device/test-kit/tmp-dir'; diff --git a/packages/daemon/src/server/__tests__/request-router-open-harness.ts b/packages/daemon/src/server/__tests__/request-router-open-harness.ts index d92ecfb654..326650d55e 100644 --- a/packages/daemon/src/server/__tests__/request-router-open-harness.ts +++ b/packages/daemon/src/server/__tests__/request-router-open-harness.ts @@ -1,4 +1,4 @@ -import { inspectDeviceClaims } from '../../resources/device/device-claim-inspection.ts'; +import { inspectDeviceClaims } from '@agent-device/daemon-contracts/device-claims'; import type { DeviceInfo } from '@agent-device/kernel/device'; /** diff --git a/packages/daemon/src/server/__tests__/request-router-open.test.ts b/packages/daemon/src/server/__tests__/request-router-open.test.ts index 325a7cf5b8..93b728c47f 100644 --- a/packages/daemon/src/server/__tests__/request-router-open.test.ts +++ b/packages/daemon/src/server/__tests__/request-router-open.test.ts @@ -76,7 +76,7 @@ import { awaitFixtureReadiness } from '../../__tests__/support/application-lifec import type { DeviceInfo } from '@agent-device/kernel/device'; import { AppError } from '@agent-device/kernel/errors'; import { makeSessionStore } from '../../__tests__/support/store-factory.ts'; -import { inspectDeviceClaims } from '../../resources/device/device-claim-inspection.ts'; +import { inspectDeviceClaims } from '@agent-device/daemon-contracts/device-claims'; import { makeAndroidDevice, makeIosDevice, diff --git a/packages/daemon/src/server/__tests__/session-idle-expiry-harness.ts b/packages/daemon/src/server/__tests__/session-idle-expiry-harness.ts index ebfd2c6bba..94058f7e42 100644 --- a/packages/daemon/src/server/__tests__/session-idle-expiry-harness.ts +++ b/packages/daemon/src/server/__tests__/session-idle-expiry-harness.ts @@ -1,3 +1,4 @@ +import { resolveDeviceClaimPath } from '@agent-device/daemon-contracts/device-claims-fixtures'; import { assert } from 'vitest'; import fs from 'node:fs'; import path from 'node:path'; @@ -13,8 +14,7 @@ import { import { acquireDeviceClaim, type DeviceClaimSessionOwnership, -} from '../../resources/device/device-claims.ts'; -import { resolveDeviceClaimPath } from '../../resources/device/device-claim-paths.ts'; +} from '@agent-device/daemon-contracts/device-claims'; import { IOS_SIMULATOR } from '@agent-device/test-kit/device-fixtures'; import { mkdtempForTestSync } from '@agent-device/test-kit/tmp-dir'; diff --git a/packages/daemon/src/server/device/__tests__/device-claim-owner-recovery.test.ts b/packages/daemon/src/server/device/__tests__/device-claim-owner-recovery.test.ts index 3cd3f730cd..15dfe80663 100644 --- a/packages/daemon/src/server/device/__tests__/device-claim-owner-recovery.test.ts +++ b/packages/daemon/src/server/device/__tests__/device-claim-owner-recovery.test.ts @@ -1,3 +1,7 @@ +import { + createOwnerScopedDeviceClaimReconciler, + type OwnerScopedClaimRecoveryComposer, +} from '@agent-device/daemon-contracts/device-claim-recovery'; import assert from 'node:assert/strict'; import fs from 'node:fs'; import path from 'node:path'; @@ -5,11 +9,7 @@ import { test } from 'vitest'; import { mkdtempForTestSync } from '@agent-device/test-kit/tmp-dir'; import type { AppLogSessionArtifacts } from '@agent-device/contracts/app-log-runtime'; import type { OwnedProcessRecordWriter } from '@agent-device/contracts/platform-runtime-host'; -import type { DeviceClaim } from '../../../resources/device/device-claim-record.ts'; -import { - createOwnerScopedDeviceClaimReconciler, - type OwnerScopedClaimRecoveryComposer, -} from '../device-claim-owner-recovery.ts'; +import type { DeviceClaim } from '@agent-device/daemon-contracts/device-claims'; const scope = { signal: new AbortController().signal, diff --git a/packages/daemon/src/server/http/daemon-runtime.ts b/packages/daemon/src/server/http/daemon-runtime.ts index 28b2f0b7b8..c0052a548d 100644 --- a/packages/daemon/src/server/http/daemon-runtime.ts +++ b/packages/daemon/src/server/http/daemon-runtime.ts @@ -1,24 +1,35 @@ +import { recoverAppLogResourcesAfterDaemonLock } from '@agent-device/capture-kit/app-log-resource-recovery'; +import { resolveSessionRequestLogPath } from '@agent-device/daemon-contracts/session-artifact-paths'; +import { + createOwnerScopedDeviceClaimReconciler, + createRecoveryPlatformScope, + type ClaimRecoveryGatewayFactory, +} from '@agent-device/daemon-contracts/device-claim-recovery'; import crypto from 'node:crypto'; import { asAppError, AppError, normalizeError } from '@agent-device/kernel/errors'; import { SessionStore } from '../../core/session-store.ts'; -import { resolveSessionRequestLogPath } from '../../core/session-artifact-paths.ts'; import { resolveDaemonPaths, resolveDaemonServerMode, } from '@agent-device/daemon-contracts/daemon-resolution'; import { createDaemonHttpServer } from './http-server.ts'; import { trackDownloadableArtifact } from '../../resources/artifact-tracking.ts'; -import { installProviderDeviceAdmission } from '../../core/provider-device-admission.ts'; +import { + installProviderDeviceAdmission, + type ProviderDeviceAdmission, +} from '../../core/provider-device-admission.ts'; import { loadDaemonPolicy, type DaemonPolicy, } from '@agent-device/daemon-contracts/daemon-policy-file'; import { assertDaemonPolicyAllowsCapability } from '../../core/daemon-policy.ts'; -import { installInteractorResolution } from '../../core/interactor-resolution.ts'; +import { + installInteractorResolution, + type InteractorResolution, +} from '../../core/interactor-resolution.ts'; import { LeaseRegistry } from '../../resources/lease-registry.ts'; import { createExpiredProviderLeaseReleaser } from '../provider-lease-expiry.ts'; -import { createRequestHandler } from '../request-router.ts'; -import type { RequestRouterDeps } from '../request-router.ts'; +import { createRequestHandler, type RequestRouterDeps } from '../request-router.ts'; import { createLeaseSessionEnd, getLeaseRegistryExecutionLocks, @@ -41,8 +52,7 @@ import { processOwnsActiveDeviceClaim, reconcileOrphanedDeviceClaims, type DeviceClaimReconciler, -} from '../../resources/device/device-claims.ts'; -import { createOwnerScopedDeviceClaimReconciler } from '../device/device-claim-owner-recovery.ts'; +} from '@agent-device/daemon-contracts/device-claims'; import { createDaemonShutdownClaimLedger } from './daemon-shutdown-claims.ts'; import { createAudioProbeAdmissionLedger } from '@agent-device/capture-kit/audio-probe-admission-ledger'; import { createPerfCaptureAdmissionLedger } from '@agent-device/capture-kit/perf-capture-admission-ledger'; @@ -75,15 +85,10 @@ import { } from './transport.ts'; import { prewarmPngWorker, terminatePngWorker } from '@agent-device/capture-kit/png-worker-client'; import { openWebSessionNames } from '../../sessions/web-session-names.ts'; -import { recoverAppLogResourcesAfterDaemonLock } from '../../resources/app-log-resource-recovery.ts'; -import { createDaemonRecoveryPlatformScope } from '../platform-request-scope.ts'; import { createAppLogAdmissionLedger } from '../../resources/app-log-admission-ledger.ts'; import type { AppLogSessionArtifacts } from '@agent-device/contracts/app-log-runtime'; -import type { InteractorResolution } from '../../core/interactor-resolution.ts'; -import type { ProviderDeviceAdmission } from '../../core/provider-device-admission.ts'; import type { PlatformOwnerLifecycle } from '../../core/platform-owner-lifecycle.ts'; import type { PlatformResourceCleanup } from '../../core/platform-resource-cleanup.ts'; -import type { ClaimRecoveryGatewayFactory } from '../../resources/device/claim-recovery-gateway.ts'; import type { LeaseLifecycleProvider, ProviderDeviceRuntime, @@ -518,7 +523,7 @@ export async function startDaemonRuntime( finalizeApplicationLifecycle: async (sessionToFinalize, runtimeHints) => await finalizeDaemonSessionApplicationLifecycle({ gateway: deviceRuntimeGateway, - scope: createDaemonRecoveryPlatformScope(), + scope: createRecoveryPlatformScope(), session: sessionToFinalize, stateDir: baseDir, runtimeHints, @@ -556,7 +561,7 @@ export async function startDaemonRuntime( }); await finalizeDaemonSessionApplicationLifecycle({ gateway: deviceRuntimeGateway, - scope: createDaemonRecoveryPlatformScope(), + scope: createRecoveryPlatformScope(), session, stateDir: baseDir, runtimeHints, @@ -764,7 +769,7 @@ export async function startDaemonRuntime( await recoverAppLogResourcesAfterDaemonLock({ sessionsDir, gateway: deviceRuntimeGateway, - scope: createDaemonRecoveryPlatformScope(), + scope: createRecoveryPlatformScope(), onDiagnostic: (diagnostic) => startupDiagnostics.push(diagnostic), }); await reapOwnedProcessRecordsAtStartup(deps.ownedProcessRecords, { @@ -806,7 +811,7 @@ export async function startDaemonRuntime( await reconcileDeviceClaimsForDaemonStartup( logPath, createOwnerScopedDeviceClaimReconciler({ - scope: createDaemonRecoveryPlatformScope(), + scope: createRecoveryPlatformScope(), composeGateway: deps.claimRecoveryGateway, }), baseDir, diff --git a/packages/daemon/src/server/http/daemon-session-idle-expiry.test.ts b/packages/daemon/src/server/http/daemon-session-idle-expiry.test.ts index 703275160e..55d46c5f59 100644 --- a/packages/daemon/src/server/http/daemon-session-idle-expiry.test.ts +++ b/packages/daemon/src/server/http/daemon-session-idle-expiry.test.ts @@ -1,3 +1,4 @@ +import { resolveDeviceClaimPath } from '@agent-device/daemon-contracts/device-claims-fixtures'; import { test } from 'vitest'; import assert from 'node:assert/strict'; import fs from 'node:fs'; @@ -17,7 +18,6 @@ import { makeIosSession, makeRepairCompleteSession, } from '../../__tests__/support/session-factories.ts'; -import { resolveDeviceClaimPath } from '../../resources/device/device-claim-paths.ts'; /** * #2833: what one expiry COMMITS, once it has run. Whether a session past its window is the kind that diff --git a/packages/daemon/src/server/http/daemon-session-idle-expiry.ts b/packages/daemon/src/server/http/daemon-session-idle-expiry.ts index b2ba5c9071..cb67444652 100644 --- a/packages/daemon/src/server/http/daemon-session-idle-expiry.ts +++ b/packages/daemon/src/server/http/daemon-session-idle-expiry.ts @@ -2,7 +2,7 @@ import { emitDiagnostic } from '@agent-device/host-kit/diagnostics'; import { clearDeviceClaim, type DeviceClaimClearOutcome, -} from '../../resources/device/device-claims.ts'; +} from '@agent-device/daemon-contracts/device-claims'; import { existingSessionExecutionLockKeys } from '../request-binding.ts'; import { withRequestExecutionLockKeys } from '../request-execution-locks.ts'; import type { RequestExecutionLockKey } from '../request-binding.ts'; diff --git a/packages/daemon/src/server/http/daemon-shutdown-claims.test.ts b/packages/daemon/src/server/http/daemon-shutdown-claims.test.ts index 671cd43990..57bf1e2a9d 100644 --- a/packages/daemon/src/server/http/daemon-shutdown-claims.test.ts +++ b/packages/daemon/src/server/http/daemon-shutdown-claims.test.ts @@ -1,3 +1,4 @@ +import { resolveDeviceClaimPath } from '@agent-device/daemon-contracts/device-claims-fixtures'; import { afterEach, expect, test, vi } from 'vitest'; import { ANDROID_EMULATOR } from '@agent-device/test-kit/device-fixtures'; import { @@ -5,9 +6,10 @@ import { retainOrphanedDeviceClaims, } from '../../__tests__/support/device-claim-store.ts'; import fs from 'node:fs'; -import { acquireDeviceClaim } from '../../resources/device/device-claims.ts'; -import { resolveDeviceClaimPath } from '../../resources/device/device-claim-paths.ts'; -import { inspectDeviceClaims } from '../../resources/device/device-claim-inspection.ts'; +import { + acquireDeviceClaim, + inspectDeviceClaims, +} from '@agent-device/daemon-contracts/device-claims'; import { createDaemonShutdownClaimLedger } from './daemon-shutdown-claims.ts'; import type { SessionState } from '../../core/session-state.ts'; diff --git a/packages/daemon/src/server/http/daemon-shutdown-claims.ts b/packages/daemon/src/server/http/daemon-shutdown-claims.ts index a2b0c3bf87..ad3c74cc46 100644 --- a/packages/daemon/src/server/http/daemon-shutdown-claims.ts +++ b/packages/daemon/src/server/http/daemon-shutdown-claims.ts @@ -3,7 +3,7 @@ import { emitDiagnostic } from '@agent-device/host-kit/diagnostics'; import { clearDeviceClaim, type DeviceClaimClearOutcome, -} from '../../resources/device/device-claims.ts'; +} from '@agent-device/daemon-contracts/device-claims'; import type { DeviceClaimRecord } from '@agent-device/daemon-contracts/daemon-shutdown-report'; import type { SessionState } from '../../core/session-state.ts'; diff --git a/packages/daemon/src/server/platform-request-scope.ts b/packages/daemon/src/server/platform-request-scope.ts index 247634ca5a..e928176bbe 100644 --- a/packages/daemon/src/server/platform-request-scope.ts +++ b/packages/daemon/src/server/platform-request-scope.ts @@ -26,16 +26,3 @@ export function createPlatformRequestScope(req: DaemonRequest): PlatformRequestS }), }); } - -/** Process-owned scope for post-lock durable-resource recovery before request admission. */ -export function createDaemonRecoveryPlatformScope(): PlatformRequestScope { - return Object.freeze({ - signal: processLifetimeSignal, - diagnostics: Object.freeze({ - emit: (event: PlatformDiagnosticEvent) => emitDiagnostic(event), - }), - progress: Object.freeze({ - report: () => {}, - }), - }); -} diff --git a/packages/daemon/src/server/request-execution-scope.ts b/packages/daemon/src/server/request-execution-scope.ts index e789c5d867..21eef92c7a 100644 --- a/packages/daemon/src/server/request-execution-scope.ts +++ b/packages/daemon/src/server/request-execution-scope.ts @@ -1,3 +1,11 @@ +import { + resolveSessionRequestLog, + resolveSessionRunnerLogPath, +} from '@agent-device/daemon-contracts/session-artifact-paths'; +import { + createOwnerScopedDeviceClaimReconciler, + type ClaimRecoveryGatewayFactory, +} from '@agent-device/daemon-contracts/device-claim-recovery'; import type { DaemonResponse } from '@agent-device/kernel/contracts'; import type { CommandFlags } from '@agent-device/contracts/command'; import type { ProviderAppCatalog } from '@agent-device/contracts/device'; @@ -57,10 +65,6 @@ import { } from '@agent-device/session-journal/session-event-log'; import type { LeaseRegistry } from '../resources/lease-registry.ts'; import { type SessionStore } from '../core/session-store.ts'; -import { - resolveSessionRequestLog, - resolveSessionRunnerLogPath, -} from '../core/session-artifact-paths.ts'; import type { DaemonRequest } from '../core/daemon-request.ts'; import type { SessionRef, SessionState } from '../core/session-state.ts'; import { teardownSessionResources } from '../sessions/session-teardown.ts'; @@ -80,8 +84,6 @@ import { createDeviceClaimAdmission, type DeviceClaimAdmission, } from '../resources/device/device-claim-admission.ts'; -import { createOwnerScopedDeviceClaimReconciler } from './device/device-claim-owner-recovery.ts'; -import type { ClaimRecoveryGatewayFactory } from '../resources/device/claim-recovery-gateway.ts'; import { applyCommandDefaults, resolveCommandDeviceClaimPolicy, diff --git a/packages/daemon/src/server/request-router.ts b/packages/daemon/src/server/request-router.ts index 5dcd2c78d5..6a5ba7037c 100644 --- a/packages/daemon/src/server/request-router.ts +++ b/packages/daemon/src/server/request-router.ts @@ -1,3 +1,7 @@ +import { + createOwnerScopedDeviceClaimReconciler, + type ClaimRecoveryGatewayFactory, +} from '@agent-device/daemon-contracts/device-claim-recovery'; import { withResolveTargetDeviceCacheScope } from '@agent-device/device-selection/dispatch-resolve'; import { withDeviceInventoryContext } from '@agent-device/device-selection/device-inventory-context'; import type { LeaseLifecycleProvider, ProviderAppCatalog } from '@agent-device/contracts/device'; @@ -59,8 +63,6 @@ import { } from '@agent-device/ad-script'; import { createPlatformRequestScope } from './platform-request-scope.ts'; import { parameterizeSensitiveString } from '@agent-device/selectors/parameterized-recorded-fill'; -import { createOwnerScopedDeviceClaimReconciler } from './device/device-claim-owner-recovery.ts'; -import type { ClaimRecoveryGatewayFactory } from '../resources/device/claim-recovery-gateway.ts'; import { isConfinedToAppLease, scopeRequestSession } from '../sessions/request-admission.ts'; import { redactMacOsAppLeaseResponse } from '../resources/macos-app-lease.ts'; import { resolveEffectiveSessionName } from '../sessions/session-routing.ts'; diff --git a/packages/daemon/src/server/session-idle-expiry.ts b/packages/daemon/src/server/session-idle-expiry.ts index fbce77fd99..903472e1da 100644 --- a/packages/daemon/src/server/session-idle-expiry.ts +++ b/packages/daemon/src/server/session-idle-expiry.ts @@ -1,5 +1,5 @@ import { AppError } from '@agent-device/kernel/errors'; -import type { DeviceClaimClearOutcome } from '../resources/device/device-claims.ts'; +import type { DeviceClaimClearOutcome } from '@agent-device/daemon-contracts/device-claims'; import type { SessionState } from '../core/session-state.ts'; import type { IdleSessionTombstone } from '../core/session-idle-tombstone.ts'; diff --git a/packages/daemon/src/sessions/device/__tests__/device-claim-reconciliation.test.ts b/packages/daemon/src/sessions/device/__tests__/device-claim-reconciliation.test.ts index bc0a19e0c9..01b34c9492 100644 --- a/packages/daemon/src/sessions/device/__tests__/device-claim-reconciliation.test.ts +++ b/packages/daemon/src/sessions/device/__tests__/device-claim-reconciliation.test.ts @@ -1,3 +1,5 @@ +import { createDeviceClaimReconciler } from '@agent-device/daemon-contracts/device-claims-fixtures'; +import { appLogResourceStore } from '@agent-device/capture-kit/app-log-durable-resource'; import fs from 'node:fs'; import path from 'node:path'; import { expect, test, vi } from 'vitest'; @@ -11,9 +13,7 @@ import type { PlatformRuntimeOperations } from '@agent-device/contracts/platform import { createTestAppLogLiveHandle } from '@agent-device/test-kit/app-log-live-handle'; import { unavailableDeploymentSnapshotAndShutdownOperationFacts } from '@agent-device/test-kit/runtime-operation-facts'; import { mkdtempForTestSync } from '@agent-device/test-kit/tmp-dir'; -import { createDeviceClaimReconciler } from '../device-claim-reconciliation.ts'; -import type { DeviceClaim } from '../../../resources/device/device-claim-record.ts'; -import { appLogResourceStore } from '../../../resources/app-log-resource-store.ts'; +import type { DeviceClaim } from '@agent-device/daemon-contracts/device-claims'; const scope = { signal: new AbortController().signal, diff --git a/packages/daemon/src/sessions/handlers/__tests__/session-boot-shutdown-device-claims.test.ts b/packages/daemon/src/sessions/handlers/__tests__/session-boot-shutdown-device-claims.test.ts index 318cdb8d9a..ef42a7d362 100644 --- a/packages/daemon/src/sessions/handlers/__tests__/session-boot-shutdown-device-claims.test.ts +++ b/packages/daemon/src/sessions/handlers/__tests__/session-boot-shutdown-device-claims.test.ts @@ -20,8 +20,10 @@ import { } from '../../../__tests__/support/device-claim-store.ts'; import { SessionStore } from '../../../core/session-store.ts'; import { LeaseRegistry } from '../../../resources/lease-registry.ts'; -import { acquireDeviceClaim } from '../../../resources/device/device-claims.ts'; -import { inspectDeviceClaims } from '../../../resources/device/device-claim-inspection.ts'; +import { + acquireDeviceClaim, + inspectDeviceClaims, +} from '@agent-device/daemon-contracts/device-claims'; import { createRequestExecutionScope } from '../../../server/request-execution-scope.ts'; import { handleBootCommand } from '../session-boot.ts'; import { handleShutdownCommand } from '../session-shutdown.ts'; diff --git a/packages/daemon/src/sessions/handlers/__tests__/session-device-claims.test.ts b/packages/daemon/src/sessions/handlers/__tests__/session-device-claims.test.ts index 017debb24f..15217b5043 100644 --- a/packages/daemon/src/sessions/handlers/__tests__/session-device-claims.test.ts +++ b/packages/daemon/src/sessions/handlers/__tests__/session-device-claims.test.ts @@ -46,8 +46,10 @@ import { prepareApplicationOpenFixture, } from '../../../__tests__/support/application-lifecycle-runtime-fixture.ts'; import { clearRequestCanceled, markRequestCanceled } from '@agent-device/host-kit/request'; -import { acquireDeviceClaim as acquireProductionDeviceClaim } from '../../../resources/device/device-claims.ts'; -import { inspectDeviceClaims } from '../../../resources/device/device-claim-inspection.ts'; +import { + acquireDeviceClaim as acquireProductionDeviceClaim, + inspectDeviceClaims, +} from '@agent-device/daemon-contracts/device-claims'; import { LeaseRegistry } from '../../../resources/lease-registry.ts'; import { SessionStore } from '../../../core/session-store.ts'; import { diff --git a/packages/daemon/src/sessions/handlers/session-command-input.ts b/packages/daemon/src/sessions/handlers/session-command-input.ts index 87b0f088f2..3e2e61aca5 100644 --- a/packages/daemon/src/sessions/handlers/session-command-input.ts +++ b/packages/daemon/src/sessions/handlers/session-command-input.ts @@ -10,7 +10,7 @@ import type { AppLogAdmissionLedger } from '../../resources/app-log-admission-le import { type AudioProbeAdmissionLedger } from '@agent-device/capture-kit/audio-probe-admission-ledger'; import { type PerfCaptureAdmissionLedger } from '@agent-device/capture-kit/perf-capture-admission-ledger'; import { type ScreenRecordingAdmissionLedger } from '@agent-device/capture-kit/screen-recording-admission-ledger'; -import type { DeviceClaimReconciler } from '../../resources/device/device-claims.ts'; +import type { DeviceClaimReconciler } from '@agent-device/daemon-contracts/device-claims'; import type { LeaseRegistry } from '../../resources/lease-registry.ts'; import type { BindDeviceRuntime, diff --git a/packages/daemon/src/sessions/lease-lifecycle.ts b/packages/daemon/src/sessions/lease-lifecycle.ts index 91d734e336..c9c45757a4 100644 --- a/packages/daemon/src/sessions/lease-lifecycle.ts +++ b/packages/daemon/src/sessions/lease-lifecycle.ts @@ -1,6 +1,6 @@ import { emitDiagnostic } from '@agent-device/host-kit/diagnostics'; import { leaseScopeToReleaseRequest } from '@agent-device/contracts/lease-scope'; -import { clearDeviceClaim } from '../resources/device/device-claims.ts'; +import { clearDeviceClaim } from '@agent-device/daemon-contracts/device-claims'; import type { LeaseRegistry } from '../resources/lease-registry.ts'; import type { DeviceLease, diff --git a/packages/daemon/src/sessions/request-handler-chain.ts b/packages/daemon/src/sessions/request-handler-chain.ts index 61fb8e2a71..8239803da6 100644 --- a/packages/daemon/src/sessions/request-handler-chain.ts +++ b/packages/daemon/src/sessions/request-handler-chain.ts @@ -16,7 +16,7 @@ import type { BindExactDeviceRuntime, InspectDeviceRuntimeFacts, } from '../core/request-runtime-binding.ts'; -import type { DeviceClaimReconciler } from '../resources/device/device-claims.ts'; +import type { DeviceClaimReconciler } from '@agent-device/daemon-contracts/device-claims'; import type { AppLogAdmissionLedger } from '../resources/app-log-admission-ledger.ts'; import { type AudioProbeAdmissionLedger } from '@agent-device/capture-kit/audio-probe-admission-ledger'; import { type PerfCaptureAdmissionLedger } from '@agent-device/capture-kit/perf-capture-admission-ledger'; diff --git a/packages/daemon/src/sessions/session-lifecycle/internal/__tests__/session-close-cwd-scoped-resources.test.ts b/packages/daemon/src/sessions/session-lifecycle/internal/__tests__/session-close-cwd-scoped-resources.test.ts index e112931867..3d6b95b9ba 100644 --- a/packages/daemon/src/sessions/session-lifecycle/internal/__tests__/session-close-cwd-scoped-resources.test.ts +++ b/packages/daemon/src/sessions/session-lifecycle/internal/__tests__/session-close-cwd-scoped-resources.test.ts @@ -1,3 +1,4 @@ +import { appLogResourceStore } from '@agent-device/capture-kit/app-log-durable-resource'; import { daemonPlatformServicesFixture } from '../../../../__tests__/support/platform-services-fixture.ts'; import { beforeEach, expect, test, vi } from 'vitest'; import type { AppLogLiveHandle } from '@agent-device/contracts/app-log-runtime'; @@ -5,7 +6,6 @@ import type { AudioProbeLiveHandle } from '@agent-device/contracts/audio-probe-r import type { PerfNativeCaptureLiveHandle } from '@agent-device/contracts/perf-runtime'; import type { DurableResourceEnvelope } from '@agent-device/contracts/durable-resource-envelope'; import { createDurableResourceEnvelope } from '@agent-device/capture-kit'; -import { appLogResourceStore } from '../../../../resources/app-log-resource-store.ts'; import { audioProbeResourceStore } from '@agent-device/capture-kit/audio-probe-resource-store'; import { perfCaptureResourceStore } from '@agent-device/capture-kit/perf-capture-resource-store'; import { screenRecordingResourceStore } from '@agent-device/capture-kit/screen-recording-resource-store'; diff --git a/packages/daemon/src/sessions/session-lifecycle/internal/__tests__/session-close-shutdown.fixtures.ts b/packages/daemon/src/sessions/session-lifecycle/internal/__tests__/session-close-shutdown.fixtures.ts index db7a4f83be..f470a015b1 100644 --- a/packages/daemon/src/sessions/session-lifecycle/internal/__tests__/session-close-shutdown.fixtures.ts +++ b/packages/daemon/src/sessions/session-lifecycle/internal/__tests__/session-close-shutdown.fixtures.ts @@ -66,8 +66,10 @@ import { } from '@agent-device/platform-apple/runner/operations'; import { cleanupAppleXctracePerfCapture } from '@agent-device/platform-apple/perf'; import { WEB_DESKTOP_DEVICE } from '@agent-device/test-kit/device-fixtures'; -import { acquireDeviceClaim } from '../../../../resources/device/device-claims.ts'; -import { inspectDeviceClaims } from '../../../../resources/device/device-claim-inspection.ts'; +import { + acquireDeviceClaim, + inspectDeviceClaims, +} from '@agent-device/daemon-contracts/device-claims'; import { type DeviceBinding, diff --git a/packages/daemon/src/sessions/session-lifecycle/internal/inventory.ts b/packages/daemon/src/sessions/session-lifecycle/internal/inventory.ts index bcb985a1bd..d83e36c570 100644 --- a/packages/daemon/src/sessions/session-lifecycle/internal/inventory.ts +++ b/packages/daemon/src/sessions/session-lifecycle/internal/inventory.ts @@ -1,3 +1,4 @@ +import { resolveSessionRunnerLogPath } from '@agent-device/daemon-contracts/session-artifact-paths'; import type { DaemonResponse } from '@agent-device/kernel/contracts'; import type { DaemonPlatformServices } from '../../../core/platform-services.ts'; import { @@ -5,7 +6,11 @@ import { listRuntimeFactCommands, } from '@agent-device/command-registry/registry'; import { listDeviceInventory } from '@agent-device/device-selection/device-inventory-context'; -import { assertResolvedAppsFilter } from '@agent-device/contracts/device'; +import { + assertResolvedAppsFilter, + type ProviderAppCatalog, + type ProviderAppCatalogQuery, +} from '@agent-device/contracts/device'; import { asAppError } from '@agent-device/kernel/errors'; import { requireRuntimeBinding, @@ -25,13 +30,12 @@ import { resolveAndroidSerialAllowlist } from '@agent-device/kernel/device-isola import { deviceClaimOwnerCannotRelease, inspectDeviceClaims, -} from '../../../resources/device/device-claim-inspection.ts'; -import { canonicalLocalDeviceKey } from '../../../resources/device/device-claim-paths.ts'; -import { deviceClaimIdentity } from '../../../resources/device/device-claims.ts'; + canonicalLocalDeviceKey, + deviceClaimIdentity, +} from '@agent-device/daemon-contracts/device-claims'; import type { DaemonRequest } from '../../../core/daemon-request.ts'; import type { SessionRef } from '../../../core/session-state.ts'; import { SessionStore } from '../../../core/session-store.ts'; -import { resolveSessionRunnerLogPath } from '../../../core/session-artifact-paths.ts'; import { requireSessionOrExplicitSelector, resolveCommandDevice, @@ -52,7 +56,6 @@ import type { BindDeviceRuntime, InspectDeviceRuntimeFacts, } from '../../../core/request-runtime-binding.ts'; -import type { ProviderAppCatalog, ProviderAppCatalogQuery } from '@agent-device/contracts/device'; import { resolveLeaseScope } from '../../../core/lease-context.ts'; import { getRequestSignal } from '@agent-device/host-kit/request'; diff --git a/packages/daemon/src/sessions/session-lifecycle/internal/session-close.ts b/packages/daemon/src/sessions/session-lifecycle/internal/session-close.ts index 286f46fc03..3286112e38 100644 --- a/packages/daemon/src/sessions/session-lifecycle/internal/session-close.ts +++ b/packages/daemon/src/sessions/session-lifecycle/internal/session-close.ts @@ -20,7 +20,7 @@ import { import { isAuthoringArmedSession } from '../../../core/session-script-publication-capability.ts'; import type { SessionCleanupFailure } from '../../session-teardown.ts'; import { isWebSession } from '../../web-session-names.ts'; -import { clearDeviceClaim } from '../../../resources/device/device-claims.ts'; +import { clearDeviceClaim } from '@agent-device/daemon-contracts/device-claims'; import { applicationLifecycleExecutionFromRequest } from '../../application-lifecycle-execution.ts'; import { hasRuntimeTransportHints } from '../../session-runtime.ts'; import type { diff --git a/packages/daemon/src/sessions/session-lifecycle/internal/session-open-execution.ts b/packages/daemon/src/sessions/session-lifecycle/internal/session-open-execution.ts index 4057ef8038..5cd67bf7ef 100644 --- a/packages/daemon/src/sessions/session-lifecycle/internal/session-open-execution.ts +++ b/packages/daemon/src/sessions/session-lifecycle/internal/session-open-execution.ts @@ -1,3 +1,7 @@ +import { + resolveSessionRequestLogPath, + resolveSessionRunnerLogPath, +} from '@agent-device/daemon-contracts/session-artifact-paths'; import { openApplicationRuntimeUse, openApplicationWithRuntimeHintApplyUse, @@ -22,10 +26,6 @@ import { import { isRequestCanceled } from '@agent-device/host-kit/request'; import { createRequestCanceledError } from '@agent-device/kernel/errors'; import { SessionStore } from '../../../core/session-store.ts'; -import { - resolveSessionRequestLogPath, - resolveSessionRunnerLogPath, -} from '../../../core/session-artifact-paths.ts'; import { countConfiguredRuntimeHints, runtimeHintValues, @@ -58,8 +58,8 @@ import { type DeviceClaimAcquireResult, type DeviceClaimSessionOwnership, type DeviceClaimReconciler, -} from '../../../resources/device/device-claims.ts'; -import type { TakenOverDeviceClaim } from '../../../resources/device/device-claim-reboot.ts'; + type TakenOverDeviceClaim, +} from '@agent-device/daemon-contracts/device-claims'; import { appendResponseWarning } from './session-open-warnings.ts'; import { buildDeviceClaimConflictError } from '../../../resources/device/device-claim-conflict.ts'; import { deviceClaimRuleForOwner } from '../../../resources/device/device-claim-rule.ts'; diff --git a/packages/daemon/src/sessions/session-lifecycle/internal/session-open-state.ts b/packages/daemon/src/sessions/session-lifecycle/internal/session-open-state.ts index e1cf41eb7e..4108c931e5 100644 --- a/packages/daemon/src/sessions/session-lifecycle/internal/session-open-state.ts +++ b/packages/daemon/src/sessions/session-lifecycle/internal/session-open-state.ts @@ -3,7 +3,7 @@ import type { SessionSurface } from '@agent-device/contracts/session'; import type { DaemonRequest } from '../../../core/daemon-request.ts'; import type { SessionRef } from '../../../core/session-state.ts'; import type { SessionStore } from '../../../core/session-store.ts'; -import type { DeviceClaimSessionOwnership } from '../../../resources/device/device-claims.ts'; +import type { DeviceClaimSessionOwnership } from '@agent-device/daemon-contracts/device-claims'; import { clearSessionSnapshot } from '../../../execution/session-snapshot.ts'; import { resolveSessionLeaseForRequest } from '../../lease-lifecycle.ts'; import { resolvePublicSessionName, resolveSessionScope } from '../../session-routing.ts'; diff --git a/packages/daemon/src/sessions/session-lifecycle/internal/session-open.ts b/packages/daemon/src/sessions/session-lifecycle/internal/session-open.ts index 4f716d0c27..03be15c5e5 100644 --- a/packages/daemon/src/sessions/session-lifecycle/internal/session-open.ts +++ b/packages/daemon/src/sessions/session-lifecycle/internal/session-open.ts @@ -29,7 +29,7 @@ import { resolveForegroundOpenRequest, } from './session-open-foreground.ts'; import { expireRefFrame } from '../../../core/ref-frame.ts'; -import type { DeviceClaimReconciler } from '../../../resources/device/device-claims.ts'; +import type { DeviceClaimReconciler } from '@agent-device/daemon-contracts/device-claims'; import type { BindDeviceRuntime, InspectDeviceRuntimeFacts, diff --git a/packages/daemon/src/sessions/session-observability/internal/__tests__/session-logs.test.ts b/packages/daemon/src/sessions/session-observability/internal/__tests__/session-logs.test.ts index 700bb8be09..13f2a05595 100644 --- a/packages/daemon/src/sessions/session-observability/internal/__tests__/session-logs.test.ts +++ b/packages/daemon/src/sessions/session-observability/internal/__tests__/session-logs.test.ts @@ -1,3 +1,4 @@ +import { appLogResourceStore } from '@agent-device/capture-kit/app-log-durable-resource'; import { daemonPlatformServicesFixture } from '../../../../__tests__/support/platform-services-fixture.ts'; import fs from 'node:fs'; import { beforeEach, expect, test, vi } from 'vitest'; @@ -20,7 +21,6 @@ import { type AppLogAdmissionLedger, } from '../../../../resources/app-log-admission-ledger.ts'; import { handleSessionCommands } from '../../../../__tests__/support/handlers/session-command-harness.ts'; -import { appLogResourceStore } from '../../../../resources/app-log-resource-store.ts'; import type { BindDeviceRuntime } from '../../../../core/request-runtime-binding.ts'; import type { SessionStore } from '../../../../core/session-store.ts'; import { diff --git a/packages/daemon/src/sessions/session-observability/internal/session-observability.ts b/packages/daemon/src/sessions/session-observability/internal/session-observability.ts index 093c44e09a..d3246f601f 100644 --- a/packages/daemon/src/sessions/session-observability/internal/session-observability.ts +++ b/packages/daemon/src/sessions/session-observability/internal/session-observability.ts @@ -1,3 +1,4 @@ +import { appLogResourceStore } from '@agent-device/capture-kit/app-log-durable-resource'; import type { DaemonPlatformServices } from '../../../core/platform-services.ts'; import type { LogBackend } from '@agent-device/contracts/observability'; import type { @@ -16,7 +17,6 @@ import { appendAppLogMarker, getAppLogPathMetadata } from '../../../resources/ap import type { AppLogAdmissionLedger } from '../../../resources/app-log-admission-ledger.ts'; import { type AudioProbeAdmissionLedger } from '@agent-device/capture-kit/audio-probe-admission-ledger'; import { type PerfCaptureAdmissionLedger } from '@agent-device/capture-kit/perf-capture-admission-ledger'; -import { appLogResourceStore } from '../../../resources/app-log-resource-store.ts'; import { adoptStartedSessionAppLog, bindSessionAppLog, diff --git a/packages/kernel/src/device.ts b/packages/kernel/src/device.ts index cd39739219..4561ab86fc 100644 --- a/packages/kernel/src/device.ts +++ b/packages/kernel/src/device.ts @@ -1,4 +1,5 @@ import { AppError } from './errors.ts'; +import type { DeviceIdentity } from './device-identity.ts'; export { deviceIdentity, @@ -256,6 +257,90 @@ export function isAppleOs(value: unknown): value is AppleOS { return (APPLE_OS_VALUES as readonly unknown[]).includes(value); } +export function decodeDeviceIdentity(value: unknown): DeviceIdentity | null { + if (!isObject(value)) return null; + const id = readDeviceIdentityId(value.id); + const family = readDeviceIdentityFamily(value.family); + const kind = readDeviceIdentityKind(value.kind); + const target = readDeviceIdentityTarget(value.target); + if (!id || !family || !kind || target === null) return null; + if (!hasCoherentAppleIdentity(value, family, kind)) return null; + return buildDeviceIdentity(value, id, family, kind, target); +} + +function buildDeviceIdentity( + value: Record & { + appleOs?: AppleOS; + iosPhysicalDeviceBackend?: DeviceInfo['iosPhysicalDeviceBackend']; + }, + id: string, + family: Platform, + kind: DeviceKind, + target: DeviceTarget | undefined, +): DeviceIdentity { + return Object.freeze({ + id, + family, + ...(value.appleOs === undefined ? {} : { appleOs: value.appleOs }), + kind, + ...(target === undefined ? {} : { target }), + ...(value.iosPhysicalDeviceBackend === undefined + ? {} + : { iosPhysicalDeviceBackend: value.iosPhysicalDeviceBackend }), + }); +} + +function readDeviceIdentityId(value: unknown): string | null { + return isNonEmptyString(value) ? value : null; +} + +function readDeviceIdentityFamily(value: unknown): Platform | null { + return isPlatform(value) ? value : null; +} + +function readDeviceIdentityKind(value: unknown): DeviceKind | null { + return isDeviceKind(value) ? value : null; +} + +function readDeviceIdentityTarget(value: unknown): DeviceTarget | undefined | null { + if (value === undefined) return undefined; + return isDeviceTarget(value) ? value : null; +} + +function hasCoherentAppleIdentity( + value: Record, + family: Platform, + kind: DeviceKind, +): value is Record & { + appleOs?: AppleOS; + iosPhysicalDeviceBackend?: DeviceInfo['iosPhysicalDeviceBackend']; +} { + if (family === 'apple') { + if (!isAppleOs(value.appleOs)) return false; + } else if (value.appleOs !== undefined || value.iosPhysicalDeviceBackend !== undefined) { + return false; + } + return hasValidPhysicalAppleBackend(value.iosPhysicalDeviceBackend, kind); +} + +function hasValidPhysicalAppleBackend(value: unknown, kind: DeviceKind): boolean { + if (value === undefined) return true; + if (kind !== 'device') return false; + return value === 'coredevice' || value === 'xctest'; +} + +function isDeviceKind(value: unknown): value is DeviceKind { + return (DEVICE_KINDS as readonly unknown[]).includes(value); +} + +function isNonEmptyString(value: unknown): value is string { + return typeof value === 'string' && value.trim().length > 0; +} + +function isObject(value: unknown): value is Record { + return typeof value === 'object' && value !== null && !Array.isArray(value); +} + export function matchesPlatformSelector( device: Pick, selector: PlatformSelector | undefined, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0d344c6697..53f44ec2c5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -355,6 +355,9 @@ importers: packages/daemon-contracts: dependencies: + '@agent-device/capture-kit': + specifier: workspace:* + version: link:../capture-kit '@agent-device/command-registry': specifier: workspace:* version: link:../command-registry diff --git a/scripts/__tests__/eager-closure-budgets.ts b/scripts/__tests__/eager-closure-budgets.ts index 01a719f264..2caa42e08c 100644 --- a/scripts/__tests__/eager-closure-budgets.ts +++ b/scripts/__tests__/eager-closure-budgets.ts @@ -132,7 +132,26 @@ export const NEW_ENTRY_CEILINGS: Readonly> = Objec */ export const APPROVED_OVER_CEILING: Readonly< Record -> = Object.freeze({}); +> = Object.freeze({ + 'packages/capture-kit/src/capture-admission/app-log-durable-resource.ts': { + issue: '#3616', + reason: + 'Moved verbatim out of the daemon app-log session resource so the claim reconciler no longer imports the daemon; the closure is the durable-capture machinery every capture resource carries, and no consumer gains modules.', + owner: 'daemon single-entry stack (L1b)', + }, + 'packages/daemon-contracts/src/device-claims.fixtures.ts': { + issue: '#3616', + reason: + 'Test-only entry for daemon tests that drive the claim reconciler directly; it loads the same closure as device-claim-recovery.ts and no production module imports it.', + owner: 'daemon single-entry stack (L1b)', + }, + 'packages/daemon-contracts/src/device-claim-recovery.ts': { + issue: '#3616', + reason: + 'Moved verbatim out of the daemon so the CLI release path and the daemon share one reconciler without the CLI importing the daemon; only the daemon and the lazily loaded stale-release path import it.', + owner: 'daemon single-entry stack (L1b)', + }, +}); /** The category is a function of the path, never a hand-written column. */ export function entryCategoryOf(entryFile: string): EntryCategory { diff --git a/scripts/layering/architecture-ownership.ts b/scripts/layering/architecture-ownership.ts index be365093c8..6faed92436 100644 --- a/scripts/layering/architecture-ownership.ts +++ b/scripts/layering/architecture-ownership.ts @@ -111,10 +111,13 @@ export const SNAPSHOT_EXECUTION_RETIRED_HANDLER_PATHS = [ 'packages/daemon/src/handlers/snapshot-session.ts', ] as const; +/** The CLI, typed client, remote transports and daemon client: code that reaches the daemon over the network. */ +export const CLIENT_SIDE_POLICY_NAME = 'client-side'; + export const LOGICAL_MODULE_POLICIES = [ { - name: 'daemon-client', - roots: ['src/daemon-client/'], + name: CLIENT_SIDE_POLICY_NAME, + roots: ['src/cli.ts', 'src/cli/', 'src/client/', 'src/remote/', 'src/daemon-client/'], forbiddenTargetRoots: [DAEMON_SOURCE_ROOT], }, { diff --git a/scripts/layering/check.ts b/scripts/layering/check.ts index f34a288ed0..c100c29bff 100644 --- a/scripts/layering/check.ts +++ b/scripts/layering/check.ts @@ -132,6 +132,7 @@ import { recordRuntimeRegistryJoinViolations } from './record-runtime-registry-p import { recordRuntimeDaemonMechanicsViolations } from './record-runtime-mechanics-policy.ts'; import { checkDaemonPlatformBoundary } from './daemon-platform-boundary.ts'; import { checkDaemonPlatformRuntimeInventory } from './daemon-platform-runtime-inventory.ts'; +import { checkDaemonClientEntry } from './daemon-client-entry.ts'; import { checkDaemonProcessReach } from './daemon-process-reach.ts'; import { checkDaemonLayers, DAEMON_ROOT } from './daemon-layers.ts'; import { checkSessionAuthorityOverlay, handlerOwnedOverlay } from './session-authority-overlay.ts'; @@ -518,6 +519,7 @@ export const LAYERING_RULE_IDS = [ 'snapshot-assembly-presentation-neutrality', 'daemon-platform-runtime-inventory', 'daemon-process-reach', + 'daemon-client-entry', 'session-authority-overlay', 'daemon-layers', ] as const; @@ -582,6 +584,7 @@ export const LAYERING_RULES: Readonly> = { 'daemon-platform-runtime-inventory': (context) => checkDaemonPlatformRuntimeInventory(context.edges), 'daemon-process-reach': (context) => checkDaemonProcessReach(context.sourceFiles, context.edges), + 'daemon-client-entry': (context) => checkDaemonClientEntry(context.sourceFiles, context.edges), 'session-authority-overlay': (context) => checkSessionAuthorityOverlay( context.ratchets.sessionAuthority, diff --git a/scripts/layering/daemon-client-entry.test.ts b/scripts/layering/daemon-client-entry.test.ts new file mode 100644 index 0000000000..dbb0316f9c --- /dev/null +++ b/scripts/layering/daemon-client-entry.test.ts @@ -0,0 +1,62 @@ +import assert from 'node:assert/strict'; +import { test } from 'node:test'; +import { + checkDaemonClientEntry, + CLI_PROCESS_ENTRIES, + DAEMON_CLIENT_ENTRY_RULE, +} from './daemon-client-entry.ts'; +import { resolveImportEdges } from './model.ts'; + +const DAEMON_MODULE = 'packages/daemon/src/server/platform-request-scope.ts'; + +/** The CLI entries and a daemon module exist as stubs, so a case fails only on what it plants. */ +function check(sources: Record) { + const tree = new Map([ + ...CLI_PROCESS_ENTRIES.map((entry) => [entry, 'export {};\n'] as const), + [DAEMON_MODULE, 'export const scope = 1;\n'], + ...Object.entries(sources), + ]); + return checkDaemonClientEntry([...tree.keys()], resolveImportEdges(tree)); +} + +test('a daemon import from a module the CLI never reaches is not CLI reach', () => { + assert.deepEqual(check({ 'src/daemon-composition.ts': `import '../${DAEMON_MODULE}';\n` }), []); +}); + +test('a root module the CLI lazy-loads cannot launder a daemon import', () => { + const violations = check({ + 'src/cli.ts': "export const run = () => import('./cli/commands/device.ts');\n", + 'src/cli/commands/device.ts': + "export const release = () => import('../../platform-runtime-claim-recovery.ts');\n", + 'src/platform-runtime-claim-recovery.ts': `import '../${DAEMON_MODULE}';\n`, + }); + assert.equal(violations.length, 1); + assert.equal(violations[0]!.rule, DAEMON_CLIENT_ENTRY_RULE); + assert.equal(violations[0]!.file, 'src/platform-runtime-claim-recovery.ts'); + assert.ok( + violations[0]!.message.includes( + `Chain: src/cli.ts -> src/cli/commands/device.ts -> src/platform-runtime-claim-recovery.ts -> ${DAEMON_MODULE}.`, + ), + violations[0]!.message, + ); +}); + +test('a daemon import inside a client-side root is left to the client-side policy', () => { + assert.deepEqual(check({ 'src/cli.ts': `import '../${DAEMON_MODULE}';\n` }), []); +}); + +test('a type-only import by a reached root module is not a load', () => { + assert.deepEqual( + check({ + 'src/cli.ts': "import './platform-runtime-types.ts';\n", + 'src/platform-runtime-types.ts': `import type { scope } from '../${DAEMON_MODULE}';\n`, + }), + [], + ); +}); + +test('a missing CLI entry fails as stale instead of passing silently', () => { + const violations = checkDaemonClientEntry(['src/cli.ts'], []); + assert.equal(violations.length, 1); + assert.match(violations[0]!.message, /stale module: src\/bin\.ts/); +}); diff --git a/scripts/layering/daemon-client-entry.ts b/scripts/layering/daemon-client-entry.ts new file mode 100644 index 0000000000..bdb150db14 --- /dev/null +++ b/scripts/layering/daemon-client-entry.ts @@ -0,0 +1,59 @@ +import { + CLIENT_SIDE_POLICY_NAME, + LOGICAL_MODULE_POLICIES, + matchesDeclaredRoot, +} from './architecture-ownership.ts'; +import type { LayeringViolation, ResolvedImportEdge } from './model.ts'; +import { staleReachModules, walkProcessReach } from './process-reach.ts'; +import { DAEMON_SOURCE_ROOT } from './tracked-sources.ts'; + +export const DAEMON_CLIENT_ENTRY_RULE = 'R78 daemon-client-entry'; + +/** The modules a CLI process starts from. */ +export const CLI_PROCESS_ENTRIES: readonly string[] = ['src/bin.ts', 'src/cli.ts']; + +/** + * Catches: any module the CLI process can load — over static and dynamic edges, so a root module + * the CLI lazy-loads counts — importing the daemon package. The folder ban on the client-side + * roots lives in `LOGICAL_MODULE_POLICIES` (R10) and covers direct edges, type-only included; + * this rule covers the hop outside those roots, where a root platform-runtime module could + * launder a daemon import into the CLI process. The violation names the chain from the entry. + * What the client shares with the daemon lives below both: `packages/daemon-contracts`, + * `DaemonWireRequest` in `@agent-device/contracts/command`, `DaemonResponse` in kernel. + * Evidence: the folder ban alone left `device release --stale` loading the daemon reconciler + * through a root module one hop away. + * Cost: one breadth-first walk over the resolved edges, inside check:layering. + * Kill criterion: a maintainer decision that the CLI process may load daemon internals directly. + */ +export function checkDaemonClientEntry( + sourceFiles: readonly string[], + edges: readonly ResolvedImportEdge[], + entries: readonly string[] = CLI_PROCESS_ENTRIES, +): LayeringViolation[] { + const stale = staleReachModules( + DAEMON_CLIENT_ENTRY_RULE, + 'scripts/layering/daemon-client-entry.ts', + entries, + sourceFiles, + 'CLI_PROCESS_ENTRIES', + ); + if (stale.length > 0) return stale; + const policy = LOGICAL_MODULE_POLICIES.find(({ name }) => name === CLIENT_SIDE_POLICY_NAME); + const isPolicyCovered = (file: string) => + policy?.roots.some((root) => matchesDeclaredRoot(file, root)) ?? false; + const walk = walkProcessReach( + entries, + edges.filter((edge) => !edge.file.startsWith(DAEMON_SOURCE_ROOT)), + (edge) => edge.target.startsWith(DAEMON_SOURCE_ROOT), + ); + return walk.stopped + .filter((edge) => !isPolicyCovered(edge.file)) + .map((edge) => ({ + rule: DAEMON_CLIENT_ENTRY_RULE, + file: edge.file, + line: edge.line, + message: + `the CLI process reaches daemon internals: ${edge.file} imports ${edge.target}. ` + + `Chain: ${[...walk.chainTo(edge.file), edge.target].join(' -> ')}.`, + })); +} diff --git a/scripts/layering/daemon-modularity.test.ts b/scripts/layering/daemon-modularity.test.ts index 77492bc706..d0c8b33fc8 100644 --- a/scripts/layering/daemon-modularity.test.ts +++ b/scripts/layering/daemon-modularity.test.ts @@ -88,7 +88,7 @@ test('a production import of the daemon request/session-state modules outside th const edges = resolveImportEdges( new Map([ [ - 'src/remote/daemon-artifacts.ts', + 'src/commands/new-importer.ts', "import type { SessionState } from '../../packages/daemon/src/core/session-state.ts';", ], ['packages/daemon/src/core/session-state.ts', 'export type SessionState = { name: string };'], @@ -97,10 +97,7 @@ test('a production import of the daemon request/session-state modules outside th const violations = checkDaemonModularityRatchets(edges, REFERENCE, REFERENCE); assert.equal(violations.length, 1); - assert.match( - violations[0]!.message, - /src\/remote\/daemon-artifacts\.ts imports a daemon request/, - ); + assert.match(violations[0]!.message, /src\/commands\/new-importer\.ts imports a daemon request/); }); test('logical modules reject forbidden imports', () => { @@ -656,28 +653,36 @@ test('R9 banks a shrink with no edit anywhere', () => { ); }); -for (const [kind, source] of [ - [ - 'type-only', - "import type { DaemonRequest } from '../../packages/daemon/src/core/daemon-request.ts';", - ], - ['value', "import { handle } from '../../packages/daemon/src/server/daemon-stop.ts';"], - [ - 'dynamic', - "export const load = () => import('../../packages/daemon/src/server/daemon-stop.ts');", - ], -] as const) { - test(`the daemon client importing the daemon package is red (${kind})`, () => { - const edges = resolveImportEdges( - new Map([ - ['src/daemon-client/daemon-client.ts', source], - ['packages/daemon/src/core/daemon-request.ts', 'export type DaemonRequest = object;'], - ['packages/daemon/src/server/daemon-stop.ts', 'export const handle = 1;'], - ]), - ); - - const violations = checkDaemonModularityRatchets(edges, REFERENCE, REFERENCE); - assert.equal(violations.length, 1); - assert.match(violations[0]!.message, /daemon-client must not import packages\/daemon\/src/); - }); +for (const file of [ + 'src/cli.ts', + 'src/cli/commands/daemon.ts', + 'src/client/client-normalizers.ts', + 'src/remote/daemon-artifacts.ts', + 'src/daemon-client/daemon-client.ts', +]) { + for (const [kind, source] of [ + [ + 'type-only', + "import type { DaemonRequest } from '../packages/daemon/src/core/daemon-request.ts';", + ], + ['value', "import { handle } from '../packages/daemon/src/server/daemon-stop.ts';"], + [ + 'dynamic', + "export const load = () => import('../packages/daemon/src/server/daemon-stop.ts');", + ], + ] as const) { + test(`client-side ${file} importing the daemon package is red (${kind})`, () => { + const edges = resolveImportEdges( + new Map([ + [file, source.replaceAll('../', '../'.repeat(file.split('/').length - 1))], + ['packages/daemon/src/core/daemon-request.ts', 'export type DaemonRequest = object;'], + ['packages/daemon/src/server/daemon-stop.ts', 'export const handle = 1;'], + ]), + ); + + const violations = checkDaemonModularityRatchets(edges, REFERENCE, REFERENCE); + assert.equal(violations.length, 1); + assert.match(violations[0]!.message, /client-side must not import packages\/daemon\/src/); + }); + } } diff --git a/scripts/layering/daemon-process-reach.ts b/scripts/layering/daemon-process-reach.ts index 43992933ec..6b91921c56 100644 --- a/scripts/layering/daemon-process-reach.ts +++ b/scripts/layering/daemon-process-reach.ts @@ -1,4 +1,5 @@ import type { LayeringViolation, ResolvedImportEdge } from './model.ts'; +import { staleReachModules, walkProcessReach } from './process-reach.ts'; export const DAEMON_PROCESS_REACH_RULE = 'R82 daemon-process-reach'; @@ -32,41 +33,19 @@ export function checkDaemonProcessReach( entry: string = DAEMON_PROCESS_ENTRY, denied: readonly string[] = DAEMON_PROCESS_DENIED_MODULES, ): LayeringViolation[] { - const tracked = new Set(sourceFiles); - const stale = [entry, ...denied].filter((module) => !tracked.has(module)); - if (stale.length > 0) { - return stale.map((module) => ({ - rule: DAEMON_PROCESS_REACH_RULE, - file: 'scripts/layering/daemon-process-reach.ts', - line: 1, - message: - `stale module: ${module} is not a tracked source. Point DAEMON_PROCESS_ENTRY or ` + - `DAEMON_PROCESS_DENIED_MODULES at its new path so the reach check keeps a real target.`, - })); - } - const outgoing = new Map(); - for (const edge of edges) { - if (edge.typeOnly && !edge.dynamic) continue; - const list = outgoing.get(edge.file) ?? []; - list.push(edge); - outgoing.set(edge.file, list); - } - const cameFrom = new Map([[entry, null]]); - const queue = [entry]; - for (let head = 0; head < queue.length; head += 1) { - const current = queue[head]!; - for (const edge of outgoing.get(current) ?? []) { - if (cameFrom.has(edge.target)) continue; - cameFrom.set(edge.target, edge); - queue.push(edge.target); - } - } + const stale = staleReachModules( + DAEMON_PROCESS_REACH_RULE, + 'scripts/layering/daemon-process-reach.ts', + [entry, ...denied], + sourceFiles, + 'DAEMON_PROCESS_ENTRY or DAEMON_PROCESS_DENIED_MODULES', + ); + if (stale.length > 0) return stale; + const walk = walkProcessReach([entry], edges); const violations: LayeringViolation[] = []; for (const module of denied) { - const via = cameFrom.get(module); + const via = walk.loadedBy.get(module); if (!via) continue; - const chain: string[] = [module]; - for (let at = via; at; at = cameFrom.get(at.file) ?? null) chain.unshift(at.file); violations.push({ rule: DAEMON_PROCESS_REACH_RULE, file: via.file, @@ -74,7 +53,7 @@ export function checkDaemonProcessReach( message: `${entry} reaches ${module}, which the daemon process must never load: the daemon ` + `receives the client's replay source bundle and does not read caller paths. ` + - `Chain: ${chain.join(' -> ')}.`, + `Chain: ${[...walk.chainTo(via.file), module].join(' -> ')}.`, }); } return violations; diff --git a/scripts/layering/package-boundaries.test.ts b/scripts/layering/package-boundaries.test.ts index 3e544c31ec..8bb4bd5376 100644 --- a/scripts/layering/package-boundaries.test.ts +++ b/scripts/layering/package-boundaries.test.ts @@ -474,6 +474,9 @@ test('the real tree parses, declares, and passes R11', () => { assert.deepEqual([...captureKitPackage.exportTargets.keys()].sort(), [ '@agent-device/capture-kit', '@agent-device/capture-kit/android-replacement-surface-occlusion', + '@agent-device/capture-kit/app-log-durable-resource', + '@agent-device/capture-kit/app-log-files', + '@agent-device/capture-kit/app-log-resource-recovery', '@agent-device/capture-kit/audio-probe-admission-ledger', '@agent-device/capture-kit/audio-probe-descriptor-fixtures', '@agent-device/capture-kit/audio-probe-recovery', diff --git a/scripts/layering/process-reach.ts b/scripts/layering/process-reach.ts new file mode 100644 index 0000000000..4777f6c00c --- /dev/null +++ b/scripts/layering/process-reach.ts @@ -0,0 +1,79 @@ +import type { LayeringViolation, ResolvedImportEdge } from './model.ts'; + +/** What one breadth-first walk over the runtime import edges from a process entry found. */ +export type ReachWalk = Readonly<{ + /** The edge that first loaded each reached module; null for an entry. */ + loadedBy: ReadonlyMap; + /** Runtime edges the walk refused to enter, in discovery order. */ + stopped: readonly ResolvedImportEdge[]; + /** The shortest chain of modules from an entry to `file`. */ + chainTo(file: string): readonly string[]; +}>; + +/** + * Walks every module a process can load from `entries` over static and dynamic edges, so a + * function-scoped `await import` counts. Type-only static edges load nothing and are skipped. An + * edge `stopAt` accepts is recorded in `stopped` and not entered. + */ +export function walkProcessReach( + entries: readonly string[], + edges: readonly ResolvedImportEdge[], + stopAt: (edge: ResolvedImportEdge) => boolean = () => false, +): ReachWalk { + const outgoing = new Map(); + for (const edge of edges) { + if (edge.typeOnly && !edge.dynamic) continue; + const list = outgoing.get(edge.file) ?? []; + list.push(edge); + outgoing.set(edge.file, list); + } + const loadedBy = new Map(entries.map((e) => [e, null])); + const stopped: ResolvedImportEdge[] = []; + const queue = [...entries]; + for (let head = 0; head < queue.length; head += 1) { + for (const edge of outgoing.get(queue[head]!) ?? []) { + if (stopAt(edge)) { + stopped.push(edge); + continue; + } + if (loadedBy.has(edge.target)) continue; + loadedBy.set(edge.target, edge); + queue.push(edge.target); + } + } + return { + loadedBy, + stopped, + chainTo(file) { + const chain = [file]; + for (let at = loadedBy.get(file) ?? null; at; at = loadedBy.get(at.file) ?? null) { + chain.unshift(at.file); + } + return chain; + }, + }; +} + +/** + * A rename or move must not turn a reach walk into a silent pass: every entry or denied module + * that is not a tracked source is a violation of its own. + */ +export function staleReachModules( + rule: string, + declaredIn: string, + modules: readonly string[], + sourceFiles: readonly string[], + declaration: string, +): LayeringViolation[] { + const tracked = new Set(sourceFiles); + return modules + .filter((module) => !tracked.has(module)) + .map((module) => ({ + rule, + file: declaredIn, + line: 1, + message: + `stale module: ${module} is not a tracked source. Point ${declaration} at its new ` + + 'path so the reach check keeps a real target.', + })); +} diff --git a/src/__tests__/cli-remote-diagnostics.test.ts b/src/__tests__/cli-remote-diagnostics.test.ts index 5e12df5cf2..3bd3b0e71b 100644 --- a/src/__tests__/cli-remote-diagnostics.test.ts +++ b/src/__tests__/cli-remote-diagnostics.test.ts @@ -5,6 +5,7 @@ * locator → fetch → rendered line) is observable end to end. */ +import { resolveSessionRequestLogPath } from '@agent-device/daemon-contracts/session-artifact-paths'; import type { DaemonWireRequest } from '@agent-device/contracts/command'; import type { DaemonResponse } from '@agent-device/kernel/contracts'; import { test } from 'vitest'; @@ -13,7 +14,6 @@ import fs from 'node:fs'; import path from 'node:path'; import { normalizeError, AppError } from '@agent-device/kernel/errors'; import { createDaemonHttpServer } from '@agent-device/daemon/testing/server/http-server'; -import { resolveSessionRequestLogPath } from '@agent-device/daemon/session-artifact-paths'; import { safeSessionName } from '@agent-device/host-kit/session-paths'; import { runCliCapture, type CapturedCliRun } from './cli-capture.ts'; import { diff --git a/src/__tests__/daemon-composition/handlers/session-device-claims-cancellation.test.ts b/src/__tests__/daemon-composition/handlers/session-device-claims-cancellation.test.ts index a36ccd82dd..816e87ff6f 100644 --- a/src/__tests__/daemon-composition/handlers/session-device-claims-cancellation.test.ts +++ b/src/__tests__/daemon-composition/handlers/session-device-claims-cancellation.test.ts @@ -40,7 +40,7 @@ import { resolveAndroidPackageForOpen, } from '@agent-device/platform-android/mechanics'; import { clearRequestCanceled, markRequestCanceled } from '@agent-device/host-kit/request'; -import { inspectDeviceClaims } from '@agent-device/daemon/device/device-claim-inspection'; +import { inspectDeviceClaims } from '@agent-device/daemon-contracts/device-claims'; import { localRuntimeOwner, narrowDeviceBinding, diff --git a/src/__tests__/daemon-composition/platform-services-fixture.ts b/src/__tests__/daemon-composition/platform-services-fixture.ts index 19ac33979d..f3b9e3a2e0 100644 --- a/src/__tests__/daemon-composition/platform-services-fixture.ts +++ b/src/__tests__/daemon-composition/platform-services-fixture.ts @@ -1,7 +1,7 @@ +import { type ClaimRecoveryGatewayFactory } from '@agent-device/daemon-contracts/device-claim-recovery'; import { createDaemonPlatformServices } from '../../platform-runtime-daemon-services.ts'; import { createClaimRecoveryGateway } from '../../platform-runtime-claim-recovery.ts'; import type { DaemonPlatformServices } from '@agent-device/daemon/platform-services'; -import type { ClaimRecoveryGatewayFactory } from '@agent-device/daemon/device/claim-recovery-gateway'; /** * The port the production composition root builds, handed to a test the same way the shared router diff --git a/src/__tests__/daemon-composition/request-router-open-claim-recovery.test.ts b/src/__tests__/daemon-composition/request-router-open-claim-recovery.test.ts index 25028a974f..08fe609a3d 100644 --- a/src/__tests__/daemon-composition/request-router-open-claim-recovery.test.ts +++ b/src/__tests__/daemon-composition/request-router-open-claim-recovery.test.ts @@ -78,7 +78,7 @@ import { } from '@agent-device/daemon/testing/__tests__/application-lifecycle-runtime-fixture'; import { discoverReadyAndroidEmulators } from './application-lifecycle-runtime-fixture.ts'; import { makeSessionStore } from '@agent-device/daemon/testing/__tests__/support/store-factory'; -import { inspectDeviceClaims } from '@agent-device/daemon/device/device-claim-inspection'; +import { inspectDeviceClaims } from '@agent-device/daemon-contracts/device-claims'; import { createOwnedProcessRecordStore, isProcessAlive, diff --git a/src/__tests__/daemon-composition/server/daemon-runtime-lifecycle-shutdown.test.ts b/src/__tests__/daemon-composition/server/daemon-runtime-lifecycle-shutdown.test.ts index 3f8e342072..8585f3a120 100644 --- a/src/__tests__/daemon-composition/server/daemon-runtime-lifecycle-shutdown.test.ts +++ b/src/__tests__/daemon-composition/server/daemon-runtime-lifecycle-shutdown.test.ts @@ -1,3 +1,4 @@ +import { resolveDeviceClaimPath } from '@agent-device/daemon-contracts/device-claims-fixtures'; import fs from 'node:fs'; import path from 'node:path'; import { afterEach, expect, test, vi } from 'vitest'; @@ -133,8 +134,7 @@ vi.mock('@agent-device/daemon/testing/provider-lease-expiry', async (importOrigi }; }); -import { acquireDeviceClaim } from '@agent-device/daemon/device/device-claims'; -import { resolveDeviceClaimPath } from '@agent-device/daemon/testing/device/device-claim-paths'; +import { acquireDeviceClaim } from '@agent-device/daemon-contracts/device-claims'; import { ANDROID_EMULATOR } from '@agent-device/test-kit/device-fixtures'; import { isolatedDeviceClaimStores, diff --git a/src/__tests__/daemon-registration-owner.test.ts b/src/__tests__/daemon-registration-owner.test.ts index fed23def09..670cfdf180 100644 --- a/src/__tests__/daemon-registration-owner.test.ts +++ b/src/__tests__/daemon-registration-owner.test.ts @@ -32,7 +32,7 @@ import { inspectProcessLock } from '@agent-device/host-kit/file'; import { AppError } from '@agent-device/kernel/errors'; import { sleep } from '@agent-device/host-kit/retry'; import { stopDaemonProcess } from '@agent-device/daemon-contracts/daemon-process'; -import { stopDaemon } from '@agent-device/daemon/daemon-stop'; +import { stopDaemon } from '@agent-device/daemon-contracts/daemon-stop'; const fields = { socketPort: 4210, diff --git a/src/__tests__/cli-device-release.test.ts b/src/__tests__/platform-runtime-claim-recovery.test.ts similarity index 98% rename from src/__tests__/cli-device-release.test.ts rename to src/__tests__/platform-runtime-claim-recovery.test.ts index 4391805f83..8c3c3ba0fc 100644 --- a/src/__tests__/cli-device-release.test.ts +++ b/src/__tests__/platform-runtime-claim-recovery.test.ts @@ -1,3 +1,4 @@ +import { appLogResourceStore } from '@agent-device/capture-kit/app-log-durable-resource'; import assert from 'node:assert/strict'; import crypto from 'node:crypto'; import fs from 'node:fs'; @@ -6,8 +7,7 @@ import { test, vi } from 'vitest'; import { readCurrentOwnerIdentity } from '@agent-device/host-kit/process'; import { createDurableResourceEnvelope } from '@agent-device/capture-kit'; import { localRuntimeOwner } from '@agent-device/contracts/platform-runtime'; -import { appLogResourceStore } from '@agent-device/daemon/testing/app-log-resource-store'; -import { runStaleDeviceClaimRelease } from '../cli/commands/device-release.ts'; +import { runStaleDeviceClaimRelease } from '../platform-runtime-claim-recovery.ts'; import { runCliCapture } from './cli-capture.ts'; import { mkdtempForTestSync } from '@agent-device/test-kit/tmp-dir'; diff --git a/src/cli.ts b/src/cli.ts index 88d9f4abff..b7bce9f4f9 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -1,3 +1,4 @@ +import { resolveRemoteRequestDiagnosticsPath } from '@agent-device/daemon-contracts/session-artifact-paths'; import { parseRawArgs, usage, usageForCommand } from './cli/parser/args.ts'; import { suggestCommandFor } from './cli/parser/command-suggestions.ts'; import { @@ -7,7 +8,7 @@ import { throwDaemonError, type NormalizedError, } from '@agent-device/kernel/errors'; -import { resolveRemoteRequestDiagnosticsPath } from '@agent-device/daemon/session-artifact-paths'; +import { resolveDaemonPaths } from '@agent-device/daemon-contracts/daemon-resolution'; import { exitAfterFlush } from './cli/process-exit.ts'; import { readVersion } from '@agent-device/host-kit/version'; import { pathToFileURL } from 'node:url'; @@ -36,7 +37,6 @@ import { registerDiagnosticSensitiveValue, withDiagnosticsScope, } from '@agent-device/host-kit/diagnostics'; -import { resolveDaemonPaths } from '@agent-device/daemon-contracts/daemon-resolution'; import { applyDefaultPlatformBinding, resolveBindingSettings } from './cli/session-binding.ts'; import { resolveCliOptions } from './cli/resolve-cli-options.ts'; import { maybeRunUpgradeNotifier } from './cli/update-check.ts'; diff --git a/src/cli/command-explain.ts b/src/cli/command-explain.ts index 569ba29a8a..0891ca8403 100644 --- a/src/cli/command-explain.ts +++ b/src/cli/command-explain.ts @@ -4,7 +4,7 @@ import { normalizeCliCommandAlias, } from '@agent-device/command-registry/cli-command-aliases'; import { buildCommandUsage } from '../commands/schema/usage.ts'; -import type { DaemonCommandRoute } from '@agent-device/daemon/daemon-command-registry'; +import type { DaemonCommandRoute } from '@agent-device/command-registry/daemon-command-descriptor'; import { commandDescriptors, type Command } from '@agent-device/command-registry/registry'; import { ownerFilesForCommand } from '@agent-device/command-registry/owner-files'; import type { CommandTimeoutPolicy } from '@agent-device/command-registry/types'; diff --git a/src/cli/commands/__tests__/daemon.test.ts b/src/cli/commands/__tests__/daemon.test.ts index 0de3ad7158..b5e4b2a265 100644 --- a/src/cli/commands/__tests__/daemon.test.ts +++ b/src/cli/commands/__tests__/daemon.test.ts @@ -1,7 +1,7 @@ import assert from 'node:assert/strict'; import fs from 'node:fs'; import { afterEach, expect, test, vi } from 'vitest'; -import type { DaemonStopResult } from '@agent-device/daemon/daemon-stop'; +import type { DaemonStopResult } from '@agent-device/daemon-contracts/daemon-stop'; import { mkdtempForTestSync } from '@agent-device/test-kit/tmp-dir'; const mocks = vi.hoisted(() => ({ @@ -12,10 +12,10 @@ const mocks = vi.hoisted(() => ({ writeCommandOutput: vi.fn(), })); -// The mock specifier must be the SUT's exact `@agent-device/daemon/daemon-stop`: the +// The mock specifier must be the SUT's exact `@agent-device/daemon-contracts/daemon-stop`: the // mutation lane aliases only the manifest's exact entries, so the `./testing/*` pattern // form resolved outside the Stryker sandbox and the mock silently missed (#3546 CI). -vi.mock('@agent-device/daemon/daemon-stop', () => ({ +vi.mock('@agent-device/daemon-contracts/daemon-stop', () => ({ readDaemonStopIdentity: mocks.readDaemonStopIdentity, stopDaemon: mocks.stopDaemon, })); diff --git a/src/cli/commands/daemon.ts b/src/cli/commands/daemon.ts index c8fa927af0..0cac73a814 100644 --- a/src/cli/commands/daemon.ts +++ b/src/cli/commands/daemon.ts @@ -3,7 +3,7 @@ import { readDaemonStopIdentity, stopDaemon, type DaemonStopResult, -} from '@agent-device/daemon/daemon-stop'; +} from '@agent-device/daemon-contracts/daemon-stop'; import { readDaemonShutdownReport } from '@agent-device/daemon-contracts/daemon-shutdown-report'; import { AppError } from '@agent-device/kernel/errors'; import { writeCommandOutput } from './shared.ts'; diff --git a/src/cli/commands/device-release.ts b/src/cli/commands/device-release.ts deleted file mode 100644 index 5259ea26d1..0000000000 --- a/src/cli/commands/device-release.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { - createOwnerScopedDeviceClaimReconciler, - type OwnerScopedClaimRecoveryComposer, -} from '@agent-device/daemon/device/device-claim-owner-recovery'; -import { createClaimRecoveryGateway } from '../../platform-runtime-claim-recovery.ts'; -import { createDaemonRecoveryPlatformScope } from '@agent-device/daemon/platform-request-scope'; -import { - releaseProvenStaleDeviceClaims, - type DeviceClaimStaleReleaseOutcome, -} from '@agent-device/daemon/device/device-claims'; -import type { DeviceClaimSelectors } from '@agent-device/daemon/device/device-claim-inspection'; - -/** - * Daemonless `device release --stale`: settles each provably dead owner - * through the same owner-scoped reconciliation the daemon uses at `open` and - * startup — recovery composed from the stale claim's own state dir, resources - * settled first, claim cleared last — without requiring the (typically dead) - * owner daemon, or any daemon, to be running. - */ -export async function runStaleDeviceClaimRelease( - selectors: DeviceClaimSelectors, - composeRecovery?: OwnerScopedClaimRecoveryComposer, -): Promise { - return await releaseProvenStaleDeviceClaims({ - selectors, - reconcile: createOwnerScopedDeviceClaimReconciler({ - scope: createDaemonRecoveryPlatformScope(), - composeGateway: createClaimRecoveryGateway, - composeRecovery, - }), - }); -} diff --git a/src/cli/commands/device.ts b/src/cli/commands/device.ts index de6c307eaf..15b2675e0f 100644 --- a/src/cli/commands/device.ts +++ b/src/cli/commands/device.ts @@ -5,8 +5,8 @@ import { deviceClaimRequiresStaleInspection, inspectDeviceClaims, type InspectedDeviceClaim, -} from '@agent-device/daemon/device/device-claim-inspection'; -import type { DeviceClaimStaleReleaseOutcome } from '@agent-device/daemon/device/device-claims'; + type DeviceClaimStaleReleaseOutcome, +} from '@agent-device/daemon-contracts/device-claims'; import { shellQuoteIfNeeded } from '@agent-device/kernel/device-shell'; import { writeCommandOutput } from './shared.ts'; import type { CliFlags } from '@agent-device/contracts/command'; @@ -65,7 +65,7 @@ async function runDeviceRelease(flags: CliFlags): Promise { }, ); } - const { runStaleDeviceClaimRelease } = await import('./device-release.ts'); + const { runStaleDeviceClaimRelease } = await import('../../platform-runtime-claim-recovery.ts'); const outcomes = await runStaleDeviceClaimRelease({ platform: flags.platform, device: flags.device, diff --git a/src/platform-runtime-app-log-output.ts b/src/platform-runtime-app-log-output.ts index 1c8845211e..e0e1d3346b 100644 --- a/src/platform-runtime-app-log-output.ts +++ b/src/platform-runtime-app-log-output.ts @@ -1,8 +1,8 @@ +import { ensureAppLogPath } from '@agent-device/capture-kit/app-log-files'; import type { AppLogOutputSink } from '@agent-device/contracts/app-log-runtime'; import fs from 'node:fs'; import { openVerifiedFileForAppend, openVerifiedFileForRead } from '@agent-device/host-kit/file'; -import { ensureAppLogPath } from '@agent-device/daemon/app-log-files'; -import { requireManagedSessionArtifactPath } from '@agent-device/daemon/managed-session-artifact-path'; +import { requireManagedSessionArtifactPath } from '@agent-device/daemon-contracts/session-artifact-paths'; export async function openAppLogOutput( sessionsDir: string, diff --git a/src/platform-runtime-app-log-process.ts b/src/platform-runtime-app-log-process.ts index 508e65aff3..f279ee811c 100644 --- a/src/platform-runtime-app-log-process.ts +++ b/src/platform-runtime-app-log-process.ts @@ -23,7 +23,7 @@ import { waitForProcessExit, } from '@agent-device/host-kit/process'; -import { requireManagedSessionArtifactPath } from '@agent-device/daemon/managed-session-artifact-path'; +import { requireManagedSessionArtifactPath } from '@agent-device/daemon-contracts/session-artifact-paths'; const APP_LOG_PID_FILENAME = 'app-log.pid'; diff --git a/src/platform-runtime-claim-recovery.ts b/src/platform-runtime-claim-recovery.ts index fbda2fe726..e63c5a6a33 100644 --- a/src/platform-runtime-claim-recovery.ts +++ b/src/platform-runtime-claim-recovery.ts @@ -1,5 +1,15 @@ +import { + createOwnerScopedDeviceClaimReconciler, + createRecoveryPlatformScope, + type ClaimRecoveryGatewayFactory, + type OwnerScopedClaimRecoveryComposer, +} from '@agent-device/daemon-contracts/device-claim-recovery'; +import { + releaseProvenStaleDeviceClaims, + type DeviceClaimSelectors, + type DeviceClaimStaleReleaseOutcome, +} from '@agent-device/daemon-contracts/device-claims'; import { createPlatformRuntimeGateway } from './platform-runtime.ts'; -import type { ClaimRecoveryGatewayFactory } from '@agent-device/daemon/device/claim-recovery-gateway'; /** * Root composition for owner-scoped claim recovery (ADR 0019 section 1): the daemon owns the @@ -15,3 +25,24 @@ export const createClaimRecoveryGateway: ClaimRecoveryGatewayFactory = (input) = ownedProcesses: input.ownedProcesses, resolveSessionArtifacts: input.resolveSessionArtifacts, }); + +/** + * Daemonless `device release --stale`: settles each provably dead owner + * through the same owner-scoped reconciliation the daemon uses at `open` and + * startup — recovery composed from the stale claim's own state dir, resources + * settled first, claim cleared last — without requiring the (typically dead) + * owner daemon, or any daemon, to be running. + */ +export async function runStaleDeviceClaimRelease( + selectors: DeviceClaimSelectors, + composeRecovery?: OwnerScopedClaimRecoveryComposer, +): Promise { + return await releaseProvenStaleDeviceClaims({ + selectors, + reconcile: createOwnerScopedDeviceClaimReconciler({ + scope: createRecoveryPlatformScope(), + composeGateway: createClaimRecoveryGateway, + composeRecovery, + }), + }); +} diff --git a/src/remote/remote-request-diagnostics.ts b/src/remote/remote-request-diagnostics.ts index a4b60e3745..076c9535ce 100644 --- a/src/remote/remote-request-diagnostics.ts +++ b/src/remote/remote-request-diagnostics.ts @@ -17,6 +17,7 @@ * on a request that already failed, so it must not turn one failure into two. */ +import { resolveRemoteRequestDiagnosticsPath } from '@agent-device/daemon-contracts/session-artifact-paths'; import fs from 'node:fs'; import path from 'node:path'; import type { DaemonError, DiagnosticsRecordRef } from '@agent-device/kernel/errors'; @@ -27,7 +28,6 @@ import { buildDaemonHttpTenantHeaders, buildDaemonHttpUrl, } from '@agent-device/contracts/daemon-http'; -import { resolveRemoteRequestDiagnosticsPath } from '@agent-device/daemon/session-artifact-paths'; const REMOTE_DIAGNOSTICS_FETCH_TIMEOUT_MS = 10_000; const REMOTE_DIAGNOSTICS_MAX_BYTES = 8 * 1024 * 1024; diff --git a/test/integration/nightly/concurrency-torture/claim-registry.ts b/test/integration/nightly/concurrency-torture/claim-registry.ts index e58319caef..468148ac87 100644 --- a/test/integration/nightly/concurrency-torture/claim-registry.ts +++ b/test/integration/nightly/concurrency-torture/claim-registry.ts @@ -1,4 +1,4 @@ -// In-memory model of the enforced host-global device claim (`packages/daemon/src/resources/device/device-claims.ts`). +// In-memory model of the enforced host-global device claim (`packages/daemon-contracts/src/device-claims.ts`). // // The production claim is a filesystem lock file guarded by a process lock, keyed // by `canonicalLocalDeviceKey`. That is real I/O and a real OS lock — neither is diff --git a/test/integration/provider-scenarios/remote-proxy-parity.test.ts b/test/integration/provider-scenarios/remote-proxy-parity.test.ts index 992d824cf0..b6d065d569 100644 --- a/test/integration/provider-scenarios/remote-proxy-parity.test.ts +++ b/test/integration/provider-scenarios/remote-proxy-parity.test.ts @@ -1,3 +1,4 @@ +import { resolveSessionRequestLogPath } from '@agent-device/daemon-contracts/session-artifact-paths'; import type { DaemonWireRequest } from '@agent-device/contracts/command'; import type { DaemonResponse } from '@agent-device/kernel/contracts'; import assert from 'node:assert/strict'; @@ -9,7 +10,6 @@ import { test } from 'vitest'; import { sendToDaemon } from '../../../src/daemon-client/daemon-client.ts'; import { LeaseRegistry } from '@agent-device/daemon/testing/lease-registry'; import { createDaemonHttpServer } from '@agent-device/daemon/testing/server/http-server'; -import { resolveSessionRequestLogPath } from '@agent-device/daemon/session-artifact-paths'; import { createDaemonProxyServer } from '@agent-device/proxy'; import { AppError, type DaemonError } from '@agent-device/kernel/errors'; import { diff --git a/test/integration/provider-scenarios/remote-proxy-request-diagnostics.test.ts b/test/integration/provider-scenarios/remote-proxy-request-diagnostics.test.ts index 2ed03f1904..428bfe2aeb 100644 --- a/test/integration/provider-scenarios/remote-proxy-request-diagnostics.test.ts +++ b/test/integration/provider-scenarios/remote-proxy-request-diagnostics.test.ts @@ -11,6 +11,7 @@ * over `/rpc`. */ +import { resolveSessionRequestLogPath } from '@agent-device/daemon-contracts/session-artifact-paths'; import type { DaemonResponse } from '@agent-device/kernel/contracts'; import assert from 'node:assert/strict'; import fs from 'node:fs'; @@ -19,7 +20,6 @@ import { test } from 'vitest'; import { createDaemonHttpServer } from '@agent-device/daemon/testing/server/http-server'; import { createDaemonProxyServer } from '@agent-device/proxy'; import { localizeRemoteDaemonError } from '../../../src/remote/remote-request-diagnostics.ts'; -import { resolveSessionRequestLogPath } from '@agent-device/daemon/session-artifact-paths'; import { safeSessionName } from '@agent-device/host-kit/session-paths'; import type { DaemonError } from '@agent-device/kernel/errors'; import {