Skip to content

Commit c794c11

Browse files
authored
refactor: close the daemon platform boundary (#2072)
* refactor: move runtime resource mechanics out of daemon * refactor: move Apple resource access out of daemon * chore: enforce the terminal daemon platform boundary
1 parent 3378d9c commit c794c11

51 files changed

Lines changed: 1399 additions & 301 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

packages/contracts/package.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -307,6 +307,10 @@
307307
"types": "./src/platform-runtime-operations.ts",
308308
"default": "./src/platform-runtime-operations.ts"
309309
},
310+
"./platform-resource-cleanup": {
311+
"types": "./src/platform-resource-cleanup.ts",
312+
"default": "./src/platform-resource-cleanup.ts"
313+
},
310314
"./platform-runtime-unavailable": {
311315
"types": "./src/platform-runtime-unavailable.ts",
312316
"default": "./src/platform-runtime-unavailable.ts"
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import type { DeviceInfo } from '@agent-device/kernel/device';
2+
3+
/** Platform-owned resource finalization invoked by neutral daemon orchestration. */
4+
export type PlatformResourceCleanup = Readonly<{
5+
stopSnapshotHelper(device: DeviceInfo): Promise<void>;
6+
closeManagedBrowser(
7+
params: Readonly<{
8+
device: DeviceInfo;
9+
sessionName: string;
10+
stateDir: string;
11+
openSessionNames: () => readonly string[];
12+
}>,
13+
): Promise<void>;
14+
cleanupSessionlessExecutionHost(device: DeviceInfo): Promise<void>;
15+
retainExecutionHostAfterClose(params: {
16+
device: DeviceInfo;
17+
shutdownRequested: boolean;
18+
hasScreenRecording: boolean;
19+
hasLease: boolean;
20+
}): boolean;
21+
}>;

scripts/__tests__/test-file-size-ratchet.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ const TRIPWIRE_LINES = 1_000;
3434
// Exact current lengths. Lower a pin when its file shrinks; never raise one — extract instead.
3535
const PINNED_TEST_FILE_LINES: Readonly<Record<string, number>> = Object.freeze({
3636
'src/__tests__/remote-connection.test.ts': 2973,
37-
'src/daemon/handlers/__tests__/snapshot-handler.test.ts': 2284,
37+
'src/daemon/handlers/__tests__/snapshot-handler.test.ts': 2242,
3838
'src/commands/interaction/runtime/settle.test.ts': 2359,
3939
'src/daemon/handlers/__tests__/session-replay-runtime-maestro.test.ts': 1963,
4040
'packages/platform-apple/src/runner/__tests__/runner-session.test.ts': 1957,

scripts/layering/check.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,9 @@
1919
// an import whose source zone outranks its target zone, plus a ratchet on the
2020
// same inversion measured over TYPE-ONLY edges (R6).
2121
// - Over the DAEMON only: SessionState field ownership (R7), because the session
22-
// record is store-owned mutable state that any daemon module can write.
22+
// record is store-owned mutable state that any daemon module can write; and the terminal
23+
// concrete-platform boundary (R65), which rejects every import form into src/platforms or a
24+
// platform package.
2325
// - Over the TYPE GRAPH: the largest type-level import cycle is pinned by
2426
// equality (R9). R4 keeps the value graph acyclic, so these cycles are free at
2527
// runtime but bound what can be read in isolation; growth fails, and so does a
@@ -98,6 +100,7 @@ import { policyLead, policyViolation, ZONE_POLICIES } from './zone-policy.ts';
98100
import { contractsImplementationAuthorityViolations } from './contracts-implementation-policy.ts';
99101
import { selectorPipelineOwnershipViolations } from './selector-pipeline-ownership.ts';
100102
import { recordRuntimeRegistryJoinViolations } from './record-runtime-registry-policy.ts';
103+
import { checkDaemonPlatformBoundary } from './daemon-platform-boundary.ts';
101104
import { listTrackedProductionSources, listTrackedTypeScriptFiles } from './tracked-sources.ts';
102105

103106
const repoRoot = execFileSync('git', ['rev-parse', '--show-toplevel'], {
@@ -485,7 +488,9 @@ function report(
485488
`inside its declared owner (R7); the largest type-level cycle is ${typeCycle} files ` +
486489
`(R9); ${daemonModularitySummary()}; ` +
487490
`${packageBoundariesSummary(repoRoot)}; ${platformPackagePolicySummary()}; ` +
488-
`${runtimeCommandCutoverSummary()}; and bin.ts imports normalizeCliCommandAlias, ` +
491+
`${runtimeCommandCutoverSummary()}; R65 keeps production src/daemon free of concrete ` +
492+
`platform imports in every executable and type-only form; and bin.ts imports ` +
493+
`normalizeCliCommandAlias, ` +
489494
`actually passes it into buildCommandUsageText, and holds no local alias literals ` +
490495
`(R12).\n`,
491496
);
@@ -543,6 +548,7 @@ export const LAYERING_RULE_IDS = [
543548
'type-spine-inversions',
544549
'session-state-ownership',
545550
'daemon-modularity-ratchets',
551+
'daemon-platform-boundary',
546552
'bin-alias-fast-path',
547553
'package-boundaries',
548554
'platform-package-policy',
@@ -564,6 +570,8 @@ export const LAYERING_RULES: Readonly<Record<LayeringRuleId, LayeringRule>> = {
564570
'session-state-ownership': (context) => checkSessionStateOwnership(context.sources),
565571
'daemon-modularity-ratchets': (context) =>
566572
checkDaemonModularityRatchets(context.edges, context.typeCycleMembers),
573+
'daemon-platform-boundary': (context) =>
574+
checkDaemonPlatformBoundary([...context.sources].map(([path, source]) => ({ path, source }))),
567575
'bin-alias-fast-path': (context) => checkBinAliasFastPath(context.sources),
568576
'package-boundaries': () => checkPackageBoundaries(repoRoot),
569577
'platform-package-policy': (context) =>
Lines changed: 292 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,292 @@
1+
import assert from 'node:assert/strict';
2+
import { test } from 'node:test';
3+
import {
4+
DAEMON_PLATFORM_BOUNDARY_RULE,
5+
daemonPlatformBoundaryViolations,
6+
findDaemonPlatformDependencies,
7+
} from './daemon-platform-boundary.ts';
8+
9+
const daemonFile = 'src/daemon/terminal-boundary-fixture.ts';
10+
11+
function dependencies(source: string, file = daemonFile) {
12+
return findDaemonPlatformDependencies([{ path: file, source }]);
13+
}
14+
15+
function violations(source: string, file = daemonFile) {
16+
return daemonPlatformBoundaryViolations([{ path: file, source }]);
17+
}
18+
19+
test('R65 rejects static, type-only, dynamic, type, and re-export dependencies with source lines', () => {
20+
const source = [
21+
"import { old } from '../platforms/android/old.ts';",
22+
"import type { OldType } from '../platforms/android/types.ts';",
23+
"const lazy = import('../platforms/android/lazy.ts');",
24+
"type ImportedType = import('../platforms/android/type.ts').ImportedType;",
25+
"export { old } from '../platforms/android/re-export.ts';",
26+
"export type { OldType } from '@agent-device/platform-android/types';",
27+
"export * from '@agent-device/platform-apple';",
28+
].join('\n');
29+
30+
assert.deepEqual(
31+
dependencies(source).map(({ kind, spec, line, target }) => ({ kind, spec, line, target })),
32+
[
33+
{
34+
kind: 'static import',
35+
spec: '../platforms/android/old.ts',
36+
line: 1,
37+
target: 'src/platforms/android/old.ts',
38+
},
39+
{
40+
kind: 'type-only import',
41+
spec: '../platforms/android/types.ts',
42+
line: 2,
43+
target: 'src/platforms/android/types.ts',
44+
},
45+
{
46+
kind: 'dynamic import',
47+
spec: '../platforms/android/lazy.ts',
48+
line: 3,
49+
target: 'src/platforms/android/lazy.ts',
50+
},
51+
{
52+
kind: 'type import',
53+
spec: '../platforms/android/type.ts',
54+
line: 4,
55+
target: 'src/platforms/android/type.ts',
56+
},
57+
{
58+
kind: 're-export',
59+
spec: '../platforms/android/re-export.ts',
60+
line: 5,
61+
target: 'src/platforms/android/re-export.ts',
62+
},
63+
{
64+
kind: 'type-only re-export',
65+
spec: '@agent-device/platform-android/types',
66+
line: 6,
67+
target: '@agent-device/platform-android/types',
68+
},
69+
{
70+
kind: 're-export',
71+
spec: '@agent-device/platform-apple',
72+
line: 7,
73+
target: '@agent-device/platform-apple',
74+
},
75+
],
76+
);
77+
78+
assert.deepEqual(
79+
violations(source).map(({ rule, file, line }) => ({ rule, file, line })),
80+
Array.from({ length: 7 }, (_, index) => ({
81+
rule: DAEMON_PLATFORM_BOUNDARY_RULE,
82+
file: daemonFile,
83+
line: index + 1,
84+
})),
85+
);
86+
});
87+
88+
test('R65 recognizes side-effect and aliased imports, including nested daemon paths', () => {
89+
const source = [
90+
"import '../../platforms/web/runtime.ts';",
91+
"import { runtime as platformRuntime } from '@agent-device/platform-web/runtime';",
92+
].join('\n');
93+
94+
assert.deepEqual(
95+
dependencies(source, 'src/daemon/handlers/terminal-boundary-fixture.ts').map((found) => ({
96+
kind: found.kind,
97+
line: found.line,
98+
spec: found.spec,
99+
target: found.target,
100+
})),
101+
[
102+
{
103+
kind: 'static import',
104+
line: 1,
105+
spec: '../../platforms/web/runtime.ts',
106+
target: 'src/platforms/web/runtime.ts',
107+
},
108+
{
109+
kind: 'static import',
110+
line: 2,
111+
spec: '@agent-device/platform-web/runtime',
112+
target: '@agent-device/platform-web/runtime',
113+
},
114+
],
115+
);
116+
});
117+
118+
test('R65 ignores comments, ordinary strings, unresolved dynamic imports, and lookalike names', () => {
119+
const source = [
120+
'const documentation = "import(\'../platforms/android/comment.ts\'); @agent-device/platform-android";',
121+
"// import { ignored } from '../platforms/android/comment.ts';",
122+
"const packageName = '@agent-device/platform-android';",
123+
"const relativeName = '../platforms/android/not-an-import.ts';",
124+
'const computed = import(platformSpecifier);',
125+
"import '../platforms-sibling/not-platform.ts';",
126+
"import '@agent-device/platforms';",
127+
"import '@agent-device/platform';",
128+
].join('\n');
129+
130+
assert.deepEqual(dependencies(source), []);
131+
});
132+
133+
test('R65 rejects require, import-equals, and template-literal type imports', () => {
134+
const source = [
135+
"const android = require('@agent-device/platform-android');",
136+
'const runtime = require(`../platforms/android/runtime.ts`);',
137+
"import web = require('@agent-device/platform-web');",
138+
'type Apple = import(`@agent-device/platform-apple`).Apple;',
139+
'type Android = import(`../platforms/android/types.ts`).Android;',
140+
].join('\n');
141+
142+
assert.deepEqual(
143+
dependencies(source).map(({ kind, spec, line }) => ({ kind, spec, line })),
144+
[
145+
{ kind: 'require', spec: '@agent-device/platform-android', line: 1 },
146+
{ kind: 'require', spec: '../platforms/android/runtime.ts', line: 2 },
147+
{ kind: 'import equals', spec: '@agent-device/platform-web', line: 3 },
148+
{ kind: 'type import', spec: '@agent-device/platform-apple', line: 4 },
149+
{ kind: 'type import', spec: '../platforms/android/types.ts', line: 5 },
150+
],
151+
);
152+
});
153+
154+
test('R65 folds statically constructed dynamic platform specifiers', () => {
155+
const source = [
156+
"const apple = import('@agent-device/' + 'platform-apple');",
157+
'const android = import(`../platforms/${"android"}/runtime.ts`);',
158+
"const wrapped = import((('@agent-device/' + 'platform-android')));",
159+
'const required = require((`@agent-device/platform-web`));',
160+
].join('\n');
161+
162+
assert.deepEqual(
163+
dependencies(source).map(({ kind, spec, line }) => ({ kind, spec, line })),
164+
[
165+
{ kind: 'dynamic import', spec: '@agent-device/platform-apple', line: 1 },
166+
{ kind: 'dynamic import', spec: '../platforms/android/runtime.ts', line: 2 },
167+
{ kind: 'dynamic import', spec: '@agent-device/platform-android', line: 3 },
168+
{ kind: 'require', spec: '@agent-device/platform-web', line: 4 },
169+
],
170+
);
171+
});
172+
173+
test('R65 unwraps erased TypeScript expressions around executable specifiers', () => {
174+
const source = [
175+
"void import('@agent-device/platform-android' as string);",
176+
"require('@agent-device/platform-web' satisfies string);",
177+
"void import(<string>'../platforms/apple/runtime.ts');",
178+
].join('\n');
179+
180+
assert.deepEqual(
181+
dependencies(source).map(({ kind, spec, line }) => ({ kind, spec, line })),
182+
[
183+
{ kind: 'dynamic import', spec: '@agent-device/platform-android', line: 1 },
184+
{ kind: 'require', spec: '@agent-device/platform-web', line: 2 },
185+
{ kind: 'dynamic import', spec: '../platforms/apple/runtime.ts', line: 3 },
186+
],
187+
);
188+
});
189+
190+
test('R65 follows common createRequire and require aliases', () => {
191+
const source = [
192+
"import { createRequire as makeRequire } from 'node:module';",
193+
"import * as moduleApi from 'node:module';",
194+
"import moduleDefault from 'node:module';",
195+
'const load = makeRequire(import.meta.url);',
196+
'const loadAgain = load;',
197+
"loadAgain('@agent-device/platform-android');",
198+
"makeRequire(import.meta.url)('../platforms/web/runtime.ts');",
199+
"moduleApi.createRequire(import.meta.url)('@agent-device/platform-apple');",
200+
"moduleDefault.createRequire(import.meta.url)('@agent-device/platform-web');",
201+
'const loadAlias = require;',
202+
"loadAlias('@agent-device/platform-linux');",
203+
"module.require('@agent-device/platform-vega');",
204+
"const { createRequire: fromCjs } = require('node:module');",
205+
"fromCjs(import.meta.url)('@agent-device/platform-harmonyos');",
206+
].join('\n');
207+
208+
assert.deepEqual(
209+
dependencies(source).map(({ spec, line }) => ({ spec, line })),
210+
[
211+
{ spec: '@agent-device/platform-android', line: 6 },
212+
{ spec: '../platforms/web/runtime.ts', line: 7 },
213+
{ spec: '@agent-device/platform-apple', line: 8 },
214+
{ spec: '@agent-device/platform-web', line: 9 },
215+
{ spec: '@agent-device/platform-linux', line: 11 },
216+
{ spec: '@agent-device/platform-vega', line: 12 },
217+
{ spec: '@agent-device/platform-harmonyos', line: 14 },
218+
],
219+
);
220+
});
221+
222+
test('R65 rejects triple-slash concrete-platform type references', () => {
223+
const source = [
224+
'/// <reference types="@agent-device/platform-android" />',
225+
'/// <reference path = "../platforms/apple/types.ts" />',
226+
'/*',
227+
'/// <reference types="@agent-device/platform-web" />',
228+
'*/',
229+
'export {};',
230+
].join('\n');
231+
assert.deepEqual(
232+
dependencies(source).map(({ kind, spec, line }) => ({ kind, spec, line })),
233+
[
234+
{ kind: 'type import', spec: '@agent-device/platform-android', line: 1 },
235+
{ kind: 'type import', spec: '../platforms/apple/types.ts', line: 2 },
236+
],
237+
);
238+
});
239+
240+
test('R65 rejects platform selection inside cleanup orchestrators', () => {
241+
const direct = violations(
242+
"export const cleanup = (session: any) => session.device.platform === 'android';",
243+
'src/daemon/session-teardown.ts',
244+
);
245+
assert.match(direct[0]?.message ?? '', /may not select a concrete platform/);
246+
247+
const predicate = violations(
248+
'export const cleanup = (device: any) => isIosFamily(device);',
249+
'src/daemon/handlers/snapshot-session.ts',
250+
);
251+
assert.match(predicate[0]?.message ?? '', /typed root-composed cleanup capability/);
252+
253+
const destructured = violations(
254+
"export const cleanup = (session: any) => { const { platform } = session.device; return platform === 'android'; };",
255+
'src/daemon/handlers/session-close-lifecycle-teardown.ts',
256+
);
257+
assert.match(destructured[0]?.message ?? '', /may not select a concrete platform/);
258+
259+
assert.deepEqual(
260+
violations(
261+
'export const cleanup = (owner: any, device: any) => owner.cleanupSessionlessExecutionHost(device);',
262+
'src/daemon/handlers/snapshot-session.ts',
263+
),
264+
[],
265+
);
266+
});
267+
268+
test('R65 ignores non-daemon and test-shaped records even when their syntax is red', () => {
269+
const source = "import { platform } from '../platforms/android/runtime.ts';";
270+
assert.deepEqual(dependencies(source, 'src/core/terminal-boundary-fixture.ts'), []);
271+
assert.deepEqual(dependencies(source, 'src/daemon/terminal-boundary-fixture.test.ts'), []);
272+
assert.deepEqual(dependencies(source, 'src/daemon/__tests__/terminal-boundary-fixture.ts'), []);
273+
});
274+
275+
test('R65 treats a relative specifier as legacy platform code only after path resolution', () => {
276+
const source = [
277+
"import { platform } from '../../platforms/android/runtime.ts';",
278+
"import { sibling } from '../platforms-sibling/runtime.ts';",
279+
"import { exact } from '../platforms';",
280+
].join('\n');
281+
282+
assert.deepEqual(
283+
dependencies(source).map(({ spec, target, line }) => ({ spec, target, line })),
284+
[
285+
{
286+
spec: '../platforms',
287+
target: 'src/platforms',
288+
line: 3,
289+
},
290+
],
291+
);
292+
});

0 commit comments

Comments
 (0)