Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions docs/adr/0019-request-bound-platform-runtime.md
Original file line number Diff line number Diff line change
Expand Up @@ -827,6 +827,25 @@ matching the layering scanner's scope) to `src/platforms/**` and concrete
`@agent-device/platform-*` packages. R13 governs concrete package imports across the whole tree,
while `retired-platforms-zone` prevents the old root seam from being recreated; platform freedom is
therefore structurally enforced rather than periodically measured.
### 11. The platform-services port carries host asks that bind no device

Some request-path work asks the host machine something without ever taking a device runtime:
concrete local readiness, boot-time and runner-session observation, and open-target
classification. Those asks now cross the daemon boundary as one required dependency,
`DaemonPlatformServices` (`src/daemon/platform-services.ts`), composed by the root in
`src/platform-runtime-daemon-services.ts` and threaded root → `RequestRouterDeps` → the request
execution scope → the handlers and consumers.

The port is not a raw escape hatch back into platform mechanics. Its observation members are the
already-published neutral contract types from `@agent-device/contracts`, readiness is a boolean,
and the three open-target members are questions over neutral inputs (`DeviceInfo`, flag strings,
`SessionSurface`) declared at the port itself — none of them carries a raw selector, gesture, or
runner call, and no platform type appears in a signature; device
execution stays on the request-bound `RequestExecutionScope.bindDevice` path, whose owner facts
remain the only admission authority. Its members are also all required: there is no throwing
placeholder an un-composed process silently runs on — a daemon process either receives the root's
composition or cannot start, and a test passes the same shared fixture rather than mocking a
platform module behind the port.

## Relationship to prior decisions

Expand Down
77 changes: 37 additions & 40 deletions scripts/layering/daemon-platform-runtime-inventory.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,11 @@ import {
isRootPlatformRuntimeTarget,
} from './daemon-platform-runtime-inventory.ts';

const DEVICE_READY_TARGET = 'src/platform-runtime-device-ready.ts';
const DEVICE_READY_STUB =
'export async function ensureLocalPlatformDeviceReady(device: unknown) { return false; }\n';
// The classified pair these fixtures plant is the platform-services composition factory: the
// edge this port introduced, and the shortest one whose symbols the rule still classifies.
const CLASSIFIED_TARGET = 'src/platform-runtime-daemon-services.ts';
const CLASSIFIED_STUB = 'export function createDaemonPlatformServices() { return {}; }\n';
const CLASSIFIED_IMPORTER = 'src/daemon/server/daemon-runtime.ts';

function violations(sources: Record<string, string>): LayeringViolation[] {
return checkDaemonPlatformRuntimeInventory(resolveImportEdges(new Map(Object.entries(sources))));
Expand All @@ -22,12 +24,12 @@ function edgeViolations(sources: Record<string, string>, file: string): Layering

test('R76 accepts a classified edge with the exact recorded symbols', () => {
const sources = {
[DEVICE_READY_TARGET]: DEVICE_READY_STUB,
'src/daemon/device/device-ready.ts':
"import { ensureLocalPlatformDeviceReady } from '../../platform-runtime-device-ready.ts';\n" +
'void ensureLocalPlatformDeviceReady;\n',
[CLASSIFIED_TARGET]: CLASSIFIED_STUB,
[CLASSIFIED_IMPORTER]:
"import { createDaemonPlatformServices } from '../../platform-runtime-daemon-services.ts';\n" +
'void createDaemonPlatformServices;\n',
};
assert.deepEqual(edgeViolations(sources, 'src/daemon/device/device-ready.ts'), []);
assert.deepEqual(edgeViolations(sources, CLASSIFIED_IMPORTER), []);
});

for (const [file, target, symbol] of [
Expand All @@ -50,20 +52,18 @@ for (const [file, target, symbol] of [

test('R76 reports every classified edge missing from the tree as stale, not the other way around', () => {
const sources = {
[DEVICE_READY_TARGET]: DEVICE_READY_STUB,
'src/daemon/device/device-ready.ts':
"import { ensureLocalPlatformDeviceReady } from '../../platform-runtime-device-ready.ts';\n" +
'void ensureLocalPlatformDeviceReady;\n',
[CLASSIFIED_TARGET]: CLASSIFIED_STUB,
[CLASSIFIED_IMPORTER]:
"import { createDaemonPlatformServices } from '../../platform-runtime-daemon-services.ts';\n" +
'void createDaemonPlatformServices;\n',
};
const stale = violations(sources).filter(
(violation) => violation.file === 'scripts/layering/daemon-platform-runtime-inventory.ts',
);
assert.equal(stale.length, DAEMON_PLATFORM_RUNTIME_EDGES.length - 1);
assert.ok(stale.every((violation) => violation.message.includes('stale classified edge')));
const deviceReadyStale = stale.find((violation) =>
violation.message.includes(DEVICE_READY_TARGET),
);
assert.equal(deviceReadyStale, undefined);
const classifiedStale = stale.find((violation) => violation.message.includes(CLASSIFIED_TARGET));
assert.equal(classifiedStale, undefined);
});

test('R76 rejects an unclassified edge with the pair and its line', () => {
Expand All @@ -86,35 +86,32 @@ test('R76 rejects an unclassified edge with the pair and its line', () => {

test('R76 rejects new symbols on a classified edge', () => {
const sources = {
[DEVICE_READY_TARGET]: DEVICE_READY_STUB + 'export function extraReadiness() {}\n',
'src/daemon/device/device-ready.ts':
"import { ensureLocalPlatformDeviceReady, extraReadiness } from '../../platform-runtime-device-ready.ts';\n" +
'void [ensureLocalPlatformDeviceReady, extraReadiness];\n',
[CLASSIFIED_TARGET]: CLASSIFIED_STUB + 'export function extraServicesKnob() {}\n',
[CLASSIFIED_IMPORTER]:
"import { createDaemonPlatformServices, extraServicesKnob } from '../../platform-runtime-daemon-services.ts';\n" +
'void [createDaemonPlatformServices, extraServicesKnob];\n',
};
const found = edgeViolations(sources, 'src/daemon/device/device-ready.ts');
const found = edgeViolations(sources, CLASSIFIED_IMPORTER);
assert.equal(found.length, 1);
assert.equal(found[0]!.rule, DAEMON_PLATFORM_RUNTIME_RULE);
assert.match(found[0]!.message, /classified symbols drifted/);
assert.match(found[0]!.message, /ensureLocalPlatformDeviceReady, extraReadiness/);
assert.match(found[0]!.message, /createDaemonPlatformServices, extraServicesKnob/);
});

test('R76 rejects a reintroduced Android-mechanics import on the selector-dispatch edge', () => {
test('R76 rejects a services object widened through an expanded composition edge', () => {
const sources = {
'src/platform-runtime-open-target.ts':
'export async function resolveSessionAppBundleIdForTarget() { return undefined; }\n' +
'export async function resolveAndroidPackageForOpen() { return undefined; }\n',
'src/daemon/handlers/session-selector-dispatch.ts':
"import { resolveAndroidPackageForOpen, resolveSessionAppBundleIdForTarget } from '../../platform-runtime-open-target.ts';\n" +
'void [resolveAndroidPackageForOpen, resolveSessionAppBundleIdForTarget];\n',
'src/platform-runtime-daemon-services.ts':
'export function createDaemonPlatformServices() { return {}; }\n' +
'export function createProviderPlatformServices() { return {}; }\n',
'src/daemon/server/daemon-runtime.ts':
"import {\n createDaemonPlatformServices,\n createProviderPlatformServices,\n} from '../../platform-runtime-daemon-services.ts';\n" +
'void [createDaemonPlatformServices, createProviderPlatformServices];\n',
};
const found = edgeViolations(sources, 'src/daemon/handlers/session-selector-dispatch.ts');
const found = edgeViolations(sources, 'src/daemon/server/daemon-runtime.ts');
assert.equal(found.length, 1);
assert.equal(found[0]!.rule, DAEMON_PLATFORM_RUNTIME_RULE);
assert.match(found[0]!.message, /classified symbols drifted/);
assert.match(
found[0]!.message,
/resolveAndroidPackageForOpen, resolveSessionAppBundleIdForTarget/,
);
assert.match(found[0]!.message, /createDaemonPlatformServices, createProviderPlatformServices/);
});

test('R76 matches a destructured dynamic import by target with the recorded bindings', () => {
Expand Down Expand Up @@ -211,13 +208,13 @@ test('R76 rejects a namespace import alongside the recorded named binding on the

test('R76 treats the import and re-export of one classified pair as one entry', () => {
const sources = {
[DEVICE_READY_TARGET]: DEVICE_READY_STUB,
'src/daemon/device/device-ready.ts':
"import { ensureLocalPlatformDeviceReady } from '../../platform-runtime-device-ready.ts';\n" +
"export { ensureLocalPlatformDeviceReady } from '../../platform-runtime-device-ready.ts';\n" +
'void ensureLocalPlatformDeviceReady;\n',
[CLASSIFIED_TARGET]: CLASSIFIED_STUB,
[CLASSIFIED_IMPORTER]:
"import { createDaemonPlatformServices } from '../../platform-runtime-daemon-services.ts';\n" +
"export { createDaemonPlatformServices } from '../../platform-runtime-daemon-services.ts';\n" +
'void createDaemonPlatformServices;\n',
};
assert.deepEqual(edgeViolations(sources, 'src/daemon/device/device-ready.ts'), []);
assert.deepEqual(edgeViolations(sources, CLASSIFIED_IMPORTER), []);
});

test('R76 ignores test-shaped and non-daemon importers', () => {
Expand Down
74 changes: 8 additions & 66 deletions scripts/layering/daemon-platform-runtime-inventory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -133,16 +133,6 @@ export const DAEMON_PLATFORM_RUNTIME_EDGES: readonly DaemonPlatformRuntimeEdge[]
'(#2168); the process root cannot carry a per-claim sessionsDir, so the scoped ' +
"composition belongs to the recovery policy's own module.",
},
{
file: 'src/daemon/device/device-ready.ts',
target: 'src/platform-runtime-device-ready.ts',
symbols: ['ensureLocalPlatformDeviceReady'],
classification: 'composition-essential',
rationale:
'neutral local-device-readiness port assembled at the root composition layer (the ' +
'platform dispatch is internal to the root module); the daemon keeps its TTL cache ' +
'and provider-device policy locally.',
},
{
file: 'src/daemon/direct-ios-selector.ts',
target: 'src/platform-runtime-apple-resources.ts',
Expand All @@ -154,54 +144,6 @@ export const DAEMON_PLATFORM_RUNTIME_EDGES: readonly DaemonPlatformRuntimeEdge[]
'audit deliberately adds no second selector producer.',
deepenedBy: '#2273, #2274',
},
{
file: 'src/daemon/ios-app-session-hint.ts',
target: 'src/platform-runtime-apple-resources.ts',
symbols: ['appleSessionObservation'],
classification: 'daemon-policy-essential',
rationale:
'daemon-owned hint composition and length limits consume the neutral foreground-app ' +
'observation; the Apple package owns ambiguity and probe mechanics.',
},
{
file: 'src/daemon/request-recording-health.ts',
target: 'src/platform-runtime-apple-resources.ts',
symbols: ['appleSessionObservation'],
classification: 'daemon-policy-essential',
rationale:
'daemon-owned recording invalidation consumes only liveness and session identity ' +
'through the neutral observation contract; runner mechanics stay Apple-owned.',
},
{
file: 'src/daemon/session-device-resolution.ts',
target: 'src/platform-runtime-apple-resources.ts',
symbols: ['appleSessionObservation'],
classification: 'daemon-policy-essential',
rationale:
'daemon-owned device refresh uses the neutral runner-session observation as boot ' +
'evidence; inventory selection and provider exclusions remain local policy.',
},
{
file: 'src/daemon/session-lifecycle/internal/session-open-execution.ts',
target: 'src/platform-runtime-device-boot.ts',
symbols: ['deviceBootObservation'],
classification: 'daemon-policy-essential',
rationale:
'daemon-owned claim reconciliation asks the device when it last booted to decide whether a ' +
'foreign claim can still describe live ownership; the per-family probe mechanics stay in the ' +
'Apple and Android packages behind the neutral observation contract (#2538).',
},
{
file: 'src/daemon/handlers/session-selector-dispatch.ts',
target: 'src/platform-runtime-open-target.ts',
symbols: ['resolveSessionAppBundleIdForTarget'],
classification: 'daemon-policy-essential',
rationale:
'selector dispatch reconstructs the session app-bundle identity after a trigger-app-event ' +
'deep link through the one neutral open-plan resolver (#2334); Android package resolution ' +
'moved behind the Android owning seam in packages/platform-android, so the resolver is the ' +
'only symbol this edge names.',
},
{
file: 'src/daemon/server/daemon-runtime.ts',
target: 'src/core/interactors.ts',
Expand Down Expand Up @@ -236,15 +178,15 @@ export const DAEMON_PLATFORM_RUNTIME_EDGES: readonly DaemonPlatformRuntimeEdge[]
'src/daemon/provider-device-admission.ts, so this edge is the composition and nothing else.',
},
{
file: 'src/daemon/session-lifecycle/internal/session-open-prepare.ts',
target: 'src/platform-runtime-open-target.ts',
symbols: ['resolveRequestedOpenSurface', 'validateOpenRelaunchTarget'],
classification: 'daemon-policy-essential',
file: 'src/daemon/server/daemon-runtime.ts',
target: 'src/platform-runtime-daemon-services.ts',
symbols: ['createDaemonPlatformServices'],
classification: 'composition-essential',
rationale:
'open-prepare policy consumes only the neutral open plan/result surface (#2334): surface ' +
'classification and relaunch-target validation. The platform mechanics that used to share ' +
'the file (Android package resolution) moved behind the Android owning seam, leaving this ' +
'edge daemon policy over two neutral, non-mechanics functions.',
'process-root assembly of the daemon platform-services port (ADR 0019): local readiness, ' +
'boot and runner-session observation, and open-target classification reach the request ' +
'path only as this frozen object, so no daemon module names a platform adapter. Device ' +
'execution never enters the port; it stays behind the request-bound runtime binding.',
},
] as const;

Expand Down
1 change: 1 addition & 0 deletions scripts/layering/root-module-zones.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ export const ROOT_MODULE_ZONES: Readonly<Record<string, readonly string[]>> = {
'src/platform-runtime-apple-runner-owner.ts',
'src/platform-runtime-daemon-lifecycle.ts',
'src/platform-runtime-daemon-owner-cleanup.ts',
'src/platform-runtime-daemon-services.ts',
'src/platform-runtime-device-boot.ts',
'src/platform-runtime-device-inventory.ts',
'src/platform-runtime-device-ready.ts',
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { daemonPlatformServicesFixture } from '../../../daemon/__tests__/platform-services-fixture.ts';
// Characterization of the shipped reporter contract for the MAESTRO adapter (#1478 P3).
//
// The sibling session-test-reporter-values.test.ts covers the same contract for native `.ad`.
Expand Down Expand Up @@ -112,6 +113,7 @@ async function runMaestroSuiteThroughReporter(params: {
(event) => runReplayTestReporterProgress(reporters, event, reporterContext),
async () =>
await handleSessionCommands({
platformServices: daemonPlatformServicesFixture(),
req: {
token: 't',
session: 'default',
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { daemonPlatformServicesFixture } from '../../../daemon/__tests__/platform-services-fixture.ts';
// Characterization of the shipped reporter contract across the scheduler seam (#1478 P3).
//
// P3 moves the scheduler into `packages/replay-test` and makes attempt identity
Expand Down Expand Up @@ -102,6 +103,7 @@ async function runSuiteThroughReporter(params: {
(event) => runReplayTestReporterProgress(reporters, event, reporterContext),
async () =>
await handleSessionCommands({
platformServices: daemonPlatformServicesFixture(),
req: {
token: 't',
session: 'default',
Expand Down
3 changes: 3 additions & 0 deletions src/daemon/__tests__/back-runtime.test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { daemonPlatformServicesFixture } from './platform-services-fixture.ts';
import { expect, test, vi } from 'vitest';
import { backRuntimeOperationFacts } from '@agent-device/contracts/back-runtime';
import {
Expand Down Expand Up @@ -90,6 +91,7 @@ test('resolves one admitted binding and drives one back navigation', async () =>
const harness = runtimeHarness(backRuntimeOperationFacts({ back: available }).back);

const resolved = await resolveBoundBackRuntime({
platformServices: daemonPlatformServicesFixture(),
device: appleDevice,
inspectFacts: harness.inspectFacts,
bindDevice: harness.bindDevice,
Expand All @@ -113,6 +115,7 @@ test('forwards the requested back mode from the resolved dispatch context', asyn
const harness = runtimeHarness();

const resolved = await resolveBoundBackRuntime({
platformServices: daemonPlatformServicesFixture(),
device: appleDevice,
inspectFacts: harness.inspectFacts,
bindDevice: harness.bindDevice,
Expand Down
4 changes: 4 additions & 0 deletions src/daemon/__tests__/focus-runtime.test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { daemonPlatformServicesFixture } from './platform-services-fixture.ts';
import { expect, test, vi } from 'vitest';
import { focusRuntimeOperationFacts } from '@agent-device/contracts/focus-runtime';
import {
Expand Down Expand Up @@ -86,6 +87,7 @@ test('resolves one admitted binding and exposes one point focus', async () => {
const harness = runtimeHarness(focusRuntimeOperationFacts({ focus: available }).focusPoint);

const resolved = await resolveBoundFocusRuntime({
platformServices: daemonPlatformServicesFixture(),
device: appleDevice,
positionals: ['40', '90'],
inspectFacts: harness.inspectFacts,
Expand Down Expand Up @@ -123,6 +125,7 @@ test('forwards the request context the retired leaf passed through its runner co
const harness = runtimeHarness();

const resolved = await resolveBoundFocusRuntime({
platformServices: daemonPlatformServicesFixture(),
device: appleDevice,
positionals: ['40', '90'],
inspectFacts: harness.inspectFacts,
Expand Down Expand Up @@ -159,6 +162,7 @@ test('rejects a missing coordinate before inspection or binding', async () => {

await expect(
resolveBoundFocusRuntime({
platformServices: daemonPlatformServicesFixture(),
device: appleDevice,
positionals: ['40'],
inspectFacts: harness.inspectFacts,
Expand Down
6 changes: 6 additions & 0 deletions src/daemon/__tests__/fold-runtime.test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { daemonPlatformServicesFixture } from './platform-services-fixture.ts';
import { recordActionEntry } from '../session-action-recorder.ts';
import { expect, test, vi } from 'vitest';

Expand Down Expand Up @@ -103,6 +104,7 @@ test('resolves one admitted binding and reports the pose the owner read back', a
);

const resolved = await resolveBoundFoldRuntime({
platformServices: daemonPlatformServicesFixture(),
device: testDevice,
positionals: ['unfolded'],
inspectFacts: harness.inspectFacts,
Expand All @@ -126,6 +128,7 @@ test('resolves one admitted binding and reports the pose the owner read back', a
test('reports a pose without a panel reading when the owner could not name the lit panel', async () => {
const harness = runtimeHarness();
const resolved = await resolveBoundFoldRuntime({
platformServices: daemonPlatformServicesFixture(),
device: testDevice,
positionals: ['open'],
inspectFacts: harness.inspectFacts,
Expand All @@ -145,6 +148,7 @@ test('rejects an invalid pose before inspection or binding', async () => {
const harness = runtimeHarness();
await expect(
resolveBoundFoldRuntime({
platformServices: daemonPlatformServicesFixture(),
device: testDevice,
positionals: ['sideways'],
inspectFacts: harness.inspectFacts,
Expand Down Expand Up @@ -177,6 +181,7 @@ test('refuses a scoped simulator set on the route with the typed reason, never b
const harness = runtimeHarness(scopeRefusal, vi.fn(), scopedDevice);

const resolved = await resolveBoundFoldRuntime({
platformServices: daemonPlatformServicesFixture(),
device: scopedDevice,
positionals: ['half-open'],
inspectFacts: harness.inspectFacts,
Expand Down Expand Up @@ -206,6 +211,7 @@ test('passes the validated timed intent to the admitted fold owner', async () =>
{ atMs: 5000, angle: 100 },
];
const resolved = await resolveBoundFoldRuntime({
platformServices: daemonPlatformServicesFixture(),
device: testDevice,
positionals: [],
keyframes: JSON.stringify(keyframes),
Expand Down
Loading
Loading