Skip to content

Commit f8eddcb

Browse files
thymikeeclaude
andauthored
chore(gates): forbid process globals outside declared entry points (#3753) (#3823)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
1 parent 7faa04e commit f8eddcb

3 files changed

Lines changed: 403 additions & 1 deletion

File tree

‎scripts/layering/check.ts‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,7 @@ import { recordRuntimeDaemonMechanicsViolations } from './record-runtime-mechani
139139
import { checkDaemonPlatformBoundary } from './daemon-platform-boundary.ts';
140140
import { checkDaemonPlatformRuntimeInventory } from './daemon-platform-runtime-inventory.ts';
141141
import { checkDaemonClientEntry } from './daemon-client-entry.ts';
142+
import { processGlobalsViolations } from './process-globals-policy.ts';
142143
import { checkDaemonProcessReach } from './daemon-process-reach.ts';
143144
import {
144145
checkPackageExportSurface,
@@ -453,7 +454,8 @@ function report(
453454
`concrete platform imports in every executable and type-only form; ` +
454455
`no production daemon file reaches the root platform-runtime family or a hub that ` +
455456
`does (R76); the daemon process reaches no caller-side replay source module over ` +
456-
`static or dynamic edges (R82); ` +
457+
`static or dynamic edges (R82); process globals stay at declared entries and ` +
458+
`command-host reads stay at command-execution/spawn boundaries (R86); ` +
457459
`${packageExportSurfaceSummary(ratchets.testOnlyExports, reference.testOnlyExports)}; ` +
458460
`${testColocationSummary(ratchets.unmatchedTests, reference.unmatchedTests)}; and ` +
459461
`the handler-owned SessionState/SessionStore ` +
@@ -539,6 +541,7 @@ export const LAYERING_RULE_IDS = [
539541
'provider-snapshot-presentation-ownership',
540542
'snapshot-assembly-presentation-neutrality',
541543
'daemon-platform-runtime-inventory',
544+
'process-globals',
542545
'daemon-process-reach',
543546
'daemon-client-entry',
544547
'session-authority-overlay',
@@ -607,6 +610,7 @@ export const LAYERING_RULES: Readonly<Record<LayeringRuleId, LayeringRule>> = {
607610
snapshotAssemblyPresentationViolations(context.sources, context.edges),
608611
'daemon-platform-runtime-inventory': (context) =>
609612
checkDaemonPlatformRuntimeInventory(context.edges),
613+
'process-globals': (context) => processGlobalsViolations(context.allTypeScriptSources),
610614
'daemon-process-reach': (context) => checkDaemonProcessReach(context.sourceFiles, context.edges),
611615
'daemon-client-entry': (context) => checkDaemonClientEntry(context.sourceFiles, context.edges),
612616
'session-authority-overlay': (context) =>
Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
import assert from 'node:assert/strict';
2+
import { test } from 'node:test';
3+
import { LAYERING_RULES, type LayeringContext } from './check.ts';
4+
import { PROCESS_GLOBALS_RULE, processGlobalsViolations } from './process-globals-policy.ts';
5+
6+
function check(source: string, file = 'src/commands/example.ts') {
7+
return processGlobalsViolations(new Map([[file, source]]));
8+
}
9+
10+
test('the registered gate rejects process configuration and command-host scope in non-entry sources', () => {
11+
const allTypeScriptSources = new Map([
12+
['src/commands/example.ts', 'const env = process.env;\ncommandHostEnvironment();'],
13+
]);
14+
const ratchets = {
15+
typeInversions: {},
16+
largestTypeCycle: [],
17+
sessionState: { writerOwnedFields: 0, ownerFileClaims: 0 },
18+
};
19+
const context: LayeringContext = {
20+
sourceFiles: [...allTypeScriptSources.keys()],
21+
sources: allTypeScriptSources,
22+
allTypeScriptSources,
23+
trackedSrcUtilsFiles: [],
24+
edges: [],
25+
ratchets,
26+
reference: ratchets,
27+
};
28+
const violations = LAYERING_RULES['process-globals'](context);
29+
assert.deepEqual(
30+
violations.map(({ rule, file, line }) => ({ rule, file, line })),
31+
[1, 2].map((line) => ({ rule: PROCESS_GLOBALS_RULE, file: 'src/commands/example.ts', line })),
32+
);
33+
});
34+
35+
test('all six process globals fail, including bracketed and imported process members', () => {
36+
for (const member of ['env', 'cwd', 'on', 'once', 'exit', 'emitWarning']) {
37+
for (const expression of [
38+
`process.${member}`,
39+
`process['${member}']`,
40+
`globalThis.process.${member}`,
41+
]) {
42+
assert.equal(check(`${expression};`).length, 1, expression);
43+
}
44+
assert.equal(check(`import p from 'node:process'; p.${member};`).length, 1);
45+
assert.equal(check(`import { ${member} as local } from 'process';`).length, 1);
46+
assert.equal(check(`const { ${member}: local } = process;`).length, 1);
47+
}
48+
});
49+
50+
test('entry declarations permit process reads but do not permit ambient command-host calls', () => {
51+
for (const file of [
52+
'src/bin.ts',
53+
'src/cli.ts',
54+
'src/cli/commands/example.ts',
55+
'src/daemon.ts',
56+
'src/agent-device-client.ts',
57+
'packages/capture-kit/src/png-worker.ts',
58+
'packages/maestro/src/daemon-port/run-script-http-child.ts',
59+
'packages/react-native/src/companion/client-companion-tunnel-worker.ts',
60+
'src/sdk/android-adb.ts',
61+
'src/io.ts',
62+
]) {
63+
assert.deepEqual(check('process.env; process.on("SIGINT", stop);', file), [], file);
64+
assert.equal(check('commandHostEnvironment();', file).length, 1, file);
65+
}
66+
});
67+
68+
test('command spawn boundaries permit the scoped host but still reject raw process configuration', () => {
69+
for (const file of [
70+
'packages/host-kit/src/internal/exec.ts',
71+
'packages/platform-apple/src/core/tool-provider.ts',
72+
'packages/platform-linux/src/tool-provider.ts',
73+
'src/platform-runtime-android-adb-host.ts',
74+
]) {
75+
assert.deepEqual(check('commandHostEnvironment();', file), [], file);
76+
assert.equal(check('process.env;', file).length, 1, file);
77+
}
78+
});
79+
80+
test('renamed and namespace command-host calls cannot bypass the boundary', () => {
81+
assert.equal(
82+
check(
83+
"import { commandHostEnvironment as host } from '@agent-device/host-kit/command'; host();",
84+
).length,
85+
1,
86+
);
87+
assert.equal(check('commands.commandHostEnvironment();').length, 1);
88+
});
89+
90+
test('test support and scripts are outside the production rule', () => {
91+
for (const file of [
92+
'src/example.test.ts',
93+
'packages/test-kit/src/example.ts',
94+
'src/example.fixtures.ts',
95+
'packages/maestro/test/example.ts',
96+
'scripts/example.ts',
97+
]) {
98+
assert.deepEqual(check('process.env; commandHostEnvironment();', file), [], file);
99+
}
100+
assert.equal(check('process.env;', 'packages/example/src/module.ts').length, 1);
101+
});
102+
103+
test('comments, strings and explicit host values are not process reads', () => {
104+
assert.deepEqual(check('// process.env\nconst text = "process.cwd()"; host.env; host.cwd;'), []);
105+
});
106+
107+
test('bare process values are captures in calls, aliases, spreads, returns and computed access', () => {
108+
for (const source of [
109+
'captureHostEnvironment(process);',
110+
'const p = process; p.env;',
111+
'someFn(process);',
112+
'const p = { ...process };',
113+
'function capture() { return process; }',
114+
'process[member];',
115+
'captureHostEnvironment(globalThis.process);',
116+
"import p from 'node:process'; someFn(p);",
117+
"import { default as p } from 'node:process'; captureHostEnvironment(p);",
118+
'const { process: p } = globalThis; p.env;',
119+
'let p; ({process: p} = globalThis); captureHostEnvironment(p);',
120+
"import * as p from 'node:process'; captureHostEnvironment(p.default);",
121+
]) {
122+
assert.equal(check(source).length, 1, source);
123+
assert.deepEqual(check(source, 'src/sdk/android-adb.ts'), [], source);
124+
}
125+
});
126+
127+
test('non-configuration process members and unrelated property names remain permitted', () => {
128+
assert.deepEqual(
129+
check(
130+
'process.platform; process.pid; process.stdout; object.process; const object = { process: true };',
131+
),
132+
[],
133+
);
134+
for (const file of [
135+
'packages/platform-apple/src/os/macos/helper.ts',
136+
'packages/platform-apple/src/runner/runner-process-launch.ts',
137+
]) {
138+
assert.equal(check('captureHostEnvironment(process);', file).length, 1, file);
139+
}
140+
});
141+
142+
test('local process bindings do not hide captures in other lexical scopes', () => {
143+
assert.deepEqual(
144+
check("import * as p from 'node:process'; function read(p) { return p.default; }"),
145+
[],
146+
);
147+
assert.deepEqual(
148+
check('function read(process) { return process; } const object = { process: 1 };'),
149+
[],
150+
);
151+
assert.equal(
152+
check('function read(process) { return process; } captureHostEnvironment(process);').length,
153+
1,
154+
);
155+
assert.equal(
156+
check('function read() { const process = child; return process; } const p = process;').length,
157+
1,
158+
);
159+
assert.deepEqual(
160+
check('type Record = { process: Child }; const child: typeof process = value;'),
161+
[],
162+
);
163+
});

0 commit comments

Comments
 (0)