Skip to content

Commit 689fb29

Browse files
committed
fix: restrict project config write sinks
1 parent 96af6d7 commit 689fb29

4 files changed

Lines changed: 71 additions & 6 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
## Unreleased
44

5-
- Security: repository `./agent-device.json` now accepts only project-safe automation defaults. It rejects daemon endpoint/auth/transport/server settings, tenant/run/lease selectors, provider/cloud and Metro connection fields, headers, and other operator-controlled values before any daemon health or RPC request. Put remote endpoint and token together in protected CI environment variables, user config, an explicit `--config` file, or the existing `connect`/`--remote-config` workflow. Daemon auth tokens no longer travel in serialized command flags.
5+
- Security: repository `./agent-device.json` now accepts only project-safe automation defaults. It rejects daemon endpoint/auth/transport/server settings, tenant/run/lease selectors, provider/cloud and Metro connection fields, headers, executable reporter modules, local write destinations, and other operator-controlled values before local module loading or any daemon health/RPC request. Put remote endpoint and token together in protected CI environment variables, user config, an explicit `--config` file, or the existing `connect`/`--remote-config` workflow. Daemon auth tokens no longer travel in serialized command flags.
66
- `viewport` is now rejected during capability admission on Apple targets instead of reaching the device and failing inside dispatch. No Apple backend can resize a screen — simulator and device geometry is fixed by the selected device type — so `viewport` on iOS/iPadOS/tvOS/macOS now fails with `UNSUPPORTED_OPERATION`, `viewport is not supported on this device`, and a hint pointing at `--platform web` and at picking a different simulator. `capabilities` no longer advertises `viewport` on Apple targets. Web viewport resizing (`agent-device viewport 1280 900 --platform web`) is unchanged, and Android was already denied.
77
- `--save-script` is now accepted only by the commands that declare it — `open`, `close`, and `replay`. A hand-built daemon request (or a `batch` step) that set `saveScript` on any other command, such as `record` or `trace`, used to arm script publication and could write a `.ad` artifact; it is now rejected with `INVALID_ARGS` before the request reaches admission, the device, or any handler. CLI, Node, and MCP usage of `--save-script` on its documented commands is unchanged.
88
- `diff screenshot` no longer runs the retired best-effort OCR and non-text analyzers. Their optional `ocr` and `nonTextDeltas` fields remain in the result type for source compatibility but are no longer emitted; use the baseline/current images and diff artifact with vision for qualitative interpretation.

src/__tests__/cli-config-trust.test.ts

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,74 @@ test.each([
8282
},
8383
);
8484

85+
test('project config rejects a custom reporter before importing repository code or dispatching', async () => {
86+
const { root, home, project } = makeTempWorkspace();
87+
const importMarker = path.join(project, 'reporter-imported');
88+
fs.writeFileSync(
89+
path.join(project, 'project-reporter.mjs'),
90+
[
91+
"import fs from 'node:fs';",
92+
`fs.writeFileSync(${JSON.stringify(importMarker)}, 'imported', 'utf8');`,
93+
"export default { name: 'project-controlled-reporter' };",
94+
].join('\n'),
95+
'utf8',
96+
);
97+
fs.writeFileSync(
98+
path.join(project, 'agent-device.json'),
99+
JSON.stringify({ reporter: ['./project-reporter.mjs'] }),
100+
'utf8',
101+
);
102+
103+
const result = await runCliCapture(['test', './suite'], {
104+
cwd: project,
105+
env: { HOME: home },
106+
});
107+
108+
assert.equal(result.code, 1);
109+
assert.equal(result.calls.length, 0);
110+
assert.equal(fs.existsSync(importMarker), false);
111+
assert.match(`${result.stdout}\n${result.stderr}`, /reporter/);
112+
assert.doesNotMatch(`${result.stdout}\n${result.stderr}`, /project-reporter\.mjs/);
113+
114+
fs.rmSync(root, { recursive: true, force: true });
115+
});
116+
117+
test.each([
118+
['reportJunit', './project.junit.xml', ['test', './suite']],
119+
['saveScript', './project.ad', ['open', 'Demo']],
120+
['launchConsole', './project.console.log', ['open', 'Demo']],
121+
] as const)(
122+
'project config rejects local write sink %s before dispatch or file creation',
123+
async (key, value, argv) => {
124+
const { root, home, project } = makeTempWorkspace();
125+
const dispatchMarker = path.join(project, 'daemon-dispatched');
126+
const outputPath = path.join(project, value);
127+
fs.writeFileSync(
128+
path.join(project, 'agent-device.json'),
129+
JSON.stringify({ [key]: value }),
130+
'utf8',
131+
);
132+
133+
const result = await runCliCapture([...argv], {
134+
cwd: project,
135+
env: { HOME: home },
136+
sendToDaemon: async () => {
137+
fs.writeFileSync(dispatchMarker, 'dispatched', 'utf8');
138+
return { ok: true, data: {} };
139+
},
140+
});
141+
142+
assert.equal(result.code, 1);
143+
assert.equal(result.calls.length, 0);
144+
assert.equal(fs.existsSync(dispatchMarker), false);
145+
assert.equal(fs.existsSync(outputPath), false);
146+
assert.match(`${result.stdout}\n${result.stderr}`, new RegExp(key));
147+
assert.doesNotMatch(`${result.stdout}\n${result.stderr}`, new RegExp(path.basename(value)));
148+
149+
fs.rmSync(root, { recursive: true, force: true });
150+
},
151+
);
152+
85153
test('project config cannot pair an endpoint with an environment token', async () => {
86154
const { root, home, project } = makeTempWorkspace();
87155
fs.writeFileSync(

src/cli-schema/cli-config.ts

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,6 @@ const PROJECT_CONFIG_FLAG_KEYS = new Set<FlagKey>([
4242
'session',
4343
'sessionLock',
4444
'activity',
45-
'launchConsole',
4645
'launchArgs',
4746
'launchUrl',
4847
'remote',
@@ -60,7 +59,6 @@ const PROJECT_CONFIG_FLAG_KEYS = new Set<FlagKey>([
6059
'noRecord',
6160
'record',
6261
'recordAs',
63-
'saveScript',
6462
'snapshotInteractiveOnly',
6563
'snapshotDiff',
6664
'snapshotDepth',
@@ -116,8 +114,6 @@ const PROJECT_CONFIG_FLAG_KEYS = new Set<FlagKey>([
116114
'batchMaxSteps',
117115
'retainPaths',
118116
'retentionMs',
119-
'reporter',
120-
'reportJunit',
121117
'shardAll',
122118
'shardSplit',
123119
'noLogin',

website/docs/docs/configuration.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,8 +85,9 @@ user- or explicit-config only:
8585
- provider/cloud fields (`provider*`, `aws*`)
8686
- Metro endpoint/token fields (`metro*`, `bundleUrl`)
8787
- request headers and structured install sources
88+
- local code and write destinations (`reporter`, `reportJunit`, `saveScript`, `launchConsole`)
8889

89-
Project config can use project-safe command defaults such as `snapshotDepth`, `snapshotScope`, `activity`, `relaunch`, `shutdown`, `fps`, `quality`, and `saveScript`. `stepsFile` is user- or explicit-config only because it selects a local file.
90+
Project config can use project-safe command defaults such as `snapshotDepth`, `snapshotScope`, `activity`, `relaunch`, `shutdown`, `fps`, and `quality`. Local path and executable-module selectors such as `stepsFile` and `reporter` are user- or explicit-config only.
9091

9192
`install-from-source` can read a structured GitHub Actions artifact source from user or explicit config when a compatible remote daemon resolves CI artifacts server-side. Repository config rejects this operator-controlled source:
9293

0 commit comments

Comments
 (0)