fixo-cli 1.0.4 → 2.0.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.
Potentially problematic release.
This version of fixo-cli might be problematic. Click here for more details.
- package/CHANGELOG.md +62 -0
- package/README.md +18 -14
- package/dist/agent/agent-client.d.ts +28 -6
- package/dist/agent/agent-client.d.ts.map +1 -1
- package/dist/agent/agent-client.js +118 -39
- package/dist/agent/agent-client.js.map +1 -1
- package/dist/agent/agent-pool.d.ts +55 -6
- package/dist/agent/agent-pool.d.ts.map +1 -1
- package/dist/agent/agent-pool.js +120 -20
- package/dist/agent/agent-pool.js.map +1 -1
- package/dist/agent/auto-verifier.d.ts +55 -0
- package/dist/agent/auto-verifier.d.ts.map +1 -0
- package/dist/agent/auto-verifier.js +50 -0
- package/dist/agent/auto-verifier.js.map +1 -0
- package/dist/agent/command-parser.d.ts.map +1 -1
- package/dist/agent/command-parser.js +176 -0
- package/dist/agent/command-parser.js.map +1 -1
- package/dist/agent/context-builder.d.ts +24 -0
- package/dist/agent/context-builder.d.ts.map +1 -0
- package/dist/agent/context-builder.js +197 -0
- package/dist/agent/context-builder.js.map +1 -0
- package/dist/agent/conversation.d.ts +14 -1
- package/dist/agent/conversation.d.ts.map +1 -1
- package/dist/agent/conversation.js +53 -7
- package/dist/agent/conversation.js.map +1 -1
- package/dist/agent/mcp-bridge.js +1 -1
- package/dist/agent/mcp-bridge.js.map +1 -1
- package/dist/agent/orchestrator.d.ts +45 -0
- package/dist/agent/orchestrator.d.ts.map +1 -1
- package/dist/agent/orchestrator.js +140 -3
- package/dist/agent/orchestrator.js.map +1 -1
- package/dist/agent/parser-adapter.d.ts +17 -0
- package/dist/agent/parser-adapter.d.ts.map +1 -1
- package/dist/agent/parser-adapter.js +254 -2
- package/dist/agent/parser-adapter.js.map +1 -1
- package/dist/agent/predictive-gate.d.ts.map +1 -1
- package/dist/agent/predictive-gate.js +4 -1
- package/dist/agent/predictive-gate.js.map +1 -1
- package/dist/agent/providers-manager.d.ts +5 -0
- package/dist/agent/providers-manager.d.ts.map +1 -1
- package/dist/agent/providers-manager.js +119 -8
- package/dist/agent/providers-manager.js.map +1 -1
- package/dist/agent/repo-map.d.ts +18 -1
- package/dist/agent/repo-map.d.ts.map +1 -1
- package/dist/agent/repo-map.js +144 -54
- package/dist/agent/repo-map.js.map +1 -1
- package/dist/agent/retry.js +1 -2
- package/dist/agent/retry.js.map +1 -1
- package/dist/agent/single-agent.d.ts.map +1 -1
- package/dist/agent/single-agent.js +129 -22
- package/dist/agent/single-agent.js.map +1 -1
- package/dist/agent/skills.d.ts.map +1 -1
- package/dist/agent/skills.js +2 -1
- package/dist/agent/skills.js.map +1 -1
- package/dist/agent/subagent.js +2 -2
- package/dist/agent/subagent.js.map +1 -1
- package/dist/agent/task-router.d.ts +46 -0
- package/dist/agent/task-router.d.ts.map +1 -0
- package/dist/agent/task-router.js +352 -0
- package/dist/agent/task-router.js.map +1 -0
- package/dist/agent/telemetry.d.ts +29 -1
- package/dist/agent/telemetry.d.ts.map +1 -1
- package/dist/agent/telemetry.js +25 -10
- package/dist/agent/telemetry.js.map +1 -1
- package/dist/agent/tool-definitions.d.ts +3 -0
- package/dist/agent/tool-definitions.d.ts.map +1 -0
- package/dist/agent/tool-definitions.js +519 -0
- package/dist/agent/tool-definitions.js.map +1 -0
- package/dist/agent/tool-executor.d.ts +6 -1
- package/dist/agent/tool-executor.d.ts.map +1 -1
- package/dist/agent/tool-executor.js +99 -553
- package/dist/agent/tool-executor.js.map +1 -1
- package/dist/agent/tools/command-tools.d.ts +6 -0
- package/dist/agent/tools/command-tools.d.ts.map +1 -0
- package/dist/agent/tools/command-tools.js +104 -0
- package/dist/agent/tools/command-tools.js.map +1 -0
- package/dist/agent/tools/file-tools.d.ts +15 -0
- package/dist/agent/tools/file-tools.d.ts.map +1 -0
- package/dist/agent/tools/file-tools.js +551 -0
- package/dist/agent/tools/file-tools.js.map +1 -0
- package/dist/agent/tools/todo-tools.d.ts +3 -0
- package/dist/agent/tools/todo-tools.d.ts.map +1 -0
- package/dist/agent/tools/todo-tools.js +70 -0
- package/dist/agent/tools/todo-tools.js.map +1 -0
- package/dist/agent/web-impl.d.ts.map +1 -1
- package/dist/agent/web-impl.js +45 -0
- package/dist/agent/web-impl.js.map +1 -1
- package/dist/agent/worker-agent.d.ts +3 -1
- package/dist/agent/worker-agent.d.ts.map +1 -1
- package/dist/agent/worker-agent.js +51 -14
- package/dist/agent/worker-agent.js.map +1 -1
- package/dist/config.d.ts +242 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +79 -0
- package/dist/config.js.map +1 -1
- package/dist/git/git-manager.d.ts +33 -2
- package/dist/git/git-manager.d.ts.map +1 -1
- package/dist/git/git-manager.js +111 -15
- package/dist/git/git-manager.js.map +1 -1
- package/dist/git/git-ops.d.ts.map +1 -1
- package/dist/git/git-ops.js +2 -1
- package/dist/git/git-ops.js.map +1 -1
- package/dist/index.js +85 -8
- package/dist/index.js.map +1 -1
- package/dist/lsp/lsp-manager.js +1 -1
- package/dist/lsp/lsp-manager.js.map +1 -1
- package/dist/model-outcomes.d.ts.map +1 -1
- package/dist/model-outcomes.js +2 -1
- package/dist/model-outcomes.js.map +1 -1
- package/dist/planner.d.ts +0 -9
- package/dist/planner.d.ts.map +1 -1
- package/dist/planner.js +0 -9
- package/dist/planner.js.map +1 -1
- package/dist/project-memory.d.ts +12 -1
- package/dist/project-memory.d.ts.map +1 -1
- package/dist/project-memory.js +8 -6
- package/dist/project-memory.js.map +1 -1
- package/dist/runtime/loop-mitigation.d.ts +78 -7
- package/dist/runtime/loop-mitigation.d.ts.map +1 -1
- package/dist/runtime/loop-mitigation.js +122 -9
- package/dist/runtime/loop-mitigation.js.map +1 -1
- package/dist/runtime/os-sandbox.d.ts +100 -0
- package/dist/runtime/os-sandbox.d.ts.map +1 -0
- package/dist/runtime/os-sandbox.js +246 -0
- package/dist/runtime/os-sandbox.js.map +1 -0
- package/dist/runtime/run-inventory.d.ts +17 -0
- package/dist/runtime/run-inventory.d.ts.map +1 -0
- package/dist/runtime/run-inventory.js +49 -0
- package/dist/runtime/run-inventory.js.map +1 -0
- package/dist/runtime/staging.d.ts.map +1 -1
- package/dist/runtime/staging.js +4 -1
- package/dist/runtime/staging.js.map +1 -1
- package/dist/runtime/task-session.d.ts +14 -0
- package/dist/runtime/task-session.d.ts.map +1 -1
- package/dist/runtime/task-session.js +26 -0
- package/dist/runtime/task-session.js.map +1 -1
- package/dist/setup-wizard.d.ts +11 -3
- package/dist/setup-wizard.d.ts.map +1 -1
- package/dist/setup-wizard.js +113 -15
- package/dist/setup-wizard.js.map +1 -1
- package/dist/types.d.ts +8 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/ui/commands/context-commands.d.ts +7 -0
- package/dist/ui/commands/context-commands.d.ts.map +1 -0
- package/dist/ui/commands/context-commands.js +241 -0
- package/dist/ui/commands/context-commands.js.map +1 -0
- package/dist/ui/commands/index.d.ts +3 -0
- package/dist/ui/commands/index.d.ts.map +1 -0
- package/dist/ui/commands/index.js +46 -0
- package/dist/ui/commands/index.js.map +1 -0
- package/dist/ui/commands/info-commands.d.ts +15 -0
- package/dist/ui/commands/info-commands.d.ts.map +1 -0
- package/dist/ui/commands/info-commands.js +122 -0
- package/dist/ui/commands/info-commands.js.map +1 -0
- package/dist/ui/commands/model-commands.d.ts +5 -0
- package/dist/ui/commands/model-commands.d.ts.map +1 -0
- package/dist/ui/commands/model-commands.js +417 -0
- package/dist/ui/commands/model-commands.js.map +1 -0
- package/dist/ui/commands/session-commands.d.ts +5 -0
- package/dist/ui/commands/session-commands.d.ts.map +1 -0
- package/dist/ui/commands/session-commands.js +154 -0
- package/dist/ui/commands/session-commands.js.map +1 -0
- package/dist/ui/commands/task-commands.d.ts +8 -0
- package/dist/ui/commands/task-commands.d.ts.map +1 -0
- package/dist/ui/commands/task-commands.js +152 -0
- package/dist/ui/commands/task-commands.js.map +1 -0
- package/dist/ui/commands/types.d.ts +46 -0
- package/dist/ui/commands/types.d.ts.map +1 -0
- package/dist/ui/commands/types.js +2 -0
- package/dist/ui/commands/types.js.map +1 -0
- package/dist/ui/commands/workspace-commands.d.ts +8 -0
- package/dist/ui/commands/workspace-commands.d.ts.map +1 -0
- package/dist/ui/commands/workspace-commands.js +131 -0
- package/dist/ui/commands/workspace-commands.js.map +1 -0
- package/dist/ui/loading-animation.d.ts +24 -0
- package/dist/ui/loading-animation.d.ts.map +1 -0
- package/dist/ui/loading-animation.js +123 -0
- package/dist/ui/loading-animation.js.map +1 -0
- package/dist/ui/markdown-stream.js +2 -2
- package/dist/ui/markdown-stream.js.map +1 -1
- package/dist/ui/prompt.d.ts +7 -0
- package/dist/ui/prompt.d.ts.map +1 -1
- package/dist/ui/prompt.js +435 -1214
- package/dist/ui/prompt.js.map +1 -1
- package/dist/ui/render-primitives.d.ts +6 -0
- package/dist/ui/render-primitives.d.ts.map +1 -1
- package/dist/ui/render-primitives.js +30 -13
- package/dist/ui/render-primitives.js.map +1 -1
- package/dist/ui/render.d.ts.map +1 -1
- package/dist/ui/render.js +2 -0
- package/dist/ui/render.js.map +1 -1
- package/package.json +17 -3
- package/scripts/check-vendor-wasm.js +11 -0
- package/vendor/tree-sitter-go.wasm +0 -0
- package/vendor/tree-sitter-javascript.wasm +0 -0
- package/vendor/tree-sitter-python.wasm +0 -0
- package/vendor/tree-sitter-rust.wasm +0 -0
- package/vendor/tree-sitter-tsx.wasm +0 -0
- package/vendor/tree-sitter-typescript.wasm +0 -0
|
@@ -19,25 +19,96 @@
|
|
|
19
19
|
*
|
|
20
20
|
* Hard-abort is left untouched — the detector still raises
|
|
21
21
|
* SemanticLoopAbortedError at its existing threshold.
|
|
22
|
+
*
|
|
23
|
+
* ── Phase 1b (Sliding Window) ──────────────────────────────────────
|
|
24
|
+
*
|
|
25
|
+
* The original tracker stored warn counts permanently for the lifetime
|
|
26
|
+
* of the session and never decayed them. Once a file accumulated 2
|
|
27
|
+
* warns it became "immortal" — every subsequent read was rejected and,
|
|
28
|
+
* because `task-session.canMutate()` requires a fresh read before any
|
|
29
|
+
* write, the file also became un-modifiable. This deadlock was
|
|
30
|
+
* observed in the June 22, 2026 log session on `style.css`,
|
|
31
|
+
* `portfolio.css`, and `script.js`.
|
|
32
|
+
*
|
|
33
|
+
* Phase 1b adds an optional sliding-window mode. When enabled, each
|
|
34
|
+
* warn carries the `currentTurn` it occurred on, and `isBlocked()`
|
|
35
|
+
* only considers warns within the last N turns. Default N is 10. The
|
|
36
|
+
* legacy session-lifetime behavior is preserved as the default
|
|
37
|
+
* (toggled by `preferences.agent.loopGuard.useSlidingWindow`) until
|
|
38
|
+
* Phase 7 flips the flag after soak.
|
|
22
39
|
*/
|
|
23
40
|
/** Number of `warn` verdicts on the same target before reads are blocked. */
|
|
24
41
|
export declare const REPEAT_WARN_BLOCK_THRESHOLD = 2;
|
|
42
|
+
/** Default sliding-window size in tool calls (used when sliding is enabled). */
|
|
43
|
+
export declare const DEFAULT_BLOCK_WINDOW_TURNS = 10;
|
|
25
44
|
export declare function isReadTool(name: string): boolean;
|
|
45
|
+
/**
|
|
46
|
+
* Construction-time options for the tracker. All fields optional.
|
|
47
|
+
* When `useSlidingWindow` is false (default), behavior is identical
|
|
48
|
+
* to the pre-Phase-1b implementation: warns accumulate for the entire
|
|
49
|
+
* session, and the block flips on at the threshold and stays on.
|
|
50
|
+
*/
|
|
51
|
+
export interface LoopMitigationOptions {
|
|
52
|
+
/** When true, warns decay outside `blockWindowTurns`. Default false. */
|
|
53
|
+
useSlidingWindow?: boolean;
|
|
54
|
+
/** Sliding-window size in tool calls. Default {@link DEFAULT_BLOCK_WINDOW_TURNS}. */
|
|
55
|
+
blockWindowTurns?: number;
|
|
56
|
+
}
|
|
26
57
|
export declare class LoopMitigationTracker {
|
|
58
|
+
/** Legacy-mode running count of warns per target. */
|
|
27
59
|
private readonly warnCounts;
|
|
60
|
+
/** Legacy-mode set of targets that have flipped to blocked. */
|
|
28
61
|
private readonly blockedTargets;
|
|
62
|
+
/** Sliding-mode timestamps (tool-call counts) of warns per target. */
|
|
63
|
+
private readonly warnTimestamps;
|
|
64
|
+
private readonly useSlidingWindow;
|
|
65
|
+
private readonly blockWindowTurns;
|
|
66
|
+
/**
|
|
67
|
+
* Monotonic fallback counter used when sliding mode is enabled but
|
|
68
|
+
* the caller didn't pass a `currentTurn`. Ensures the sliding-window
|
|
69
|
+
* path stays consistent (both recordWarn and isBlocked operate on
|
|
70
|
+
* the same datastructure) even for legacy callers.
|
|
71
|
+
*/
|
|
72
|
+
private fallbackTurn;
|
|
73
|
+
constructor(options?: LoopMitigationOptions);
|
|
29
74
|
/**
|
|
30
75
|
* Record a `warn` verdict for `target`. Returns true if this verdict
|
|
31
76
|
* pushed `target` over the block threshold (caller may choose to
|
|
32
77
|
* surface a one-time "now blocking" message).
|
|
78
|
+
*
|
|
79
|
+
* `currentTurn` is required when the tracker was constructed with
|
|
80
|
+
* `useSlidingWindow: true` — it's the tool-call count at the moment
|
|
81
|
+
* of the warn, used to compare against the sliding window. In
|
|
82
|
+
* legacy mode the parameter is accepted but ignored, preserving
|
|
83
|
+
* backward-compatible callers.
|
|
84
|
+
*/
|
|
85
|
+
recordWarn(target: string, currentTurn?: number): boolean;
|
|
86
|
+
private recordWarnLegacy;
|
|
87
|
+
/**
|
|
88
|
+
* True if the target has been blocked from further reads.
|
|
89
|
+
*
|
|
90
|
+
* In sliding-window mode, `currentTurn` should be passed so the
|
|
91
|
+
* window can be evaluated against the present moment. Without it
|
|
92
|
+
* the method evaluates against the most recent warn timestamp,
|
|
93
|
+
* which is usually correct but may keep a target blocked for one
|
|
94
|
+
* extra turn after the window would have decayed.
|
|
95
|
+
*/
|
|
96
|
+
isBlocked(target: string, currentTurn?: number): boolean;
|
|
97
|
+
/**
|
|
98
|
+
* Number of warns recorded for the target (for diagnostics).
|
|
99
|
+
* In sliding mode this returns warns within the current window
|
|
100
|
+
* rather than the cumulative session count.
|
|
101
|
+
*/
|
|
102
|
+
warnsFor(target: string, currentTurn?: number): number;
|
|
103
|
+
/**
|
|
104
|
+
* Reset all tracker state. Used between sessions, in tests, and
|
|
105
|
+
* (Phase 1b) optionally between orchestrator subtasks so one stuck
|
|
106
|
+
* subtask cannot poison the rest of the run.
|
|
107
|
+
*
|
|
108
|
+
* Passing a target resets state for just that path; passing nothing
|
|
109
|
+
* clears everything.
|
|
33
110
|
*/
|
|
34
|
-
|
|
35
|
-
/** True if the target has been blocked from further reads. */
|
|
36
|
-
isBlocked(target: string): boolean;
|
|
37
|
-
/** Number of warns recorded for the target (for diagnostics). */
|
|
38
|
-
warnsFor(target: string): number;
|
|
39
|
-
/** Reset all state (used between sessions or in tests). */
|
|
40
|
-
reset(): void;
|
|
111
|
+
reset(target?: string): void;
|
|
41
112
|
}
|
|
42
113
|
/**
|
|
43
114
|
* Build the synthetic tool-result that replaces a blocked read.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"loop-mitigation.d.ts","sourceRoot":"","sources":["../../src/runtime/loop-mitigation.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"loop-mitigation.d.ts","sourceRoot":"","sources":["../../src/runtime/loop-mitigation.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsCG;AAEH,6EAA6E;AAC7E,eAAO,MAAM,2BAA2B,IAAI,CAAC;AAE7C,gFAAgF;AAChF,eAAO,MAAM,0BAA0B,KAAK,CAAC;AAS7C,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAEhD;AAED;;;;;GAKG;AACH,MAAM,WAAW,qBAAqB;IACpC,wEAAwE;IACxE,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B,qFAAqF;IACrF,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC3B;AAED,qBAAa,qBAAqB;IAChC,qDAAqD;IACrD,OAAO,CAAC,QAAQ,CAAC,UAAU,CAA6B;IACxD,+DAA+D;IAC/D,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAqB;IACpD,sEAAsE;IACtE,OAAO,CAAC,QAAQ,CAAC,cAAc,CAA+B;IAE9D,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAAU;IAC3C,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAAS;IAC1C;;;;;OAKG;IACH,OAAO,CAAC,YAAY,CAAK;gBAEb,OAAO,GAAE,qBAA0B;IAK/C;;;;;;;;;;OAUG;IACH,UAAU,CAAC,MAAM,EAAE,MAAM,EAAE,WAAW,CAAC,EAAE,MAAM,GAAG,OAAO;IA0BzD,OAAO,CAAC,gBAAgB;IAWxB;;;;;;;;OAQG;IACH,SAAS,CAAC,MAAM,EAAE,MAAM,EAAE,WAAW,CAAC,EAAE,MAAM,GAAG,OAAO;IAYxD;;;;OAIG;IACH,QAAQ,CAAC,MAAM,EAAE,MAAM,EAAE,WAAW,CAAC,EAAE,MAAM,GAAG,MAAM;IAWtD;;;;;;;OAOG;IACH,KAAK,CAAC,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI;CAY7B;AAED;;;;GAIG;AACH,wBAAgB,0BAA0B,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAShF"}
|
|
@@ -19,9 +19,28 @@
|
|
|
19
19
|
*
|
|
20
20
|
* Hard-abort is left untouched — the detector still raises
|
|
21
21
|
* SemanticLoopAbortedError at its existing threshold.
|
|
22
|
+
*
|
|
23
|
+
* ── Phase 1b (Sliding Window) ──────────────────────────────────────
|
|
24
|
+
*
|
|
25
|
+
* The original tracker stored warn counts permanently for the lifetime
|
|
26
|
+
* of the session and never decayed them. Once a file accumulated 2
|
|
27
|
+
* warns it became "immortal" — every subsequent read was rejected and,
|
|
28
|
+
* because `task-session.canMutate()` requires a fresh read before any
|
|
29
|
+
* write, the file also became un-modifiable. This deadlock was
|
|
30
|
+
* observed in the June 22, 2026 log session on `style.css`,
|
|
31
|
+
* `portfolio.css`, and `script.js`.
|
|
32
|
+
*
|
|
33
|
+
* Phase 1b adds an optional sliding-window mode. When enabled, each
|
|
34
|
+
* warn carries the `currentTurn` it occurred on, and `isBlocked()`
|
|
35
|
+
* only considers warns within the last N turns. Default N is 10. The
|
|
36
|
+
* legacy session-lifetime behavior is preserved as the default
|
|
37
|
+
* (toggled by `preferences.agent.loopGuard.useSlidingWindow`) until
|
|
38
|
+
* Phase 7 flips the flag after soak.
|
|
22
39
|
*/
|
|
23
40
|
/** Number of `warn` verdicts on the same target before reads are blocked. */
|
|
24
41
|
export const REPEAT_WARN_BLOCK_THRESHOLD = 2;
|
|
42
|
+
/** Default sliding-window size in tool calls (used when sliding is enabled). */
|
|
43
|
+
export const DEFAULT_BLOCK_WINDOW_TURNS = 10;
|
|
25
44
|
/** Tool names that count as "reading" the target path. */
|
|
26
45
|
const READ_TOOL_NAMES = new Set([
|
|
27
46
|
'read_file',
|
|
@@ -32,14 +51,63 @@ export function isReadTool(name) {
|
|
|
32
51
|
return READ_TOOL_NAMES.has(name);
|
|
33
52
|
}
|
|
34
53
|
export class LoopMitigationTracker {
|
|
54
|
+
/** Legacy-mode running count of warns per target. */
|
|
35
55
|
warnCounts = new Map();
|
|
56
|
+
/** Legacy-mode set of targets that have flipped to blocked. */
|
|
36
57
|
blockedTargets = new Set();
|
|
58
|
+
/** Sliding-mode timestamps (tool-call counts) of warns per target. */
|
|
59
|
+
warnTimestamps = new Map();
|
|
60
|
+
useSlidingWindow;
|
|
61
|
+
blockWindowTurns;
|
|
62
|
+
/**
|
|
63
|
+
* Monotonic fallback counter used when sliding mode is enabled but
|
|
64
|
+
* the caller didn't pass a `currentTurn`. Ensures the sliding-window
|
|
65
|
+
* path stays consistent (both recordWarn and isBlocked operate on
|
|
66
|
+
* the same datastructure) even for legacy callers.
|
|
67
|
+
*/
|
|
68
|
+
fallbackTurn = 0;
|
|
69
|
+
constructor(options = {}) {
|
|
70
|
+
this.useSlidingWindow = options.useSlidingWindow === true;
|
|
71
|
+
this.blockWindowTurns = options.blockWindowTurns ?? DEFAULT_BLOCK_WINDOW_TURNS;
|
|
72
|
+
}
|
|
37
73
|
/**
|
|
38
74
|
* Record a `warn` verdict for `target`. Returns true if this verdict
|
|
39
75
|
* pushed `target` over the block threshold (caller may choose to
|
|
40
76
|
* surface a one-time "now blocking" message).
|
|
77
|
+
*
|
|
78
|
+
* `currentTurn` is required when the tracker was constructed with
|
|
79
|
+
* `useSlidingWindow: true` — it's the tool-call count at the moment
|
|
80
|
+
* of the warn, used to compare against the sliding window. In
|
|
81
|
+
* legacy mode the parameter is accepted but ignored, preserving
|
|
82
|
+
* backward-compatible callers.
|
|
41
83
|
*/
|
|
42
|
-
recordWarn(target) {
|
|
84
|
+
recordWarn(target, currentTurn) {
|
|
85
|
+
if (this.useSlidingWindow) {
|
|
86
|
+
// Sliding mode: append a timestamp and re-evaluate block state.
|
|
87
|
+
// If the caller didn't pass currentTurn, use a monotonic fallback
|
|
88
|
+
// so the sliding path remains the single source of truth (rather
|
|
89
|
+
// than splitting state across legacy + sliding stores).
|
|
90
|
+
const turn = currentTurn ?? ++this.fallbackTurn;
|
|
91
|
+
if (currentTurn !== undefined) {
|
|
92
|
+
// Keep fallback ahead of explicit turns so a later mixed call
|
|
93
|
+
// never re-uses an earlier turn id.
|
|
94
|
+
if (currentTurn > this.fallbackTurn)
|
|
95
|
+
this.fallbackTurn = currentTurn;
|
|
96
|
+
}
|
|
97
|
+
const arr = this.warnTimestamps.get(target) ?? [];
|
|
98
|
+
arr.push(turn);
|
|
99
|
+
// Prune anything outside the current window so the array doesn't
|
|
100
|
+
// grow unboundedly across long sessions.
|
|
101
|
+
const cutoff = turn - this.blockWindowTurns;
|
|
102
|
+
const pruned = arr.filter((t) => t >= cutoff);
|
|
103
|
+
this.warnTimestamps.set(target, pruned);
|
|
104
|
+
const wasBlocked = pruned.length - 1 >= REPEAT_WARN_BLOCK_THRESHOLD; // prior state (before this push, but pruned)
|
|
105
|
+
const isNowBlocked = pruned.length >= REPEAT_WARN_BLOCK_THRESHOLD;
|
|
106
|
+
return !wasBlocked && isNowBlocked;
|
|
107
|
+
}
|
|
108
|
+
return this.recordWarnLegacy(target);
|
|
109
|
+
}
|
|
110
|
+
recordWarnLegacy(target) {
|
|
43
111
|
const prev = this.warnCounts.get(target) ?? 0;
|
|
44
112
|
const next = prev + 1;
|
|
45
113
|
this.warnCounts.set(target, next);
|
|
@@ -49,18 +117,63 @@ export class LoopMitigationTracker {
|
|
|
49
117
|
}
|
|
50
118
|
return false;
|
|
51
119
|
}
|
|
52
|
-
/**
|
|
53
|
-
|
|
120
|
+
/**
|
|
121
|
+
* True if the target has been blocked from further reads.
|
|
122
|
+
*
|
|
123
|
+
* In sliding-window mode, `currentTurn` should be passed so the
|
|
124
|
+
* window can be evaluated against the present moment. Without it
|
|
125
|
+
* the method evaluates against the most recent warn timestamp,
|
|
126
|
+
* which is usually correct but may keep a target blocked for one
|
|
127
|
+
* extra turn after the window would have decayed.
|
|
128
|
+
*/
|
|
129
|
+
isBlocked(target, currentTurn) {
|
|
130
|
+
if (this.useSlidingWindow) {
|
|
131
|
+
const arr = this.warnTimestamps.get(target);
|
|
132
|
+
if (!arr || arr.length === 0)
|
|
133
|
+
return false;
|
|
134
|
+
const ref = currentTurn ?? arr[arr.length - 1];
|
|
135
|
+
const cutoff = ref - this.blockWindowTurns;
|
|
136
|
+
const live = arr.filter((t) => t >= cutoff);
|
|
137
|
+
return live.length >= REPEAT_WARN_BLOCK_THRESHOLD;
|
|
138
|
+
}
|
|
54
139
|
return this.blockedTargets.has(target);
|
|
55
140
|
}
|
|
56
|
-
/**
|
|
57
|
-
|
|
141
|
+
/**
|
|
142
|
+
* Number of warns recorded for the target (for diagnostics).
|
|
143
|
+
* In sliding mode this returns warns within the current window
|
|
144
|
+
* rather than the cumulative session count.
|
|
145
|
+
*/
|
|
146
|
+
warnsFor(target, currentTurn) {
|
|
147
|
+
if (this.useSlidingWindow) {
|
|
148
|
+
const arr = this.warnTimestamps.get(target);
|
|
149
|
+
if (!arr)
|
|
150
|
+
return 0;
|
|
151
|
+
if (currentTurn === undefined)
|
|
152
|
+
return arr.length;
|
|
153
|
+
const cutoff = currentTurn - this.blockWindowTurns;
|
|
154
|
+
return arr.filter((t) => t >= cutoff).length;
|
|
155
|
+
}
|
|
58
156
|
return this.warnCounts.get(target) ?? 0;
|
|
59
157
|
}
|
|
60
|
-
/**
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
158
|
+
/**
|
|
159
|
+
* Reset all tracker state. Used between sessions, in tests, and
|
|
160
|
+
* (Phase 1b) optionally between orchestrator subtasks so one stuck
|
|
161
|
+
* subtask cannot poison the rest of the run.
|
|
162
|
+
*
|
|
163
|
+
* Passing a target resets state for just that path; passing nothing
|
|
164
|
+
* clears everything.
|
|
165
|
+
*/
|
|
166
|
+
reset(target) {
|
|
167
|
+
if (target === undefined) {
|
|
168
|
+
this.warnCounts.clear();
|
|
169
|
+
this.blockedTargets.clear();
|
|
170
|
+
this.warnTimestamps.clear();
|
|
171
|
+
this.fallbackTurn = 0;
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
this.warnCounts.delete(target);
|
|
175
|
+
this.blockedTargets.delete(target);
|
|
176
|
+
this.warnTimestamps.delete(target);
|
|
64
177
|
}
|
|
65
178
|
}
|
|
66
179
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"loop-mitigation.js","sourceRoot":"","sources":["../../src/runtime/loop-mitigation.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"loop-mitigation.js","sourceRoot":"","sources":["../../src/runtime/loop-mitigation.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* os-sandbox.ts — Phase 1.2 OS-level sandbox wrapper for `run_command`.
|
|
3
|
+
*
|
|
4
|
+
* The CLI already enforces a regex-based command-parser layer and
|
|
5
|
+
* a WorkspaceGuard path-boundary check before any shell command runs.
|
|
6
|
+
* That stops the obvious cases (`> /etc/passwd`, `sed -i ../foo`) but
|
|
7
|
+
* is not a complete defence against a confused or prompt-injected
|
|
8
|
+
* model — a regex blocklist is a speed bump, not a wall.
|
|
9
|
+
*
|
|
10
|
+
* This module adds an OS-enforced second layer that the model has no
|
|
11
|
+
* way around: `sandbox-exec` on macOS, `bwrap` on Linux. Both use the
|
|
12
|
+
* same OS primitives Apple's own `xcrun` and Flatpak rely on, so the
|
|
13
|
+
* deny semantics are exactly the kernel's, not a JavaScript regex.
|
|
14
|
+
*
|
|
15
|
+
* Design choices, deliberate:
|
|
16
|
+
*
|
|
17
|
+
* - Default is OFF (config `safety.sandboxMode === 'guard'`). Turning
|
|
18
|
+
* it on changes runtime behaviour and we want users to opt in
|
|
19
|
+
* explicitly, not be surprised by a build that suddenly fails
|
|
20
|
+
* because their `npm test` script writes to `~/.npm/_cacache`.
|
|
21
|
+
*
|
|
22
|
+
* - When ON and the platform binary is missing, we surface a clear
|
|
23
|
+
* structured error instead of silently downgrading. A user who
|
|
24
|
+
* opted into `os-sandbox` and got the regex layer would have the
|
|
25
|
+
* worst of both worlds — false safety.
|
|
26
|
+
*
|
|
27
|
+
* - Network is allowed by default — most legitimate build/test
|
|
28
|
+
* commands need it (npm install, cargo fetch, pip). Callers that
|
|
29
|
+
* want strict isolation can pass `allowNetwork: false`.
|
|
30
|
+
*
|
|
31
|
+
* - Reads are unrestricted. The agent legitimately needs to inspect
|
|
32
|
+
* the system to plan its work. Writes are the dangerous side.
|
|
33
|
+
*
|
|
34
|
+
* - The macOS profile is generated at runtime into the OS temp dir
|
|
35
|
+
* from a template (no static `.sb` file shipped) so allow-write
|
|
36
|
+
* paths can be parameterised per-invocation without macros.
|
|
37
|
+
* `sandbox-exec` is technically deprecated by Apple but still
|
|
38
|
+
* ships and works on darwin 25; what's available today wins.
|
|
39
|
+
*/
|
|
40
|
+
import { type SpawnSyncReturns } from 'node:child_process';
|
|
41
|
+
export type SandboxPlatform = 'darwin' | 'linux';
|
|
42
|
+
export interface SandboxOpts {
|
|
43
|
+
/** Directory the shell runs in. Must be readable. */
|
|
44
|
+
cwd: string;
|
|
45
|
+
/**
|
|
46
|
+
* Roots the sandboxed process may write to. Reads are not
|
|
47
|
+
* restricted — only writes. Always includes the OS temp dir
|
|
48
|
+
* regardless of what the caller passes, because nearly every
|
|
49
|
+
* build tool writes there.
|
|
50
|
+
*/
|
|
51
|
+
allowedWritePaths: string[];
|
|
52
|
+
/**
|
|
53
|
+
* Whether the sandboxed process may open network sockets. Most
|
|
54
|
+
* legitimate `run_command` calls need this (npm install,
|
|
55
|
+
* cargo fetch), so the caller usually wants `true`.
|
|
56
|
+
*/
|
|
57
|
+
allowNetwork: boolean;
|
|
58
|
+
/** Wall-clock timeout in ms. Defaults to 60s. */
|
|
59
|
+
timeout?: number;
|
|
60
|
+
/** Max captured stdout/stderr bytes. Defaults to 1 MiB. */
|
|
61
|
+
maxBuffer?: number;
|
|
62
|
+
/** Environment for the child process. */
|
|
63
|
+
env?: NodeJS.ProcessEnv;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Why a sandboxed `run_command` couldn't actually be wrapped in an
|
|
67
|
+
* OS sandbox. Distinct from a *command-failed-inside-the-sandbox*
|
|
68
|
+
* error, which surfaces through {@link SpawnSyncReturns.status} as
|
|
69
|
+
* usual.
|
|
70
|
+
*/
|
|
71
|
+
export declare class SandboxUnavailableError extends Error {
|
|
72
|
+
platform: NodeJS.Platform;
|
|
73
|
+
reason: string;
|
|
74
|
+
constructor(platform: NodeJS.Platform, reason: string);
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Run a shell command inside an OS-enforced sandbox.
|
|
78
|
+
*
|
|
79
|
+
* Throws {@link SandboxUnavailableError} if the current platform has
|
|
80
|
+
* no supported wrapper or the required binary is not on `$PATH`.
|
|
81
|
+
* Otherwise returns the same {@link SpawnSyncReturns} shape the
|
|
82
|
+
* caller would get from a plain `spawnSync(command, { shell: true })`
|
|
83
|
+
* call — so the call site only differs by routing decision, not by
|
|
84
|
+
* result handling.
|
|
85
|
+
*/
|
|
86
|
+
export declare function runSandboxed(command: string, opts: SandboxOpts): SpawnSyncReturns<string>;
|
|
87
|
+
/**
|
|
88
|
+
* Best-effort probe: would {@link runSandboxed} succeed on this
|
|
89
|
+
* machine without actually running a command? Used by the CLI's
|
|
90
|
+
* startup sanity check so we can warn at boot rather than at the
|
|
91
|
+
* first command. Returns `null` on success, a structured reason on
|
|
92
|
+
* failure.
|
|
93
|
+
*/
|
|
94
|
+
export declare function probeSandbox(): {
|
|
95
|
+
ok: true;
|
|
96
|
+
} | {
|
|
97
|
+
ok: false;
|
|
98
|
+
reason: string;
|
|
99
|
+
};
|
|
100
|
+
//# sourceMappingURL=os-sandbox.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"os-sandbox.d.ts","sourceRoot":"","sources":["../../src/runtime/os-sandbox.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsCG;AACH,OAAO,EAAa,KAAK,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AAKtE,MAAM,MAAM,eAAe,GAAG,QAAQ,GAAG,OAAO,CAAC;AAEjD,MAAM,WAAW,WAAW;IAC1B,qDAAqD;IACrD,GAAG,EAAE,MAAM,CAAC;IACZ;;;;;OAKG;IACH,iBAAiB,EAAE,MAAM,EAAE,CAAC;IAC5B;;;;OAIG;IACH,YAAY,EAAE,OAAO,CAAC;IACtB,iDAAiD;IACjD,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,2DAA2D;IAC3D,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,yCAAyC;IACzC,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;CACzB;AAED;;;;;GAKG;AACH,qBAAa,uBAAwB,SAAQ,KAAK;IAC7B,QAAQ,EAAE,MAAM,CAAC,QAAQ;IAAS,MAAM,EAAE,MAAM;gBAAhD,QAAQ,EAAE,MAAM,CAAC,QAAQ,EAAS,MAAM,EAAE,MAAM;CAIpE;AAID;;;;;;;;;GASG;AACH,wBAAgB,YAAY,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,WAAW,GAAG,gBAAgB,CAAC,MAAM,CAAC,CASzF;AAED;;;;;;GAMG;AACH,wBAAgB,YAAY,IAAI;IAAE,EAAE,EAAE,IAAI,CAAA;CAAE,GAAG;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAe3E"}
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* os-sandbox.ts — Phase 1.2 OS-level sandbox wrapper for `run_command`.
|
|
3
|
+
*
|
|
4
|
+
* The CLI already enforces a regex-based command-parser layer and
|
|
5
|
+
* a WorkspaceGuard path-boundary check before any shell command runs.
|
|
6
|
+
* That stops the obvious cases (`> /etc/passwd`, `sed -i ../foo`) but
|
|
7
|
+
* is not a complete defence against a confused or prompt-injected
|
|
8
|
+
* model — a regex blocklist is a speed bump, not a wall.
|
|
9
|
+
*
|
|
10
|
+
* This module adds an OS-enforced second layer that the model has no
|
|
11
|
+
* way around: `sandbox-exec` on macOS, `bwrap` on Linux. Both use the
|
|
12
|
+
* same OS primitives Apple's own `xcrun` and Flatpak rely on, so the
|
|
13
|
+
* deny semantics are exactly the kernel's, not a JavaScript regex.
|
|
14
|
+
*
|
|
15
|
+
* Design choices, deliberate:
|
|
16
|
+
*
|
|
17
|
+
* - Default is OFF (config `safety.sandboxMode === 'guard'`). Turning
|
|
18
|
+
* it on changes runtime behaviour and we want users to opt in
|
|
19
|
+
* explicitly, not be surprised by a build that suddenly fails
|
|
20
|
+
* because their `npm test` script writes to `~/.npm/_cacache`.
|
|
21
|
+
*
|
|
22
|
+
* - When ON and the platform binary is missing, we surface a clear
|
|
23
|
+
* structured error instead of silently downgrading. A user who
|
|
24
|
+
* opted into `os-sandbox` and got the regex layer would have the
|
|
25
|
+
* worst of both worlds — false safety.
|
|
26
|
+
*
|
|
27
|
+
* - Network is allowed by default — most legitimate build/test
|
|
28
|
+
* commands need it (npm install, cargo fetch, pip). Callers that
|
|
29
|
+
* want strict isolation can pass `allowNetwork: false`.
|
|
30
|
+
*
|
|
31
|
+
* - Reads are unrestricted. The agent legitimately needs to inspect
|
|
32
|
+
* the system to plan its work. Writes are the dangerous side.
|
|
33
|
+
*
|
|
34
|
+
* - The macOS profile is generated at runtime into the OS temp dir
|
|
35
|
+
* from a template (no static `.sb` file shipped) so allow-write
|
|
36
|
+
* paths can be parameterised per-invocation without macros.
|
|
37
|
+
* `sandbox-exec` is technically deprecated by Apple but still
|
|
38
|
+
* ships and works on darwin 25; what's available today wins.
|
|
39
|
+
*/
|
|
40
|
+
import { spawnSync } from 'node:child_process';
|
|
41
|
+
import fs from 'node:fs';
|
|
42
|
+
import os from 'node:os';
|
|
43
|
+
import path from 'node:path';
|
|
44
|
+
/**
|
|
45
|
+
* Why a sandboxed `run_command` couldn't actually be wrapped in an
|
|
46
|
+
* OS sandbox. Distinct from a *command-failed-inside-the-sandbox*
|
|
47
|
+
* error, which surfaces through {@link SpawnSyncReturns.status} as
|
|
48
|
+
* usual.
|
|
49
|
+
*/
|
|
50
|
+
export class SandboxUnavailableError extends Error {
|
|
51
|
+
platform;
|
|
52
|
+
reason;
|
|
53
|
+
constructor(platform, reason) {
|
|
54
|
+
super(`OS sandbox unavailable on ${platform}: ${reason}`);
|
|
55
|
+
this.platform = platform;
|
|
56
|
+
this.reason = reason;
|
|
57
|
+
this.name = 'SandboxUnavailableError';
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
/* ──────────────────────── Public entry point ──────────────────────── */
|
|
61
|
+
/**
|
|
62
|
+
* Run a shell command inside an OS-enforced sandbox.
|
|
63
|
+
*
|
|
64
|
+
* Throws {@link SandboxUnavailableError} if the current platform has
|
|
65
|
+
* no supported wrapper or the required binary is not on `$PATH`.
|
|
66
|
+
* Otherwise returns the same {@link SpawnSyncReturns} shape the
|
|
67
|
+
* caller would get from a plain `spawnSync(command, { shell: true })`
|
|
68
|
+
* call — so the call site only differs by routing decision, not by
|
|
69
|
+
* result handling.
|
|
70
|
+
*/
|
|
71
|
+
export function runSandboxed(command, opts) {
|
|
72
|
+
const platform = process.platform;
|
|
73
|
+
if (platform === 'darwin') {
|
|
74
|
+
return runSandboxedMacos(command, opts);
|
|
75
|
+
}
|
|
76
|
+
if (platform === 'linux') {
|
|
77
|
+
return runSandboxedLinux(command, opts);
|
|
78
|
+
}
|
|
79
|
+
throw new SandboxUnavailableError(platform, 'no supported OS sandbox wrapper for this platform');
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Best-effort probe: would {@link runSandboxed} succeed on this
|
|
83
|
+
* machine without actually running a command? Used by the CLI's
|
|
84
|
+
* startup sanity check so we can warn at boot rather than at the
|
|
85
|
+
* first command. Returns `null` on success, a structured reason on
|
|
86
|
+
* failure.
|
|
87
|
+
*/
|
|
88
|
+
export function probeSandbox() {
|
|
89
|
+
const platform = process.platform;
|
|
90
|
+
if (platform === 'darwin') {
|
|
91
|
+
const which = whichBinary('sandbox-exec');
|
|
92
|
+
return which ? { ok: true } : { ok: false, reason: '`sandbox-exec` not on $PATH (macOS system tool)' };
|
|
93
|
+
}
|
|
94
|
+
if (platform === 'linux') {
|
|
95
|
+
const which = whichBinary('bwrap');
|
|
96
|
+
if (which)
|
|
97
|
+
return { ok: true };
|
|
98
|
+
return {
|
|
99
|
+
ok: false,
|
|
100
|
+
reason: '`bwrap` (bubblewrap) not installed. Install with: apt install bubblewrap | dnf install bubblewrap | apk add bubblewrap',
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
return { ok: false, reason: `unsupported platform: ${platform}` };
|
|
104
|
+
}
|
|
105
|
+
/* ──────────────────────── macOS: sandbox-exec ──────────────────────── */
|
|
106
|
+
function runSandboxedMacos(command, opts) {
|
|
107
|
+
if (!whichBinary('sandbox-exec')) {
|
|
108
|
+
throw new SandboxUnavailableError('darwin', '`sandbox-exec` not on $PATH');
|
|
109
|
+
}
|
|
110
|
+
const profile = buildMacosProfile({
|
|
111
|
+
allowedWritePaths: dedupeAndResolve([...opts.allowedWritePaths, os.tmpdir()]),
|
|
112
|
+
allowNetwork: opts.allowNetwork,
|
|
113
|
+
});
|
|
114
|
+
// Write the profile to a temp file. sandbox-exec will read it
|
|
115
|
+
// before exec; we delete it immediately after the child returns.
|
|
116
|
+
const profilePath = path.join(os.tmpdir(), `fixo-sandbox-${process.pid}-${Date.now()}.sb`);
|
|
117
|
+
fs.writeFileSync(profilePath, profile, { encoding: 'utf-8', mode: 0o600 });
|
|
118
|
+
try {
|
|
119
|
+
return spawnSync('sandbox-exec', ['-f', profilePath, '/bin/sh', '-c', command], {
|
|
120
|
+
cwd: opts.cwd,
|
|
121
|
+
encoding: 'utf-8',
|
|
122
|
+
timeout: opts.timeout ?? 60_000,
|
|
123
|
+
maxBuffer: opts.maxBuffer ?? 1024 * 1024,
|
|
124
|
+
env: opts.env ?? process.env,
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
finally {
|
|
128
|
+
try {
|
|
129
|
+
fs.unlinkSync(profilePath);
|
|
130
|
+
}
|
|
131
|
+
catch { /* safe: best-effort cleanup of tmp profile */ }
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
function buildMacosProfile(opts) {
|
|
135
|
+
// Quote each allow-write path in TinyScheme-style string literal
|
|
136
|
+
// form. sandbox-exec uses backslash-escaped double-quoted strings.
|
|
137
|
+
const writeRoots = opts.allowedWritePaths
|
|
138
|
+
.map((p) => ` (subpath ${quoteSb(p)})`)
|
|
139
|
+
.join('\n');
|
|
140
|
+
const networkRule = opts.allowNetwork ? '(allow network*)' : '(deny network*)';
|
|
141
|
+
// Inspired by Apple's own /System/Library/Sandbox/Profiles seed
|
|
142
|
+
// profiles. `allow file-read*` is total because the agent must be
|
|
143
|
+
// able to read the system to do useful work; writes are the side
|
|
144
|
+
// we lock down. The auxiliary `allow` rules (sysctl, mach-lookup,
|
|
145
|
+
// process-fork, ipc-posix-shm) are the bare minimum a `/bin/sh`
|
|
146
|
+
// exec of a typical build command needs to not crash.
|
|
147
|
+
return `(version 1)
|
|
148
|
+
(deny default)
|
|
149
|
+
|
|
150
|
+
; Reads: unrestricted. The agent needs to inspect the system.
|
|
151
|
+
(allow file-read*)
|
|
152
|
+
|
|
153
|
+
; Writes: only inside the explicitly-allowed roots.
|
|
154
|
+
(allow file-write*
|
|
155
|
+
${writeRoots})
|
|
156
|
+
|
|
157
|
+
; Process / sysctl / IPC: required for /bin/sh to execute a command.
|
|
158
|
+
(allow process-fork)
|
|
159
|
+
(allow process-exec*)
|
|
160
|
+
(allow sysctl-read)
|
|
161
|
+
(allow mach-lookup)
|
|
162
|
+
(allow file-ioctl)
|
|
163
|
+
(allow ipc-posix-shm)
|
|
164
|
+
(allow signal (target self))
|
|
165
|
+
|
|
166
|
+
; Network rule (gated by caller).
|
|
167
|
+
${networkRule}
|
|
168
|
+
`;
|
|
169
|
+
}
|
|
170
|
+
function quoteSb(s) {
|
|
171
|
+
// sandbox-exec strings use double-quotes with backslash escaping.
|
|
172
|
+
return `"${s.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`;
|
|
173
|
+
}
|
|
174
|
+
/* ──────────────────────── Linux: bubblewrap ──────────────────────── */
|
|
175
|
+
function runSandboxedLinux(command, opts) {
|
|
176
|
+
if (!whichBinary('bwrap')) {
|
|
177
|
+
throw new SandboxUnavailableError('linux', '`bwrap` (bubblewrap) not installed. Install with: apt install bubblewrap | dnf install bubblewrap | apk add bubblewrap');
|
|
178
|
+
}
|
|
179
|
+
const writeRoots = dedupeAndResolve([...opts.allowedWritePaths, os.tmpdir()]);
|
|
180
|
+
const args = [
|
|
181
|
+
// Read-only mount the standard system tree. /usr is the
|
|
182
|
+
// important one — that's where /bin/sh, coreutils, etc. live.
|
|
183
|
+
'--ro-bind', '/usr', '/usr',
|
|
184
|
+
'--ro-bind', '/bin', '/bin',
|
|
185
|
+
'--ro-bind', '/sbin', '/sbin',
|
|
186
|
+
'--ro-bind', '/lib', '/lib',
|
|
187
|
+
'--symlink', 'usr/lib64', '/lib64',
|
|
188
|
+
'--ro-bind', '/etc', '/etc',
|
|
189
|
+
// Procfs + devtmpfs: needed by virtually every binary.
|
|
190
|
+
'--proc', '/proc',
|
|
191
|
+
'--dev', '/dev',
|
|
192
|
+
// Tmpfs for /run and /var so the child can drop pidfiles.
|
|
193
|
+
'--tmpfs', '/run',
|
|
194
|
+
'--tmpfs', '/var',
|
|
195
|
+
];
|
|
196
|
+
for (const wr of writeRoots) {
|
|
197
|
+
args.push('--bind', wr, wr);
|
|
198
|
+
}
|
|
199
|
+
if (!opts.allowNetwork) {
|
|
200
|
+
args.push('--unshare-net');
|
|
201
|
+
}
|
|
202
|
+
// PID + user namespaces — cheap isolation that doesn't break
|
|
203
|
+
// most build tools.
|
|
204
|
+
args.push('--unshare-pid');
|
|
205
|
+
args.push('--die-with-parent');
|
|
206
|
+
args.push('--chdir', opts.cwd);
|
|
207
|
+
args.push('/bin/sh', '-c', command);
|
|
208
|
+
return spawnSync('bwrap', args, {
|
|
209
|
+
cwd: opts.cwd,
|
|
210
|
+
encoding: 'utf-8',
|
|
211
|
+
timeout: opts.timeout ?? 60_000,
|
|
212
|
+
maxBuffer: opts.maxBuffer ?? 1024 * 1024,
|
|
213
|
+
env: opts.env ?? process.env,
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
/* ──────────────────────── Helpers ──────────────────────── */
|
|
217
|
+
function whichBinary(name) {
|
|
218
|
+
const which = spawnSync('which', [name], { encoding: 'utf-8' });
|
|
219
|
+
return which.status === 0 && which.stdout.trim().length > 0;
|
|
220
|
+
}
|
|
221
|
+
function dedupeAndResolve(paths) {
|
|
222
|
+
const seen = new Set();
|
|
223
|
+
const out = [];
|
|
224
|
+
for (const p of paths) {
|
|
225
|
+
// Canonicalize: on macOS `/var/folders/...` is actually
|
|
226
|
+
// `/private/var/folders/...`; sandbox-exec matches on the
|
|
227
|
+
// canonical path, so an allow rule written against the symlink
|
|
228
|
+
// path silently denies. Same trap exists for `/tmp` →
|
|
229
|
+
// `/private/tmp`. realpathSync resolves both. Fall back to the
|
|
230
|
+
// resolved-but-uncanonicalized path if the directory does not
|
|
231
|
+
// exist yet (callers may pass it before creation).
|
|
232
|
+
let abs;
|
|
233
|
+
try {
|
|
234
|
+
abs = fs.realpathSync(path.resolve(p));
|
|
235
|
+
}
|
|
236
|
+
catch {
|
|
237
|
+
abs = path.resolve(p);
|
|
238
|
+
}
|
|
239
|
+
if (seen.has(abs))
|
|
240
|
+
continue;
|
|
241
|
+
seen.add(abs);
|
|
242
|
+
out.push(abs);
|
|
243
|
+
}
|
|
244
|
+
return out;
|
|
245
|
+
}
|
|
246
|
+
//# sourceMappingURL=os-sandbox.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"os-sandbox.js","sourceRoot":"","sources":["../../src/runtime/os-sandbox.ts"],"names":[],"mappings":"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"}
|