profclaw 2.3.0 → 2.4.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 (60) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/README.md +5 -2
  3. package/config/pricing.yml +26 -2
  4. package/dist/agents/command-sandbox.d.ts +93 -0
  5. package/dist/agents/command-sandbox.js +235 -0
  6. package/dist/agents/executor-runner.d.ts +135 -0
  7. package/dist/agents/executor-runner.js +303 -0
  8. package/dist/agents/executor.d.ts +2 -0
  9. package/dist/agents/executor.js +35 -2
  10. package/dist/agents/output-truncator.d.ts +59 -0
  11. package/dist/agents/output-truncator.js +128 -0
  12. package/dist/agents/receipt/receipt-cli.d.ts +43 -0
  13. package/dist/agents/receipt/receipt-cli.js +82 -0
  14. package/dist/agents/receipt/receipt-types.d.ts +136 -0
  15. package/dist/agents/receipt/receipt-types.js +19 -0
  16. package/dist/agents/receipt/receipt-verify.d.ts +19 -0
  17. package/dist/agents/receipt/receipt-verify.js +176 -0
  18. package/dist/agents/receipt/receipt-view.d.ts +29 -0
  19. package/dist/agents/receipt/receipt-view.js +227 -0
  20. package/dist/agents/receipt/receipt-writer.d.ts +47 -0
  21. package/dist/agents/receipt/receipt-writer.js +186 -0
  22. package/dist/agents/result-store.d.ts +6 -0
  23. package/dist/agents/result-store.js +43 -2
  24. package/dist/agents/session-diff.d.ts +6 -0
  25. package/dist/agents/session-diff.js +10 -19
  26. package/dist/agents/tool-selector.d.ts +123 -0
  27. package/dist/agents/tool-selector.js +321 -0
  28. package/dist/agents/verified-run-git.d.ts +49 -0
  29. package/dist/agents/verified-run-git.js +82 -0
  30. package/dist/agents/verified-run.d.ts +149 -0
  31. package/dist/agents/verified-run.js +402 -0
  32. package/dist/agents/verifier.d.ts +64 -0
  33. package/dist/agents/verifier.js +186 -0
  34. package/dist/agents/worktree-tools.d.ts +47 -0
  35. package/dist/agents/worktree-tools.js +246 -0
  36. package/dist/chat/agentic-executor.js +14 -6
  37. package/dist/chat/execution/sandbox.d.ts +8 -0
  38. package/dist/chat/execution/sandbox.js +25 -1
  39. package/dist/chat/execution/tools/tts-speak.d.ts +2 -2
  40. package/dist/chat/meta-tool-handlers.d.ts +18 -0
  41. package/dist/chat/meta-tool-handlers.js +51 -0
  42. package/dist/chat/system-prompts.js +6 -1
  43. package/dist/chat/tool-handler.d.ts +2 -0
  44. package/dist/chat/tool-handler.js +47 -3
  45. package/dist/cli/commands/receipt.d.ts +7 -0
  46. package/dist/cli/commands/receipt.js +90 -0
  47. package/dist/cli/commands/run.d.ts +28 -0
  48. package/dist/cli/commands/run.js +204 -0
  49. package/dist/cli/index.js +4 -0
  50. package/dist/costs/token-tracker.d.ts +10 -1
  51. package/dist/costs/token-tracker.js +37 -5
  52. package/dist/providers/ai-sdk.js +33 -37
  53. package/dist/providers/core/models.js +59 -17
  54. package/dist/providers/core/types.d.ts +4 -0
  55. package/dist/providers/prompt-cache.d.ts +68 -0
  56. package/dist/providers/prompt-cache.js +165 -0
  57. package/dist/providers/smart-router.js +11 -3
  58. package/dist/routes/chat.js +22 -5
  59. package/dist/server.js +3 -0
  60. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -2,6 +2,21 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file.
4
4
 
5
+ ## [2.4.0](https://github.com/profclaw/profclaw/compare/v2.3.0...v2.4.0) (2026-10-05)
6
+
7
+
8
+ ### Features
9
+
10
+ * OS-level sandbox for the run bash tool ([#71](https://github.com/profclaw/profclaw/issues/71)) ([cc6ab33](https://github.com/profclaw/profclaw/commit/cc6ab33a7f683b20137b118e600300a200e88d72))
11
+ * tamper-evident run receipts with verify and HTML viewer ([#74](https://github.com/profclaw/profclaw/issues/74)) ([322307f](https://github.com/profclaw/profclaw/commit/322307f35f843825d51f7f87ac2ee4ac2ee0af88))
12
+ * token-efficient harness and verified autonomous run ([#68](https://github.com/profclaw/profclaw/issues/68)) ([68826ca](https://github.com/profclaw/profclaw/commit/68826ca9cd627aac6a6a9119b1c2178d6b7407be))
13
+
14
+
15
+ ### Bug Fixes
16
+
17
+ * judge the first run attempt against a baseline, fix wrong unified diffs ([#70](https://github.com/profclaw/profclaw/issues/70)) ([19b47ad](https://github.com/profclaw/profclaw/commit/19b47ad6aa330e557f202c8a00d44d371153eb81))
18
+ * time out the Docker ping so a wedged daemon cannot hang startup ([#72](https://github.com/profclaw/profclaw/issues/72)) ([72dd4f2](https://github.com/profclaw/profclaw/commit/72dd4f2ef42df0a0e5624518fb3b1b02ff0e374e))
19
+
5
20
  ## [2.3.0](https://github.com/profclaw/profclaw/compare/v2.2.0...v2.3.0) (2026-04-13)
6
21
 
7
22
 
package/README.md CHANGED
@@ -17,12 +17,13 @@
17
17
  <img src="https://img.shields.io/badge/providers-35-f43f5e?style=for-the-badge" alt="35 providers">
18
18
  <img src="https://img.shields.io/badge/tools-72-fb7185?style=for-the-badge" alt="72 tools">
19
19
  <img src="https://img.shields.io/badge/channels-22-e11d48?style=for-the-badge" alt="22 channels">
20
+ <a href="https://discord.gg/Ar5pcaZB99"><img src="https://img.shields.io/discord/829168897080557579?style=for-the-badge&logo=discord&logoColor=white&label=discord&color=5865F2" alt="Discord"></a>
20
21
  </p>
21
22
 
22
23
  <p align="center">
23
24
  <a href="https://profclaw.ai/docs"><b>Docs</b></a> &middot;
24
25
  <a href="https://github.com/profclaw/profclaw/issues"><b>Report Bug</b></a> &middot;
25
- <a href="https://discord.gg/profclaw"><b>Discord</b></a>
26
+ <a href="https://discord.gg/Ar5pcaZB99"><b>Discord</b></a>
26
27
  </p>
27
28
 
28
29
  ---
@@ -388,7 +389,9 @@ See [SECURITY.md](SECURITY.md) for our security policy and how to report vulnera
388
389
  - [Quickstart guide](https://docs.profclaw.ai/guides/quickstart)
389
390
  - [Full docs](https://docs.profclaw.ai)
390
391
  - [Report a bug](https://github.com/profclaw/profclaw/issues/new?template=bug_report.md)
391
- - [Discord](https://discord.gg/profclaw)
392
+ - [Discord](https://discord.gg/Ar5pcaZB99): the #profclaw forum on the GLINR community server
393
+
394
+ <a href="https://discord.gg/Ar5pcaZB99"><img src="https://discord.com/api/guilds/829168897080557579/widget.png?style=banner2" alt="Join the GLINR Discord" /></a>
392
395
 
393
396
  ## License
394
397
 
@@ -3,6 +3,24 @@
3
3
 
4
4
  pricing:
5
5
  # Anthropic Models
6
+ # Verified: https://platform.claude.com/docs/en/about-claude/models/overview (2026-09-26)
7
+ claude-fable-5-1:
8
+ provider: anthropic
9
+ inputRate: 10.00
10
+ outputRate: 50.00
11
+ claude-opus-5-5:
12
+ provider: anthropic
13
+ inputRate: 4.00
14
+ outputRate: 20.00
15
+ claude-sonnet-5:
16
+ provider: anthropic
17
+ inputRate: 2.00
18
+ outputRate: 10.00
19
+ claude-haiku-4-5:
20
+ provider: anthropic
21
+ inputRate: 1.00
22
+ outputRate: 5.00
23
+ # Legacy (unverified this pass)
6
24
  claude-3-5-sonnet:
7
25
  provider: anthropic
8
26
  inputRate: 3.00
@@ -16,7 +34,7 @@ pricing:
16
34
  inputRate: 0.25
17
35
  outputRate: 1.25
18
36
 
19
- # OpenAI Models
37
+ # OpenAI Models (unverified this pass, left unchanged)
20
38
  gpt-4o:
21
39
  provider: openai
22
40
  inputRate: 5.00
@@ -30,7 +48,7 @@ pricing:
30
48
  inputRate: 0.50
31
49
  outputRate: 1.50
32
50
 
33
- # Google Models
51
+ # Google Models (unverified this pass, left unchanged)
34
52
  gemini-1.5-pro:
35
53
  provider: google
36
54
  inputRate: 3.50
@@ -40,6 +58,12 @@ pricing:
40
58
  inputRate: 0.35
41
59
  outputRate: 1.05
42
60
 
61
+ # Local open-weight models via Ollama: no per-token cost
62
+ gemma4:
63
+ provider: other
64
+ inputRate: 0
65
+ outputRate: 0
66
+
43
67
  defaultPricing:
44
68
  provider: other
45
69
  inputRate: 5.00
@@ -0,0 +1,93 @@
1
+ /**
2
+ * OS-level confinement for shell commands run by `profclaw run`.
3
+ *
4
+ * Wraps a command so the kernel, not pattern matching, enforces the limits:
5
+ * - writes only inside the worktree, a per-run temp dir, the git metadata the
6
+ * worktree needs and a few device nodes (plus opt-in extras)
7
+ * - reads are broad (node, pnpm, git and tsc need many system paths) but
8
+ * credential directories such as ~/.ssh and ~/.aws are denied
9
+ * - network is denied unless PROFCLAW_RUN_ALLOW_NETWORK is set
10
+ *
11
+ * Backends: macOS Seatbelt (`/usr/bin/sandbox-exec`) and Linux `bwrap`.
12
+ * Modes (PROFCLAW_RUN_SANDBOX): `auto` uses a sandbox when available and
13
+ * otherwise falls back to pattern screening with a one-time warning, `on`
14
+ * fails closed, `off` disables.
15
+ *
16
+ * Known limits: a writable git dir can be used to plant state outside the
17
+ * worktree (hooks and config are denied, other refs are not), and a sandboxed
18
+ * process can still read anything not in the deny list.
19
+ */
20
+ export type SandboxMode = 'auto' | 'on' | 'off';
21
+ export type SandboxBackend = 'seatbelt' | 'bwrap' | 'none';
22
+ export interface SandboxAvailability {
23
+ backend: SandboxBackend;
24
+ /** Absolute path of the sandbox binary */
25
+ binary: string;
26
+ }
27
+ export interface SandboxOptions {
28
+ workdir: string;
29
+ /** Per-run scratch directory that is writable */
30
+ tempDir: string;
31
+ /** Home directory whose credential dirs are denied (default: os.homedir()) */
32
+ home?: string;
33
+ allowNetwork?: boolean;
34
+ /** Extra writable paths (e.g. the git dir of a linked worktree) */
35
+ extraWritable?: string[];
36
+ /** Paths inside an extraWritable tree that stay read-only (e.g. hooks) */
37
+ extraReadOnly?: string[];
38
+ /** Extra paths whose reads are denied */
39
+ extraDenyRead?: string[];
40
+ }
41
+ export interface SandboxPlan {
42
+ backend: SandboxBackend;
43
+ file: string;
44
+ args: string[];
45
+ /** Environment overrides the caller must merge in (TMPDIR) */
46
+ env: Record<string, string>;
47
+ }
48
+ export type DetectFn = () => SandboxAvailability;
49
+ /** Home-relative locations holding credentials. Reads are denied. */
50
+ export declare const SENSITIVE_HOME_PATHS: string[];
51
+ /** Absolute credential locations outside of HOME. */
52
+ export declare const SENSITIVE_ABSOLUTE_PATHS: string[];
53
+ export declare function resolveSandboxMode(env?: NodeJS.ProcessEnv): SandboxMode;
54
+ export declare function resolveAllowNetwork(env?: NodeJS.ProcessEnv): boolean;
55
+ /** Extra writable paths from PROFCLAW_RUN_SANDBOX_WRITABLE (path-delimiter separated). */
56
+ export declare function resolveExtraWritable(env?: NodeJS.ProcessEnv): string[];
57
+ /** Resolve symlinks (Seatbelt matches real paths, e.g. /tmp is /private/tmp). */
58
+ export declare function canonicalPath(path: string): string;
59
+ /**
60
+ * Quote a path as an SBPL string literal. Backslash and double quote are
61
+ * escaped; control characters cannot be represented safely and are rejected,
62
+ * so a crafted path can never terminate the literal and inject rules.
63
+ */
64
+ export declare function escapeSeatbeltString(value: string): string;
65
+ /**
66
+ * Build the Seatbelt profile. SBPL applies the last matching rule, so the
67
+ * broad allow comes first and the specific denies and re-allows follow.
68
+ */
69
+ export declare function buildSeatbeltProfile(options: SandboxOptions): string;
70
+ export declare function buildBwrapArgs(options: SandboxOptions): string[];
71
+ /**
72
+ * Detect a working sandbox. A probe run confirms it functions, since
73
+ * sandbox-exec fails when already inside a sandbox and bwrap can be blocked
74
+ * by unprivileged user namespace policy.
75
+ */
76
+ export declare function detectSandbox(): SandboxAvailability;
77
+ /** Test hook: allow the one-time warning to fire again. */
78
+ export declare function resetSandboxWarning(): void;
79
+ /** One-line description for CLI output. */
80
+ export declare function describeSandbox(mode: SandboxMode, detect?: DetectFn, allowNetwork?: boolean): string;
81
+ export interface PlanOptions {
82
+ mode?: SandboxMode;
83
+ detect?: DetectFn;
84
+ /** Receives the one-time fallback warning (default: stderr) */
85
+ warn?: (message: string) => void;
86
+ shell?: string;
87
+ }
88
+ /**
89
+ * Turn a shell command into the process to spawn. Throws in `on` mode when no
90
+ * sandbox is available. In `auto` mode falls back to the plain shell and warns once.
91
+ */
92
+ export declare function planSandboxedCommand(command: string, options: SandboxOptions, plan?: PlanOptions): SandboxPlan;
93
+ //# sourceMappingURL=command-sandbox.d.ts.map
@@ -0,0 +1,235 @@
1
+ /**
2
+ * OS-level confinement for shell commands run by `profclaw run`.
3
+ *
4
+ * Wraps a command so the kernel, not pattern matching, enforces the limits:
5
+ * - writes only inside the worktree, a per-run temp dir, the git metadata the
6
+ * worktree needs and a few device nodes (plus opt-in extras)
7
+ * - reads are broad (node, pnpm, git and tsc need many system paths) but
8
+ * credential directories such as ~/.ssh and ~/.aws are denied
9
+ * - network is denied unless PROFCLAW_RUN_ALLOW_NETWORK is set
10
+ *
11
+ * Backends: macOS Seatbelt (`/usr/bin/sandbox-exec`) and Linux `bwrap`.
12
+ * Modes (PROFCLAW_RUN_SANDBOX): `auto` uses a sandbox when available and
13
+ * otherwise falls back to pattern screening with a one-time warning, `on`
14
+ * fails closed, `off` disables.
15
+ *
16
+ * Known limits: a writable git dir can be used to plant state outside the
17
+ * worktree (hooks and config are denied, other refs are not), and a sandboxed
18
+ * process can still read anything not in the deny list.
19
+ */
20
+ import { spawnSync } from 'node:child_process';
21
+ import { existsSync, realpathSync, statSync } from 'node:fs';
22
+ import { homedir } from 'node:os';
23
+ import { delimiter, join, resolve } from 'node:path';
24
+ /** Home-relative locations holding credentials. Reads are denied. */
25
+ export const SENSITIVE_HOME_PATHS = [
26
+ '.ssh',
27
+ '.aws',
28
+ '.config/gh',
29
+ '.gnupg',
30
+ '.netrc',
31
+ '.git-credentials',
32
+ '.kube',
33
+ '.azure',
34
+ '.config/gcloud',
35
+ '.docker/config.json',
36
+ 'Library/Keychains',
37
+ ];
38
+ /** Absolute credential locations outside of HOME. */
39
+ export const SENSITIVE_ABSOLUTE_PATHS = ['/Library/Keychains', '/private/var/db/dslocal'];
40
+ // Environment parsing
41
+ export function resolveSandboxMode(env = process.env) {
42
+ const raw = (env.PROFCLAW_RUN_SANDBOX ?? 'auto').trim().toLowerCase();
43
+ if (raw === 'on' || raw === 'off' || raw === 'auto')
44
+ return raw;
45
+ if (raw === '1' || raw === 'true')
46
+ return 'on';
47
+ if (raw === '0' || raw === 'false')
48
+ return 'off';
49
+ throw new Error(`Invalid PROFCLAW_RUN_SANDBOX "${raw}" (expected auto, on or off)`);
50
+ }
51
+ export function resolveAllowNetwork(env = process.env) {
52
+ const raw = (env.PROFCLAW_RUN_ALLOW_NETWORK ?? '').trim().toLowerCase();
53
+ return raw === '1' || raw === 'true' || raw === 'yes';
54
+ }
55
+ /** Extra writable paths from PROFCLAW_RUN_SANDBOX_WRITABLE (path-delimiter separated). */
56
+ export function resolveExtraWritable(env = process.env) {
57
+ return (env.PROFCLAW_RUN_SANDBOX_WRITABLE ?? '')
58
+ .split(delimiter)
59
+ .map((p) => p.trim())
60
+ .filter((p) => p.length > 0);
61
+ }
62
+ // Path handling
63
+ /** Resolve symlinks (Seatbelt matches real paths, e.g. /tmp is /private/tmp). */
64
+ export function canonicalPath(path) {
65
+ const absolute = resolve(path);
66
+ try {
67
+ return realpathSync(absolute);
68
+ }
69
+ catch {
70
+ return absolute;
71
+ }
72
+ }
73
+ /**
74
+ * Quote a path as an SBPL string literal. Backslash and double quote are
75
+ * escaped; control characters cannot be represented safely and are rejected,
76
+ * so a crafted path can never terminate the literal and inject rules.
77
+ */
78
+ export function escapeSeatbeltString(value) {
79
+ if (/[\u0000-\u001f\u007f]/.test(value))
80
+ throw new Error('Path contains control characters and cannot be sandboxed');
81
+ return `"${value.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`;
82
+ }
83
+ function subpath(path) {
84
+ return `(subpath ${escapeSeatbeltString(path)})`;
85
+ }
86
+ function deniedReadPaths(options) {
87
+ const home = canonicalPath(options.home ?? homedir());
88
+ return [
89
+ ...SENSITIVE_HOME_PATHS.map((p) => join(home, p)),
90
+ ...SENSITIVE_ABSOLUTE_PATHS,
91
+ ...(options.extraDenyRead ?? []).map(canonicalPath),
92
+ ];
93
+ }
94
+ // Seatbelt
95
+ /**
96
+ * Build the Seatbelt profile. SBPL applies the last matching rule, so the
97
+ * broad allow comes first and the specific denies and re-allows follow.
98
+ */
99
+ export function buildSeatbeltProfile(options) {
100
+ const writable = [options.workdir, options.tempDir, ...(options.extraWritable ?? [])].map(canonicalPath);
101
+ const readOnly = (options.extraReadOnly ?? []).map(canonicalPath);
102
+ const lines = [
103
+ '(version 1)',
104
+ '(allow default)',
105
+ '(deny file-write*)',
106
+ `(allow file-write* ${writable.map(subpath).join(' ')})`,
107
+ '(allow file-write* (literal "/dev/null") (literal "/dev/zero") (literal "/dev/tty") (literal "/dev/dtracehelper"))',
108
+ ];
109
+ if (readOnly.length > 0)
110
+ lines.push(`(deny file-write* ${readOnly.map(subpath).join(' ')})`);
111
+ lines.push(`(deny file-read* ${deniedReadPaths(options).map(subpath).join(' ')})`);
112
+ if (options.allowNetwork !== true)
113
+ lines.push('(deny network*)');
114
+ return `${lines.join('\n')}\n`;
115
+ }
116
+ // bubblewrap
117
+ function isDirectory(path) {
118
+ try {
119
+ return statSync(path).isDirectory();
120
+ }
121
+ catch {
122
+ return false;
123
+ }
124
+ }
125
+ export function buildBwrapArgs(options) {
126
+ const workdir = canonicalPath(options.workdir);
127
+ const tempDir = canonicalPath(options.tempDir);
128
+ const args = ['--ro-bind', '/', '/', '--dev', '/dev', '--proc', '/proc', '--die-with-parent', '--new-session'];
129
+ for (const path of [workdir, tempDir, ...(options.extraWritable ?? []).map(canonicalPath)]) {
130
+ args.push('--bind', path, path);
131
+ }
132
+ for (const path of (options.extraReadOnly ?? []).map(canonicalPath)) {
133
+ if (existsSync(path))
134
+ args.push('--ro-bind', path, path);
135
+ }
136
+ for (const path of deniedReadPaths(options)) {
137
+ if (!existsSync(path))
138
+ continue;
139
+ // Directories are hidden behind an empty tmpfs, files behind /dev/null.
140
+ args.push(...(isDirectory(path) ? ['--tmpfs', path] : ['--ro-bind', '/dev/null', path]));
141
+ }
142
+ if (options.allowNetwork !== true)
143
+ args.push('--unshare-net');
144
+ args.push('--chdir', workdir);
145
+ return args;
146
+ }
147
+ // Detection
148
+ let cachedAvailability;
149
+ function findOnPath(name) {
150
+ for (const dir of (process.env.PATH ?? '').split(delimiter)) {
151
+ if (dir.length > 0 && existsSync(join(dir, name)))
152
+ return join(dir, name);
153
+ }
154
+ return undefined;
155
+ }
156
+ /**
157
+ * Detect a working sandbox. A probe run confirms it functions, since
158
+ * sandbox-exec fails when already inside a sandbox and bwrap can be blocked
159
+ * by unprivileged user namespace policy.
160
+ */
161
+ export function detectSandbox() {
162
+ if (cachedAvailability)
163
+ return cachedAvailability;
164
+ let found = { backend: 'none', binary: '' };
165
+ if (process.platform === 'darwin' && existsSync('/usr/bin/sandbox-exec')) {
166
+ const probe = spawnSync('/usr/bin/sandbox-exec', ['-p', '(version 1)(allow default)', '/usr/bin/true']);
167
+ if (probe.status === 0)
168
+ found = { backend: 'seatbelt', binary: '/usr/bin/sandbox-exec' };
169
+ }
170
+ else if (process.platform === 'linux') {
171
+ const bwrap = findOnPath('bwrap');
172
+ if (bwrap) {
173
+ const probe = spawnSync(bwrap, ['--ro-bind', '/', '/', '--unshare-net', 'true']);
174
+ if (probe.status === 0)
175
+ found = { backend: 'bwrap', binary: bwrap };
176
+ }
177
+ }
178
+ cachedAvailability = found;
179
+ return found;
180
+ }
181
+ // Planning
182
+ let warned = false;
183
+ /** Test hook: allow the one-time warning to fire again. */
184
+ export function resetSandboxWarning() {
185
+ warned = false;
186
+ }
187
+ /** One-line description for CLI output. */
188
+ export function describeSandbox(mode, detect = detectSandbox, allowNetwork = resolveAllowNetwork()) {
189
+ if (mode === 'off')
190
+ return 'Sandbox: off (pattern screening only)';
191
+ const { backend } = detect();
192
+ if (backend === 'none') {
193
+ return mode === 'on'
194
+ ? 'Sandbox: required but unavailable, bash commands will fail'
195
+ : 'Sandbox: unavailable, bash commands are pattern screened only';
196
+ }
197
+ return `Sandbox: ${mode} (${backend}, network ${allowNetwork ? 'allowed' : 'denied'})`;
198
+ }
199
+ /**
200
+ * Turn a shell command into the process to spawn. Throws in `on` mode when no
201
+ * sandbox is available. In `auto` mode falls back to the plain shell and warns once.
202
+ */
203
+ export function planSandboxedCommand(command, options, plan = {}) {
204
+ const mode = plan.mode ?? resolveSandboxMode();
205
+ const shell = plan.shell ?? '/bin/sh';
206
+ const plain = { backend: 'none', file: shell, args: ['-c', command], env: {} };
207
+ if (mode === 'off')
208
+ return plain;
209
+ const available = (plan.detect ?? detectSandbox)();
210
+ if (available.backend === 'none') {
211
+ if (mode === 'on') {
212
+ throw new Error('PROFCLAW_RUN_SANDBOX=on but no sandbox is available (need sandbox-exec on macOS or bwrap on Linux)');
213
+ }
214
+ if (!warned) {
215
+ warned = true;
216
+ const message = '[profclaw] No OS sandbox available, shell commands are only pattern screened (set PROFCLAW_RUN_SANDBOX=on to require one).\n';
217
+ if (plan.warn)
218
+ plan.warn(message);
219
+ else
220
+ process.stderr.write(message);
221
+ }
222
+ return plain;
223
+ }
224
+ const env = { TMPDIR: canonicalPath(options.tempDir) };
225
+ if (available.backend === 'seatbelt') {
226
+ return {
227
+ backend: 'seatbelt',
228
+ file: available.binary,
229
+ args: ['-p', buildSeatbeltProfile(options), shell, '-c', command],
230
+ env,
231
+ };
232
+ }
233
+ return { backend: 'bwrap', file: available.binary, args: [...buildBwrapArgs(options), shell, '-c', command], env };
234
+ }
235
+ //# sourceMappingURL=command-sandbox.js.map
@@ -0,0 +1,135 @@
1
+ /**
2
+ * ExecutorRunner: the built-in model-backed AgentRunner for verified runs.
3
+ *
4
+ * Adapts AgentExecutor to the AgentRunner seam used by runVerifiedGoal:
5
+ * - runs the executor with the run's worktree as the only place it can touch
6
+ * (worktree-confined tools plus the PermissionManager, never push or PR);
7
+ * - feeds it the goal plus the previous attempt's trimmed verifier output;
8
+ * - reports tokens, dollars and cache hit rate so the loop's budget guard works,
9
+ * and cancels an attempt mid-flight when the remaining budget is spent;
10
+ * - cascades models: starts on the smart router's cheap pick and moves one
11
+ * tier up after N consecutive verifier failures (N is configurable).
12
+ *
13
+ * AgentExecutor is used as-is. Cache usage is read from its public
14
+ * `step:complete` event, so the executor internals are not touched.
15
+ */
16
+ import type { LanguageModel, ModelMessage, ToolSet } from 'ai';
17
+ import type { ToolExecuteHandler } from './executor.js';
18
+ import type { AgentConfig, AgentState } from './types.js';
19
+ import type { AgentAttemptInput, AgentAttemptResult, AgentRunner } from './verified-run.js';
20
+ import type { PermissionManager } from './permissions.js';
21
+ import type { ComplexityTier } from '../providers/smart-router.js';
22
+ import type { ProviderType } from '../providers/core/types.js';
23
+ import type { PromptCacheConfig } from '../providers/prompt-cache.js';
24
+ export interface ModelChoice {
25
+ provider: ProviderType;
26
+ model: string;
27
+ costPer1MInput: number;
28
+ costPer1MOutput: number;
29
+ /** Router tier the pick came from, or 'pinned' for an explicit --model */
30
+ tier: ComplexityTier | 'pinned';
31
+ }
32
+ /**
33
+ * Build the ordered list of models an attempt series may use.
34
+ *
35
+ * `modelSpec` (comma separated) pins an explicit ladder, cheapest first.
36
+ * Without it the smart router classifies the goal, picks the cheap model for
37
+ * that tier, and appends the picks for each higher tier (deduplicated).
38
+ */
39
+ export declare function buildModelLadder(goal: string, availableProviders: Set<ProviderType>, modelSpec?: string): ModelChoice[];
40
+ export interface UsageTotals {
41
+ inputTokens: number;
42
+ outputTokens: number;
43
+ cacheReadTokens: number;
44
+ cacheWriteTokens: number;
45
+ }
46
+ /** Dollar cost with cache reads and writes priced by their multipliers. */
47
+ export declare function computeCostUsd(choice: ModelChoice, usage: UsageTotals, cache: PromptCacheConfig): number;
48
+ export interface ExecutorRunnerConfig {
49
+ /** Consecutive verifier failures on a model before moving up a tier. 0 disables cascading. */
50
+ escalateAfter: number;
51
+ maxStepsPerAttempt: number;
52
+ /** Token cap per attempt (also bounded by the run's remaining budget) */
53
+ attemptTokenBudget: number;
54
+ commandTimeoutMs: number;
55
+ maxToolOutputChars: number;
56
+ }
57
+ export declare const DEFAULT_RUNNER_CONFIG: ExecutorRunnerConfig;
58
+ /** Precedence: explicit flag > PROFCLAW_RUN_* env var > default. */
59
+ export declare function resolveRunnerConfig(flags: Partial<ExecutorRunnerConfig>, env?: NodeJS.ProcessEnv): ExecutorRunnerConfig;
60
+ /** The slice of AgentExecutor the runner uses; lets tests inject a fake. */
61
+ export interface AgentLoop {
62
+ run(model: LanguageModel, messages: ModelMessage[], tools: ToolSet, onToolExecute?: ToolExecuteHandler, providerHint?: string): Promise<AgentState>;
63
+ cancel(): void;
64
+ getState(): AgentState;
65
+ on(event: 'step:complete', listener: (state: AgentState, result: unknown) => void): unknown;
66
+ }
67
+ export interface CreateLoopArgs {
68
+ sessionId: string;
69
+ goal: string;
70
+ config: Partial<AgentConfig>;
71
+ }
72
+ export type RunnerEvent = {
73
+ type: 'model_selected';
74
+ attempt: number;
75
+ choice: ModelChoice;
76
+ escalated: boolean;
77
+ };
78
+ /** One tool invocation by the agent, reported after it finishes. */
79
+ export interface ToolCallEvent {
80
+ attempt: number;
81
+ name: string;
82
+ args: Record<string, unknown>;
83
+ durationMs: number;
84
+ ok: boolean;
85
+ }
86
+ export interface ExecutorRunnerOptions {
87
+ ladder: ModelChoice[];
88
+ createModel: (choice: ModelChoice) => Promise<LanguageModel>;
89
+ config?: Partial<ExecutorRunnerConfig>;
90
+ env?: NodeJS.ProcessEnv;
91
+ /** Run-level budgets (0 disables); attempts are capped to what is left */
92
+ maxTokens?: number;
93
+ maxCostUsd?: number;
94
+ permissions?: PermissionManager;
95
+ createLoop?: (args: CreateLoopArgs) => AgentLoop;
96
+ onEvent?: (event: RunnerEvent) => void;
97
+ /** Called after every tool call (used for run receipts) */
98
+ onToolCall?: (event: ToolCallEvent) => void;
99
+ }
100
+ export declare function buildAgentMessages(input: AgentAttemptInput): ModelMessage[];
101
+ export declare class ExecutorRunner implements AgentRunner {
102
+ private readonly options;
103
+ private readonly config;
104
+ private readonly env;
105
+ private readonly permissions;
106
+ private tierIndex;
107
+ private failuresOnTier;
108
+ private spentTokens;
109
+ private spentUsd;
110
+ constructor(options: ExecutorRunnerOptions);
111
+ /** Model the next attempt would use (for tests and diagnostics). */
112
+ get currentChoice(): ModelChoice;
113
+ /**
114
+ * A call that arrives with feedback means the previous attempt failed
115
+ * verification (or crashed). After `escalateAfter` such failures in a row on
116
+ * the same model, move to the next tier up, if there is one.
117
+ */
118
+ private selectChoice;
119
+ runAttempt(input: AgentAttemptInput): Promise<AgentAttemptResult>;
120
+ private summarize;
121
+ }
122
+ export interface DefaultRunnerArgs {
123
+ goal: string;
124
+ /** Comma separated ladder, cheapest first; a single value pins the model */
125
+ model?: string;
126
+ config?: Partial<ExecutorRunnerConfig>;
127
+ maxTokens?: number;
128
+ maxCostUsd?: number;
129
+ env?: NodeJS.ProcessEnv;
130
+ onEvent?: (event: RunnerEvent) => void;
131
+ onToolCall?: (event: ToolCallEvent) => void;
132
+ }
133
+ /** Build a runner on the configured providers via the smart router. */
134
+ export declare function createDefaultExecutorRunner(args: DefaultRunnerArgs): Promise<ExecutorRunner>;
135
+ //# sourceMappingURL=executor-runner.d.ts.map