@deepagents/context 3.1.0 → 4.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.
- package/README.md +46 -13
- package/dist/browser.js +30 -56
- package/dist/browser.js.map +3 -3
- package/dist/index.js +2359 -2384
- package/dist/index.js.map +4 -4
- package/dist/lib/chain-summary.d.ts.map +1 -1
- package/dist/lib/engine.d.ts.map +1 -1
- package/dist/lib/fragments/message/user.d.ts +42 -85
- package/dist/lib/fragments/message/user.d.ts.map +1 -1
- package/dist/lib/sandbox/agent-os-sandbox.d.ts.map +1 -1
- package/dist/lib/sandbox/bash-tool.d.ts +9 -28
- package/dist/lib/sandbox/bash-tool.d.ts.map +1 -1
- package/dist/lib/sandbox/daytona-sandbox.d.ts.map +1 -1
- package/dist/lib/sandbox/docker-sandbox.d.ts +29 -0
- package/dist/lib/sandbox/docker-sandbox.d.ts.map +1 -1
- package/dist/lib/sandbox/index.d.ts +2 -1
- package/dist/lib/sandbox/index.d.ts.map +1 -1
- package/dist/lib/sandbox/installers/bin.d.ts.map +1 -1
- package/dist/lib/sandbox/installers/index.d.ts +7 -7
- package/dist/lib/sandbox/installers/index.d.ts.map +1 -1
- package/dist/lib/sandbox/installers/installer.d.ts +0 -5
- package/dist/lib/sandbox/installers/installer.d.ts.map +1 -1
- package/dist/lib/sandbox/installers/url-binary.d.ts.map +1 -1
- package/dist/lib/sandbox/shell-quote.d.ts +10 -0
- package/dist/lib/sandbox/shell-quote.d.ts.map +1 -0
- package/dist/lib/sandbox/strace/file-change.d.ts +21 -0
- package/dist/lib/sandbox/strace/file-change.d.ts.map +1 -0
- package/dist/lib/sandbox/strace/file-changes.d.ts +66 -0
- package/dist/lib/sandbox/strace/file-changes.d.ts.map +1 -0
- package/dist/lib/sandbox/strace/index.d.ts +89 -0
- package/dist/lib/sandbox/strace/index.d.ts.map +1 -0
- package/dist/lib/sandbox/strace/index.js +285 -0
- package/dist/lib/sandbox/strace/index.js.map +7 -0
- package/dist/lib/sandbox/types.d.ts +6 -1
- package/dist/lib/sandbox/types.d.ts.map +1 -1
- package/dist/lib/sandbox/virtual-sandbox.d.ts.map +1 -1
- package/dist/lib/save/save-pipeline.d.ts +1 -13
- package/dist/lib/save/save-pipeline.d.ts.map +1 -1
- package/dist/lib/skills/skill-reminder.d.ts +2 -2
- package/dist/lib/skills/skill-reminder.d.ts.map +1 -1
- package/package.json +7 -2
- package/dist/lib/sandbox/file-changes.d.ts +0 -83
- package/dist/lib/sandbox/file-changes.d.ts.map +0 -1
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import type { FileChange } from './file-change.ts';
|
|
2
|
+
export type { FileChange, FileChangeOp } from './file-change.ts';
|
|
3
|
+
export type StraceUnavailableReason = 'ptrace-blocked' | 'strace-missing' | 'trace-unparseable';
|
|
4
|
+
/**
|
|
5
|
+
* Thrown by {@link selfTestStrace} when per-command strace tracking cannot be
|
|
6
|
+
* used. `reason` lets callers branch (fix-image vs fix-caps vs fix-arch). This
|
|
7
|
+
* is a hard failure by design — there is no silent degrade to the snapshot
|
|
8
|
+
* observer.
|
|
9
|
+
*/
|
|
10
|
+
export declare class StraceUnavailableError extends Error {
|
|
11
|
+
readonly reason: StraceUnavailableReason;
|
|
12
|
+
readonly diagnostics: string;
|
|
13
|
+
constructor(reason: StraceUnavailableReason, diagnostics: string);
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Wraps `command` so its filesystem syscalls are traced to `traceFile`. Both
|
|
17
|
+
* the inner command and this wrapper are re-parsed by a shell (`sh -c`), so the
|
|
18
|
+
* single-quoting via {@link shellQuote} composes with the backend's own
|
|
19
|
+
* `sh -c`. strace propagates the traced command's exit code. The `mkdir -p`
|
|
20
|
+
* runs before strace (so it isn't traced) and self-heals the trace dir per
|
|
21
|
+
* command — if it ever disappears mid-session, the next command recreates it
|
|
22
|
+
* rather than strace failing to open `-o` and skipping the command entirely.
|
|
23
|
+
*/
|
|
24
|
+
export declare function buildStraceCommand(command: string, traceFile: string, traceDir: string): string;
|
|
25
|
+
/**
|
|
26
|
+
* Like {@link buildStraceCommand}, but dumps the (base64) trace inline on stdout
|
|
27
|
+
* after `sentinel`, then exits with the traced command's code. This lets one
|
|
28
|
+
* `executeCommand` round-trip carry both the command output and its trace — no
|
|
29
|
+
* second `readFile` exec to pull the trace out of the container. Pair with
|
|
30
|
+
* {@link splitTracedOutput}. `sentinel` must be unguessable (a UUID) so the
|
|
31
|
+
* command's own stdout cannot contain it. `base64` keeps the trace stream-safe
|
|
32
|
+
* and its stderr is dropped so a missing trace (strace failed) doesn't leak into
|
|
33
|
+
* the command's stderr.
|
|
34
|
+
*/
|
|
35
|
+
export declare function buildTracedCommand(command: string, traceFile: string, traceDir: string, sentinel: string): string;
|
|
36
|
+
/**
|
|
37
|
+
* Split {@link buildTracedCommand} output into the command's own stdout and its
|
|
38
|
+
* decoded strace trace. A missing sentinel (strace produced nothing) yields an
|
|
39
|
+
* empty trace so the caller degrades to "no changes".
|
|
40
|
+
*/
|
|
41
|
+
export declare function splitTracedOutput(stdout: string, sentinel: string): {
|
|
42
|
+
stdout: string;
|
|
43
|
+
trace: string;
|
|
44
|
+
};
|
|
45
|
+
export interface ParseStraceOptions {
|
|
46
|
+
include: string[];
|
|
47
|
+
exclude?: string[];
|
|
48
|
+
traceFile?: string;
|
|
49
|
+
traceDir?: string;
|
|
50
|
+
}
|
|
51
|
+
/** A path is tracked when it matches an `include` glob and no `exclude` glob. */
|
|
52
|
+
export declare function matchesGlobs(path: string, include: string[], exclude?: string[]): boolean;
|
|
53
|
+
/**
|
|
54
|
+
* Parse one command's strace output into a coarse `FileChange[]`. Failed
|
|
55
|
+
* syscalls are skipped; paths are filtered to `destination`; the trace file and
|
|
56
|
+
* `/proc`,`/sys`,`/dev` are excluded. Per-path final state collapses repeated
|
|
57
|
+
* writes to one `write`; a path written then deleted within the command is
|
|
58
|
+
* treated as transient and dropped.
|
|
59
|
+
*/
|
|
60
|
+
export declare function parseStraceTrace(raw: string, options: ParseStraceOptions): FileChange[];
|
|
61
|
+
/**
|
|
62
|
+
* The minimal structural surface {@link selfTestStrace} needs from its host.
|
|
63
|
+
* Deliberately narrower than `DisposableSandbox` so the probe can run anywhere:
|
|
64
|
+
* a remote/docker caller passes its sandbox unchanged (it satisfies this shape
|
|
65
|
+
* structurally), and an in-process caller (e.g. a daemon running as PID 1
|
|
66
|
+
* inside the container) implements just these two methods over
|
|
67
|
+
* `node:child_process` + `node:fs`.
|
|
68
|
+
*/
|
|
69
|
+
export interface StraceHost {
|
|
70
|
+
executeCommand(command: string): Promise<{
|
|
71
|
+
exitCode: number;
|
|
72
|
+
stderr: string;
|
|
73
|
+
}>;
|
|
74
|
+
readFile(path: string): Promise<string>;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* One-time probe at sandbox setup. Runs a known create/write/rename sequence
|
|
78
|
+
* under strace and asserts the trace is clean and parseable. Throws
|
|
79
|
+
* {@link StraceUnavailableError} (hard-fail) when ptrace is blocked, strace is
|
|
80
|
+
* absent, or the trace is garbled (e.g. amd64 under Rosetta).
|
|
81
|
+
*
|
|
82
|
+
* "strace works in this sandbox" is an invariant of the (image + host kernel +
|
|
83
|
+
* seccomp/caps) — constant across every tool call and chat turn in a given
|
|
84
|
+
* container — so this is the consumer's once-per-container responsibility (e.g.
|
|
85
|
+
* a daemon boot gate), NOT a per-tool cost. `createBashTool` /
|
|
86
|
+
* `withStraceFileChanges` no longer call it.
|
|
87
|
+
*/
|
|
88
|
+
export declare function selfTestStrace(host: StraceHost): Promise<void>;
|
|
89
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/lib/sandbox/strace/index.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAEnD,YAAY,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AAEjE,MAAM,MAAM,uBAAuB,GAC/B,gBAAgB,GAChB,gBAAgB,GAChB,mBAAmB,CAAC;AAWxB;;;;;GAKG;AACH,qBAAa,sBAAuB,SAAQ,KAAK;IAC/C,QAAQ,CAAC,MAAM,EAAE,uBAAuB,CAAC;IACzC,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;gBAEjB,MAAM,EAAE,uBAAuB,EAAE,WAAW,EAAE,MAAM;CAQjE;AAID;;;;;;;;GAQG;AACH,wBAAgB,kBAAkB,CAChC,OAAO,EAAE,MAAM,EACf,SAAS,EAAE,MAAM,EACjB,QAAQ,EAAE,MAAM,GACf,MAAM,CAKR;AAED;;;;;;;;;GASG;AACH,wBAAgB,kBAAkB,CAChC,OAAO,EAAE,MAAM,EACf,SAAS,EAAE,MAAM,EACjB,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,MAAM,GACf,MAAM,CAUR;AAED;;;;GAIG;AACH,wBAAgB,iBAAiB,CAC/B,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,MAAM,GACf;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CASnC;AAgID,MAAM,WAAW,kBAAkB;IACjC,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,iFAAiF;AACjF,wBAAgB,YAAY,CAC1B,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,MAAM,EAAE,EACjB,OAAO,CAAC,EAAE,MAAM,EAAE,GACjB,OAAO,CAKT;AAED;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAC9B,GAAG,EAAE,MAAM,EACX,OAAO,EAAE,kBAAkB,GAC1B,UAAU,EAAE,CAiGd;AASD;;;;;;;GAOG;AACH,MAAM,WAAW,UAAU;IACzB,cAAc,CACZ,OAAO,EAAE,MAAM,GACd,OAAO,CAAC;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACjD,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;CACzC;AAED;;;;;;;;;;;GAWG;AACH,wBAAsB,cAAc,CAAC,IAAI,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC,CAsDpE"}
|
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
// packages/context/src/lib/sandbox/strace/index.ts
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
3
|
+
import { posix } from "node:path";
|
|
4
|
+
|
|
5
|
+
// packages/context/src/lib/sandbox/shell-quote.ts
|
|
6
|
+
function shellQuote(s) {
|
|
7
|
+
return `'${s.replace(/'/g, `'\\''`)}'`;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
// packages/context/src/lib/sandbox/strace/index.ts
|
|
11
|
+
var REASON_HINT = {
|
|
12
|
+
"ptrace-blocked": "ptrace is denied by the sandbox runtime. Modern Docker permits it by default; on a hardened host, relax the runtime seccomp/ptrace policy (allow the ptrace syscall, or add the SYS_PTRACE capability).",
|
|
13
|
+
"strace-missing": "strace is not installed in the sandbox image. Bake `strace` into the image.",
|
|
14
|
+
"trace-unparseable": "strace ran but its output is unusable \u2014 the sandbox is likely running under emulation (e.g. amd64 via Rosetta). Use a native-architecture sandbox."
|
|
15
|
+
};
|
|
16
|
+
var StraceUnavailableError = class extends Error {
|
|
17
|
+
reason;
|
|
18
|
+
diagnostics;
|
|
19
|
+
constructor(reason, diagnostics) {
|
|
20
|
+
super(
|
|
21
|
+
`strace file-change tracking unavailable (${reason}): ${REASON_HINT[reason]}`
|
|
22
|
+
);
|
|
23
|
+
this.name = "StraceUnavailableError";
|
|
24
|
+
this.reason = reason;
|
|
25
|
+
this.diagnostics = diagnostics;
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
var STRACE_FLAGS = "-f -y -qq -e trace=%file,write,pwrite64,writev";
|
|
29
|
+
function buildStraceCommand(command, traceFile, traceDir) {
|
|
30
|
+
return `mkdir -p ${shellQuote(traceDir)} 2>/dev/null; strace ${STRACE_FLAGS} -o ${shellQuote(traceFile)} -- sh -c ${shellQuote(command)}`;
|
|
31
|
+
}
|
|
32
|
+
function buildTracedCommand(command, traceFile, traceDir, sentinel) {
|
|
33
|
+
return `mkdir -p ${shellQuote(traceDir)} 2>/dev/null; strace ${STRACE_FLAGS} -o ${shellQuote(traceFile)} -- sh -c ${shellQuote(command)}; __dat_rc=$?; printf '\\n%s\\n' ${shellQuote(sentinel)}; base64 ${shellQuote(traceFile)} 2>/dev/null; rm -f ${shellQuote(traceFile)} 2>/dev/null; exit $__dat_rc`;
|
|
34
|
+
}
|
|
35
|
+
function splitTracedOutput(stdout, sentinel) {
|
|
36
|
+
const marker = `
|
|
37
|
+
${sentinel}
|
|
38
|
+
`;
|
|
39
|
+
const at = stdout.indexOf(marker);
|
|
40
|
+
if (at === -1) return { stdout, trace: "" };
|
|
41
|
+
const base64 = stdout.slice(at + marker.length);
|
|
42
|
+
return {
|
|
43
|
+
stdout: stdout.slice(0, at),
|
|
44
|
+
trace: base64 ? Buffer.from(base64, "base64").toString("utf8") : ""
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
var CREATE_OR_TRUNC_FLAGS = /O_(CREAT|TRUNC)/;
|
|
48
|
+
var RENAME_SYSCALLS = /* @__PURE__ */ new Set(["rename", "renameat", "renameat2"]);
|
|
49
|
+
var DELETE_SYSCALLS = /* @__PURE__ */ new Set(["unlink", "unlinkat", "rmdir"]);
|
|
50
|
+
var MKDIR_SYSCALLS = /* @__PURE__ */ new Set(["mkdir", "mkdirat"]);
|
|
51
|
+
var LINK_SYSCALLS = /* @__PURE__ */ new Set(["link", "linkat", "symlink", "symlinkat"]);
|
|
52
|
+
var OPEN_SYSCALLS = /* @__PURE__ */ new Set(["open", "openat", "openat2", "creat"]);
|
|
53
|
+
var WRITE_SYSCALLS = /* @__PURE__ */ new Set([
|
|
54
|
+
"write",
|
|
55
|
+
"pwrite64",
|
|
56
|
+
"pwrite",
|
|
57
|
+
"writev",
|
|
58
|
+
"pwritev2"
|
|
59
|
+
]);
|
|
60
|
+
var TRUNCATE_SYSCALLS = /* @__PURE__ */ new Set(["truncate", "ftruncate"]);
|
|
61
|
+
function stitchLines(raw) {
|
|
62
|
+
const pending = /* @__PURE__ */ new Map();
|
|
63
|
+
const out = [];
|
|
64
|
+
for (const original of raw.split("\n")) {
|
|
65
|
+
const line = original.trimEnd();
|
|
66
|
+
if (!line) continue;
|
|
67
|
+
const pid = line.match(/^\s*(\d+)\s+/)?.[1] ?? "0";
|
|
68
|
+
if (/<unfinished \.\.\.>\s*$/.test(line)) {
|
|
69
|
+
pending.set(pid, line.replace(/<unfinished \.\.\.>\s*$/, ""));
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
const resumed = line.match(/<\.\.\.\s+\S+\s+resumed>(.*)$/);
|
|
73
|
+
if (resumed) {
|
|
74
|
+
const head = pending.get(pid);
|
|
75
|
+
pending.delete(pid);
|
|
76
|
+
if (head !== void 0) out.push(head + resumed[1]);
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
out.push(line);
|
|
80
|
+
}
|
|
81
|
+
return out;
|
|
82
|
+
}
|
|
83
|
+
function parseCall(line) {
|
|
84
|
+
const m = line.match(
|
|
85
|
+
/^\s*(?:\d+\s+)?([a-z_][a-z0-9_]*)\((.*)\)\s*=\s*(.+?)\s*$/
|
|
86
|
+
);
|
|
87
|
+
if (!m) return null;
|
|
88
|
+
const [, syscall, args, rest] = m;
|
|
89
|
+
const errnoMatch = rest.match(/^(-?\d+)\s+([A-Z][A-Z0-9_]*)/);
|
|
90
|
+
if (errnoMatch) {
|
|
91
|
+
return { syscall, args, retval: errnoMatch[1], errno: errnoMatch[2] };
|
|
92
|
+
}
|
|
93
|
+
return { syscall, args, retval: rest.split(/\s+/)[0] };
|
|
94
|
+
}
|
|
95
|
+
var STRACE_ESCAPE = {
|
|
96
|
+
n: 10,
|
|
97
|
+
t: 9,
|
|
98
|
+
r: 13,
|
|
99
|
+
v: 11,
|
|
100
|
+
f: 12,
|
|
101
|
+
'"': 34,
|
|
102
|
+
"\\": 92
|
|
103
|
+
};
|
|
104
|
+
function decodeStraceString(s) {
|
|
105
|
+
const bytes = [];
|
|
106
|
+
for (let i = 0; i < s.length; ) {
|
|
107
|
+
if (s[i] === "\\" && i + 1 < s.length) {
|
|
108
|
+
const octal = s.slice(i + 1, i + 4).match(/^[0-7]{1,3}/)?.[0];
|
|
109
|
+
if (octal) {
|
|
110
|
+
bytes.push(parseInt(octal, 8) & 255);
|
|
111
|
+
i += 1 + octal.length;
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
const mapped = STRACE_ESCAPE[s[i + 1]];
|
|
115
|
+
bytes.push(mapped ?? s.charCodeAt(i + 1));
|
|
116
|
+
i += 2;
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
const code = s.charCodeAt(i);
|
|
120
|
+
if (code < 128) bytes.push(code);
|
|
121
|
+
else for (const b of Buffer.from(s[i], "utf8")) bytes.push(b);
|
|
122
|
+
i += 1;
|
|
123
|
+
}
|
|
124
|
+
return Buffer.from(bytes).toString("utf8");
|
|
125
|
+
}
|
|
126
|
+
function quotedStrings(args) {
|
|
127
|
+
const out = [];
|
|
128
|
+
const re = /"((?:[^"\\]|\\.)*)"/g;
|
|
129
|
+
let m;
|
|
130
|
+
while ((m = re.exec(args)) !== null) {
|
|
131
|
+
out.push(decodeStraceString(m[1]));
|
|
132
|
+
}
|
|
133
|
+
return out;
|
|
134
|
+
}
|
|
135
|
+
function dirfdPath(args) {
|
|
136
|
+
return args.match(/^\s*(?:AT_FDCWD|-?\d+)<([^>]*)>/)?.[1] ?? "/";
|
|
137
|
+
}
|
|
138
|
+
function fdPath(args) {
|
|
139
|
+
return args.match(/^\s*-?\d+<([^>]*)>/)?.[1] ?? null;
|
|
140
|
+
}
|
|
141
|
+
function openFlags(args) {
|
|
142
|
+
return args.match(/\bO_[A-Z_]+(?:\|O_[A-Z_]+)*/)?.[0] ?? "";
|
|
143
|
+
}
|
|
144
|
+
function matchesGlobs(path, include, exclude) {
|
|
145
|
+
return include.some((glob) => posix.matchesGlob(path, glob)) && !exclude?.some((glob) => posix.matchesGlob(path, glob));
|
|
146
|
+
}
|
|
147
|
+
function parseStraceTrace(raw, options) {
|
|
148
|
+
const { include, exclude, traceDir, traceFile } = options;
|
|
149
|
+
const now = Date.now();
|
|
150
|
+
const state = /* @__PURE__ */ new Map();
|
|
151
|
+
const renames = [];
|
|
152
|
+
const resolve = (dir, p) => posix.normalize(p.startsWith("/") ? p : posix.join(dir, p));
|
|
153
|
+
const write = (path) => {
|
|
154
|
+
state.set(path, "write");
|
|
155
|
+
};
|
|
156
|
+
const remove = (path) => {
|
|
157
|
+
if (state.get(path) === "write") state.delete(path);
|
|
158
|
+
else state.set(path, "delete");
|
|
159
|
+
};
|
|
160
|
+
for (const line of stitchLines(raw)) {
|
|
161
|
+
const call = parseCall(line);
|
|
162
|
+
if (!call || call.errno || call.retval === "-1") continue;
|
|
163
|
+
const { syscall, args } = call;
|
|
164
|
+
const dir = dirfdPath(args);
|
|
165
|
+
if (RENAME_SYSCALLS.has(syscall)) {
|
|
166
|
+
const strings = quotedStrings(args);
|
|
167
|
+
if (strings.length < 2) continue;
|
|
168
|
+
const from = resolve(dir, strings[0]);
|
|
169
|
+
const to = resolve(dir, strings[strings.length - 1]);
|
|
170
|
+
state.delete(from);
|
|
171
|
+
renames.push({ from, to });
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
if (DELETE_SYSCALLS.has(syscall)) {
|
|
175
|
+
const strings = quotedStrings(args);
|
|
176
|
+
if (strings.length) remove(resolve(dir, strings[0]));
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
if (MKDIR_SYSCALLS.has(syscall)) {
|
|
180
|
+
const strings = quotedStrings(args);
|
|
181
|
+
if (strings.length) write(resolve(dir, strings[0]));
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
if (LINK_SYSCALLS.has(syscall)) {
|
|
185
|
+
const strings = quotedStrings(args);
|
|
186
|
+
if (strings.length) write(resolve(dir, strings[strings.length - 1]));
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
189
|
+
if (OPEN_SYSCALLS.has(syscall)) {
|
|
190
|
+
if (!CREATE_OR_TRUNC_FLAGS.test(openFlags(args))) continue;
|
|
191
|
+
const strings = quotedStrings(args);
|
|
192
|
+
if (strings.length) write(resolve(dir, strings[0]));
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
if (WRITE_SYSCALLS.has(syscall)) {
|
|
196
|
+
const p = fdPath(args);
|
|
197
|
+
if (p && Number(call.retval) > 0) write(posix.normalize(p));
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
if (TRUNCATE_SYSCALLS.has(syscall)) {
|
|
201
|
+
const p = fdPath(args) ?? quotedStrings(args)[0];
|
|
202
|
+
if (p) write(resolve(dir, p));
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
const keep = (path) => {
|
|
206
|
+
if (traceFile && path === traceFile) return false;
|
|
207
|
+
if (traceDir && (path === traceDir || path.startsWith(`${traceDir}/`))) {
|
|
208
|
+
return false;
|
|
209
|
+
}
|
|
210
|
+
if (/^\/(proc|sys|dev)(\/|$)/.test(path)) return false;
|
|
211
|
+
return matchesGlobs(path, include, exclude);
|
|
212
|
+
};
|
|
213
|
+
const changes = [];
|
|
214
|
+
const emittedRenameTargets = /* @__PURE__ */ new Set();
|
|
215
|
+
for (const { from, to } of renames) {
|
|
216
|
+
if (!keep(to)) continue;
|
|
217
|
+
if (state.get(to) === "delete") continue;
|
|
218
|
+
changes.push({ op: "rename", path: to, from, timestamp: now });
|
|
219
|
+
emittedRenameTargets.add(to);
|
|
220
|
+
}
|
|
221
|
+
const tail = [];
|
|
222
|
+
for (const [path, op] of state) {
|
|
223
|
+
if (emittedRenameTargets.has(path)) continue;
|
|
224
|
+
if (keep(path)) tail.push({ op, path, timestamp: now });
|
|
225
|
+
}
|
|
226
|
+
tail.sort((a, b) => {
|
|
227
|
+
if (a.path < b.path) return -1;
|
|
228
|
+
if (a.path > b.path) return 1;
|
|
229
|
+
return 0;
|
|
230
|
+
});
|
|
231
|
+
changes.push(...tail);
|
|
232
|
+
return changes;
|
|
233
|
+
}
|
|
234
|
+
var PTRACE_DENIED = /strace:[^\n]*(?:PTRACE_TRACEME|ptrace|Operation not permitted|EPERM)/i;
|
|
235
|
+
var STRACE_MISSING = /strace:?\s+(?:command\s+)?not found/i;
|
|
236
|
+
async function selfTestStrace(host) {
|
|
237
|
+
const probeDir = `/tmp/dat-strace-${randomUUID()}`;
|
|
238
|
+
const traceFile = `/tmp/dat-strace-${randomUUID()}.trace`;
|
|
239
|
+
const q = shellQuote;
|
|
240
|
+
const sequence = `mkdir -p ${q(probeDir)} && echo hi > ${q(`${probeDir}/a.txt`)} && mv ${q(`${probeDir}/a.txt`)} ${q(`${probeDir}/b.txt`)} && echo more > ${q(`${probeDir}/c.txt`)}`;
|
|
241
|
+
const wrapped = buildStraceCommand(sequence, traceFile, "/tmp");
|
|
242
|
+
let result;
|
|
243
|
+
let raw = "";
|
|
244
|
+
try {
|
|
245
|
+
result = await host.executeCommand(wrapped);
|
|
246
|
+
raw = await host.readFile(traceFile).catch(() => "");
|
|
247
|
+
const diagnostics = `exit=${result.exitCode}
|
|
248
|
+
stderr=${result.stderr}
|
|
249
|
+
trace[0:600]=${raw.slice(0, 600)}`;
|
|
250
|
+
if (PTRACE_DENIED.test(result.stderr)) {
|
|
251
|
+
throw new StraceUnavailableError("ptrace-blocked", diagnostics);
|
|
252
|
+
}
|
|
253
|
+
if (!raw || result.exitCode === 127 || STRACE_MISSING.test(result.stderr)) {
|
|
254
|
+
throw new StraceUnavailableError("strace-missing", diagnostics);
|
|
255
|
+
}
|
|
256
|
+
const changes = parseStraceTrace(raw, {
|
|
257
|
+
include: [probeDir, `${probeDir}/**`],
|
|
258
|
+
traceFile
|
|
259
|
+
});
|
|
260
|
+
const hasRealFdPath = raw.includes(`<${probeDir}/`);
|
|
261
|
+
const hasRename = changes.some(
|
|
262
|
+
(c) => c.op === "rename" && c.from === `${probeDir}/a.txt` && c.path === `${probeDir}/b.txt`
|
|
263
|
+
);
|
|
264
|
+
const hasWrite = changes.some((c) => c.op === "write");
|
|
265
|
+
const allUnderDir = changes.every(
|
|
266
|
+
(c) => c.path === probeDir || c.path.startsWith(`${probeDir}/`)
|
|
267
|
+
);
|
|
268
|
+
if (!hasRealFdPath || !hasRename || !hasWrite || !allUnderDir) {
|
|
269
|
+
throw new StraceUnavailableError("trace-unparseable", diagnostics);
|
|
270
|
+
}
|
|
271
|
+
} finally {
|
|
272
|
+
void host.executeCommand(`rm -rf ${q(probeDir)} ${q(traceFile)}`).catch(() => {
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
export {
|
|
277
|
+
StraceUnavailableError,
|
|
278
|
+
buildStraceCommand,
|
|
279
|
+
buildTracedCommand,
|
|
280
|
+
matchesGlobs,
|
|
281
|
+
parseStraceTrace,
|
|
282
|
+
selfTestStrace,
|
|
283
|
+
splitTracedOutput
|
|
284
|
+
};
|
|
285
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../src/lib/sandbox/strace/index.ts", "../../../../src/lib/sandbox/shell-quote.ts"],
|
|
4
|
+
"sourcesContent": ["import { randomUUID } from 'node:crypto';\nimport { posix } from 'node:path';\n\nimport { shellQuote } from '../shell-quote.ts';\nimport type { FileChange } from './file-change.ts';\n\nexport type { FileChange, FileChangeOp } from './file-change.ts';\n\nexport type StraceUnavailableReason =\n | 'ptrace-blocked'\n | 'strace-missing'\n | 'trace-unparseable';\n\nconst REASON_HINT: Record<StraceUnavailableReason, string> = {\n 'ptrace-blocked':\n 'ptrace is denied by the sandbox runtime. Modern Docker permits it by default; on a hardened host, relax the runtime seccomp/ptrace policy (allow the ptrace syscall, or add the SYS_PTRACE capability).',\n 'strace-missing':\n 'strace is not installed in the sandbox image. Bake `strace` into the image.',\n 'trace-unparseable':\n 'strace ran but its output is unusable \u2014 the sandbox is likely running under emulation (e.g. amd64 via Rosetta). Use a native-architecture sandbox.',\n};\n\n/**\n * Thrown by {@link selfTestStrace} when per-command strace tracking cannot be\n * used. `reason` lets callers branch (fix-image vs fix-caps vs fix-arch). This\n * is a hard failure by design \u2014 there is no silent degrade to the snapshot\n * observer.\n */\nexport class StraceUnavailableError extends Error {\n readonly reason: StraceUnavailableReason;\n readonly diagnostics: string;\n\n constructor(reason: StraceUnavailableReason, diagnostics: string) {\n super(\n `strace file-change tracking unavailable (${reason}): ${REASON_HINT[reason]}`,\n );\n this.name = 'StraceUnavailableError';\n this.reason = reason;\n this.diagnostics = diagnostics;\n }\n}\n\nconst STRACE_FLAGS = '-f -y -qq -e trace=%file,write,pwrite64,writev';\n\n/**\n * Wraps `command` so its filesystem syscalls are traced to `traceFile`. Both\n * the inner command and this wrapper are re-parsed by a shell (`sh -c`), so the\n * single-quoting via {@link shellQuote} composes with the backend's own\n * `sh -c`. strace propagates the traced command's exit code. The `mkdir -p`\n * runs before strace (so it isn't traced) and self-heals the trace dir per\n * command \u2014 if it ever disappears mid-session, the next command recreates it\n * rather than strace failing to open `-o` and skipping the command entirely.\n */\nexport function buildStraceCommand(\n command: string,\n traceFile: string,\n traceDir: string,\n): string {\n return (\n `mkdir -p ${shellQuote(traceDir)} 2>/dev/null; ` +\n `strace ${STRACE_FLAGS} -o ${shellQuote(traceFile)} -- sh -c ${shellQuote(command)}`\n );\n}\n\n/**\n * Like {@link buildStraceCommand}, but dumps the (base64) trace inline on stdout\n * after `sentinel`, then exits with the traced command's code. This lets one\n * `executeCommand` round-trip carry both the command output and its trace \u2014 no\n * second `readFile` exec to pull the trace out of the container. Pair with\n * {@link splitTracedOutput}. `sentinel` must be unguessable (a UUID) so the\n * command's own stdout cannot contain it. `base64` keeps the trace stream-safe\n * and its stderr is dropped so a missing trace (strace failed) doesn't leak into\n * the command's stderr.\n */\nexport function buildTracedCommand(\n command: string,\n traceFile: string,\n traceDir: string,\n sentinel: string,\n): string {\n return (\n `mkdir -p ${shellQuote(traceDir)} 2>/dev/null; ` +\n `strace ${STRACE_FLAGS} -o ${shellQuote(traceFile)} -- sh -c ${shellQuote(command)}; ` +\n `__dat_rc=$?; ` +\n `printf '\\\\n%s\\\\n' ${shellQuote(sentinel)}; ` +\n `base64 ${shellQuote(traceFile)} 2>/dev/null; ` +\n `rm -f ${shellQuote(traceFile)} 2>/dev/null; ` +\n `exit $__dat_rc`\n );\n}\n\n/**\n * Split {@link buildTracedCommand} output into the command's own stdout and its\n * decoded strace trace. A missing sentinel (strace produced nothing) yields an\n * empty trace so the caller degrades to \"no changes\".\n */\nexport function splitTracedOutput(\n stdout: string,\n sentinel: string,\n): { stdout: string; trace: string } {\n const marker = `\\n${sentinel}\\n`;\n const at = stdout.indexOf(marker);\n if (at === -1) return { stdout, trace: '' };\n const base64 = stdout.slice(at + marker.length);\n return {\n stdout: stdout.slice(0, at),\n trace: base64 ? Buffer.from(base64, 'base64').toString('utf8') : '',\n };\n}\n\n// An open is a definite write only when it creates or truncates. Bare\n// O_WRONLY/O_RDWR/O_APPEND opens are NOT assumed to mutate \u2014 a real write()\n// syscall confirms them \u2014 so read-only-via-O_RDWR (sqlite SELECT, an editor\n// that opens then quits) doesn't produce a phantom `write`.\nconst CREATE_OR_TRUNC_FLAGS = /O_(CREAT|TRUNC)/;\nconst RENAME_SYSCALLS = new Set(['rename', 'renameat', 'renameat2']);\nconst DELETE_SYSCALLS = new Set(['unlink', 'unlinkat', 'rmdir']);\nconst MKDIR_SYSCALLS = new Set(['mkdir', 'mkdirat']);\nconst LINK_SYSCALLS = new Set(['link', 'linkat', 'symlink', 'symlinkat']);\nconst OPEN_SYSCALLS = new Set(['open', 'openat', 'openat2', 'creat']);\nconst WRITE_SYSCALLS = new Set([\n 'write',\n 'pwrite64',\n 'pwrite',\n 'writev',\n 'pwritev2',\n]);\nconst TRUNCATE_SYSCALLS = new Set(['truncate', 'ftruncate']);\n\ninterface ParsedCall {\n syscall: string;\n args: string;\n retval: string;\n errno?: string;\n}\n\n/** Reassemble `<unfinished \u2026>` / `<\u2026 resumed>` syscall splits per PID. */\nfunction stitchLines(raw: string): string[] {\n const pending = new Map<string, string>();\n const out: string[] = [];\n for (const original of raw.split('\\n')) {\n const line = original.trimEnd();\n if (!line) continue;\n const pid = line.match(/^\\s*(\\d+)\\s+/)?.[1] ?? '0';\n if (/<unfinished \\.\\.\\.>\\s*$/.test(line)) {\n pending.set(pid, line.replace(/<unfinished \\.\\.\\.>\\s*$/, ''));\n continue;\n }\n const resumed = line.match(/<\\.\\.\\.\\s+\\S+\\s+resumed>(.*)$/);\n if (resumed) {\n const head = pending.get(pid);\n pending.delete(pid);\n if (head !== undefined) out.push(head + resumed[1]);\n continue;\n }\n out.push(line);\n }\n return out;\n}\n\nfunction parseCall(line: string): ParsedCall | null {\n const m = line.match(\n /^\\s*(?:\\d+\\s+)?([a-z_][a-z0-9_]*)\\((.*)\\)\\s*=\\s*(.+?)\\s*$/,\n );\n if (!m) return null;\n const [, syscall, args, rest] = m;\n const errnoMatch = rest.match(/^(-?\\d+)\\s+([A-Z][A-Z0-9_]*)/);\n if (errnoMatch) {\n return { syscall, args, retval: errnoMatch[1], errno: errnoMatch[2] };\n }\n return { syscall, args, retval: rest.split(/\\s+/)[0] };\n}\n\nconst STRACE_ESCAPE: Record<string, number> = {\n n: 0x0a,\n t: 0x09,\n r: 0x0d,\n v: 0x0b,\n f: 0x0c,\n '\"': 0x22,\n '\\\\': 0x5c,\n};\n\n/**\n * Decode one strace-rendered string. strace emits printable ASCII verbatim and\n * non-printable / non-ASCII bytes as octal `\\NNN`, plus a few `\\x` escapes. We\n * reconstruct the raw byte sequence and decode it as UTF-8 so multi-byte\n * filenames (e.g. `caf\u00E9.txt` \u2192 `\\303\\251`) round-trip instead of being mangled.\n */\nfunction decodeStraceString(s: string): string {\n const bytes: number[] = [];\n for (let i = 0; i < s.length; ) {\n if (s[i] === '\\\\' && i + 1 < s.length) {\n const octal = s.slice(i + 1, i + 4).match(/^[0-7]{1,3}/)?.[0];\n if (octal) {\n bytes.push(parseInt(octal, 8) & 0xff);\n i += 1 + octal.length;\n continue;\n }\n const mapped = STRACE_ESCAPE[s[i + 1]];\n bytes.push(mapped ?? s.charCodeAt(i + 1));\n i += 2;\n continue;\n }\n const code = s.charCodeAt(i);\n if (code < 0x80) bytes.push(code);\n else for (const b of Buffer.from(s[i], 'utf8')) bytes.push(b);\n i += 1;\n }\n return Buffer.from(bytes).toString('utf8');\n}\n\nfunction quotedStrings(args: string): string[] {\n const out: string[] = [];\n const re = /\"((?:[^\"\\\\]|\\\\.)*)\"/g;\n let m: RegExpExecArray | null;\n while ((m = re.exec(args)) !== null) {\n out.push(decodeStraceString(m[1]));\n }\n return out;\n}\n\n/** Directory path annotated on the leading dirfd (`AT_FDCWD</cwd>` / `7</dir>`). */\nfunction dirfdPath(args: string): string {\n return args.match(/^\\s*(?:AT_FDCWD|-?\\d+)<([^>]*)>/)?.[1] ?? '/';\n}\n\n/** Path annotated on the leading fd argument (`-y` output), for write syscalls. */\nfunction fdPath(args: string): string | null {\n return args.match(/^\\s*-?\\d+<([^>]*)>/)?.[1] ?? null;\n}\n\nfunction openFlags(args: string): string {\n return args.match(/\\bO_[A-Z_]+(?:\\|O_[A-Z_]+)*/)?.[0] ?? '';\n}\n\nexport interface ParseStraceOptions {\n include: string[];\n exclude?: string[];\n traceFile?: string;\n traceDir?: string;\n}\n\n/** A path is tracked when it matches an `include` glob and no `exclude` glob. */\nexport function matchesGlobs(\n path: string,\n include: string[],\n exclude?: string[],\n): boolean {\n return (\n include.some((glob) => posix.matchesGlob(path, glob)) &&\n !exclude?.some((glob) => posix.matchesGlob(path, glob))\n );\n}\n\n/**\n * Parse one command's strace output into a coarse `FileChange[]`. Failed\n * syscalls are skipped; paths are filtered to `destination`; the trace file and\n * `/proc`,`/sys`,`/dev` are excluded. Per-path final state collapses repeated\n * writes to one `write`; a path written then deleted within the command is\n * treated as transient and dropped.\n */\nexport function parseStraceTrace(\n raw: string,\n options: ParseStraceOptions,\n): FileChange[] {\n const { include, exclude, traceDir, traceFile } = options;\n const now = Date.now();\n\n const state = new Map<string, 'write' | 'delete'>();\n const renames: Array<{ from: string; to: string }> = [];\n\n const resolve = (dir: string, p: string): string =>\n posix.normalize(p.startsWith('/') ? p : posix.join(dir, p));\n\n const write = (path: string) => {\n state.set(path, 'write');\n };\n const remove = (path: string) => {\n if (state.get(path) === 'write') state.delete(path);\n else state.set(path, 'delete');\n };\n\n for (const line of stitchLines(raw)) {\n const call = parseCall(line);\n if (!call || call.errno || call.retval === '-1') continue;\n const { syscall, args } = call;\n const dir = dirfdPath(args);\n\n if (RENAME_SYSCALLS.has(syscall)) {\n const strings = quotedStrings(args);\n if (strings.length < 2) continue;\n const from = resolve(dir, strings[0]);\n const to = resolve(dir, strings[strings.length - 1]);\n state.delete(from);\n renames.push({ from, to });\n continue;\n }\n if (DELETE_SYSCALLS.has(syscall)) {\n const strings = quotedStrings(args);\n if (strings.length) remove(resolve(dir, strings[0]));\n continue;\n }\n if (MKDIR_SYSCALLS.has(syscall)) {\n const strings = quotedStrings(args);\n if (strings.length) write(resolve(dir, strings[0]));\n continue;\n }\n if (LINK_SYSCALLS.has(syscall)) {\n const strings = quotedStrings(args);\n if (strings.length) write(resolve(dir, strings[strings.length - 1]));\n continue;\n }\n if (OPEN_SYSCALLS.has(syscall)) {\n if (!CREATE_OR_TRUNC_FLAGS.test(openFlags(args))) continue;\n const strings = quotedStrings(args);\n if (strings.length) write(resolve(dir, strings[0]));\n continue;\n }\n if (WRITE_SYSCALLS.has(syscall)) {\n const p = fdPath(args);\n if (p && Number(call.retval) > 0) write(posix.normalize(p));\n continue;\n }\n if (TRUNCATE_SYSCALLS.has(syscall)) {\n const p = fdPath(args) ?? quotedStrings(args)[0];\n if (p) write(resolve(dir, p));\n }\n }\n\n const keep = (path: string): boolean => {\n if (traceFile && path === traceFile) return false;\n if (traceDir && (path === traceDir || path.startsWith(`${traceDir}/`))) {\n return false;\n }\n if (/^\\/(proc|sys|dev)(\\/|$)/.test(path)) return false;\n return matchesGlobs(path, include, exclude);\n };\n\n const changes: FileChange[] = [];\n const emittedRenameTargets = new Set<string>();\n for (const { from, to } of renames) {\n if (!keep(to)) continue;\n // A later delete of the target supersedes the rename (the tail emits the\n // delete); otherwise the rename entry already represents the target's final\n // state, so the tail must NOT also emit a `write` for it (would duplicate).\n if (state.get(to) === 'delete') continue;\n changes.push({ op: 'rename', path: to, from, timestamp: now });\n emittedRenameTargets.add(to);\n }\n const tail: FileChange[] = [];\n for (const [path, op] of state) {\n if (emittedRenameTargets.has(path)) continue;\n if (keep(path)) tail.push({ op, path, timestamp: now });\n }\n tail.sort((a, b) => {\n if (a.path < b.path) return -1;\n if (a.path > b.path) return 1;\n return 0;\n });\n changes.push(...tail);\n return changes;\n}\n\n// strace prints its own failures to stderr prefixed `strace: `. Anchor on that\n// so a benign command whose output merely contains \"Operation not permitted\" or\n// \"not found\" is not misclassified as a tracer failure.\nconst PTRACE_DENIED =\n /strace:[^\\n]*(?:PTRACE_TRACEME|ptrace|Operation not permitted|EPERM)/i;\nconst STRACE_MISSING = /strace:?\\s+(?:command\\s+)?not found/i;\n\n/**\n * The minimal structural surface {@link selfTestStrace} needs from its host.\n * Deliberately narrower than `DisposableSandbox` so the probe can run anywhere:\n * a remote/docker caller passes its sandbox unchanged (it satisfies this shape\n * structurally), and an in-process caller (e.g. a daemon running as PID 1\n * inside the container) implements just these two methods over\n * `node:child_process` + `node:fs`.\n */\nexport interface StraceHost {\n executeCommand(\n command: string,\n ): Promise<{ exitCode: number; stderr: string }>;\n readFile(path: string): Promise<string>;\n}\n\n/**\n * One-time probe at sandbox setup. Runs a known create/write/rename sequence\n * under strace and asserts the trace is clean and parseable. Throws\n * {@link StraceUnavailableError} (hard-fail) when ptrace is blocked, strace is\n * absent, or the trace is garbled (e.g. amd64 under Rosetta).\n *\n * \"strace works in this sandbox\" is an invariant of the (image + host kernel +\n * seccomp/caps) \u2014 constant across every tool call and chat turn in a given\n * container \u2014 so this is the consumer's once-per-container responsibility (e.g.\n * a daemon boot gate), NOT a per-tool cost. `createBashTool` /\n * `withStraceFileChanges` no longer call it.\n */\nexport async function selfTestStrace(host: StraceHost): Promise<void> {\n const probeDir = `/tmp/dat-strace-${randomUUID()}`;\n const traceFile = `/tmp/dat-strace-${randomUUID()}.trace`;\n const q = shellQuote;\n const sequence =\n `mkdir -p ${q(probeDir)} && ` +\n `echo hi > ${q(`${probeDir}/a.txt`)} && ` +\n `mv ${q(`${probeDir}/a.txt`)} ${q(`${probeDir}/b.txt`)} && ` +\n `echo more > ${q(`${probeDir}/c.txt`)}`;\n const wrapped = buildStraceCommand(sequence, traceFile, '/tmp');\n\n let result: { exitCode: number; stderr: string } | undefined;\n let raw = '';\n try {\n result = await host.executeCommand(wrapped);\n raw = await host.readFile(traceFile).catch(() => '');\n\n const diagnostics = `exit=${result.exitCode}\\nstderr=${result.stderr}\\ntrace[0:600]=${raw.slice(0, 600)}`;\n\n // Order matters: a ptrace block makes strace error on stderr AND emit no\n // trace, so check the ptrace signal (stderr only) before the empty-trace\n // strace-missing check, or a blocked ptrace would be mislabelled.\n if (PTRACE_DENIED.test(result.stderr)) {\n throw new StraceUnavailableError('ptrace-blocked', diagnostics);\n }\n if (!raw || result.exitCode === 127 || STRACE_MISSING.test(result.stderr)) {\n throw new StraceUnavailableError('strace-missing', diagnostics);\n }\n\n // Scope to the probe dir; the exact-path `traceFile` exclusion drops the\n // sibling trace file that lives under /tmp alongside it.\n const changes = parseStraceTrace(raw, {\n include: [probeDir, `${probeDir}/**`],\n traceFile,\n });\n const hasRealFdPath = raw.includes(`<${probeDir}/`);\n const hasRename = changes.some(\n (c) =>\n c.op === 'rename' &&\n c.from === `${probeDir}/a.txt` &&\n c.path === `${probeDir}/b.txt`,\n );\n const hasWrite = changes.some((c) => c.op === 'write');\n const allUnderDir = changes.every(\n (c) => c.path === probeDir || c.path.startsWith(`${probeDir}/`),\n );\n if (!hasRealFdPath || !hasRename || !hasWrite || !allUnderDir) {\n throw new StraceUnavailableError('trace-unparseable', diagnostics);\n }\n } finally {\n void host\n .executeCommand(`rm -rf ${q(probeDir)} ${q(traceFile)}`)\n .catch(() => {});\n }\n}\n", "/**\n * POSIX shell single-quote escape: wrap in `'...'`, replace any embedded\n * `'` with `'\\''`. Safe for arbitrary content inside `sh -c`.\n *\n * Lives in its own zero-import module so node-builtins-only consumers (e.g. the\n * lean `./sandbox/strace` leaf entry) can use it without dragging in the\n * installer/runtime surface.\n */\nexport function shellQuote(s: string): string {\n return `'${s.replace(/'/g, `'\\\\''`)}'`;\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -39,9 +39,14 @@ export interface SandboxProcess {
|
|
|
39
39
|
* `if (!sandbox.spawn) ...` — no silent fallback that aggregates output
|
|
40
40
|
* and flushes on completion.
|
|
41
41
|
*/
|
|
42
|
-
export interface DisposableSandbox extends Omit<UpstreamSandbox, 'executeCommand'
|
|
42
|
+
export interface DisposableSandbox extends Omit<UpstreamSandbox, 'executeCommand'>, AsyncDisposable {
|
|
43
43
|
executeCommand(command: string, options?: ExecuteCommandOptions): Promise<CommandResult>;
|
|
44
44
|
spawn?(command: string, options?: SpawnOptions): SandboxProcess;
|
|
45
|
+
/**
|
|
46
|
+
* Release the backend's external resources. Called explicitly, or
|
|
47
|
+
* automatically at scope exit via `await using` — every backend also
|
|
48
|
+
* implements `[Symbol.asyncDispose]`, which delegates here.
|
|
49
|
+
*/
|
|
45
50
|
dispose(): Promise<void>;
|
|
46
51
|
}
|
|
47
52
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/lib/sandbox/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,IAAI,CAAC;AAC/B,OAAO,KAAK,EACV,WAAW,EACX,aAAa,EACb,OAAO,IAAI,eAAe,EAC3B,MAAM,WAAW,CAAC;AAEnB,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AAE3D;;;;GAIG;AACH,MAAM,WAAW,qBAAqB;IACpC,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB;AAED,MAAM,WAAW,YAAY;IAC3B,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC7B,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,QAAQ;IACvB,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,MAAM,EAAE,MAAM,CAAC,OAAO,GAAG,IAAI,CAAC;IAC9B,oCAAoC;IACpC,OAAO,EAAE,OAAO,CAAC;CAClB;AAED,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,MAAM,EAAE,cAAc,CAAC,UAAU,CAAC,CAAC;IAC5C,QAAQ,CAAC,MAAM,EAAE,cAAc,CAAC,UAAU,CAAC,CAAC;IAC5C,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC,QAAQ,CAAC,CAAC;CAClC;AAED;;;;;;;;;;;;;GAaG;AACH,MAAM,WAAW,
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/lib/sandbox/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,IAAI,CAAC;AAC/B,OAAO,KAAK,EACV,WAAW,EACX,aAAa,EACb,OAAO,IAAI,eAAe,EAC3B,MAAM,WAAW,CAAC;AAEnB,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AAE3D;;;;GAIG;AACH,MAAM,WAAW,qBAAqB;IACpC,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB;AAED,MAAM,WAAW,YAAY;IAC3B,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC7B,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,QAAQ;IACvB,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,MAAM,EAAE,MAAM,CAAC,OAAO,GAAG,IAAI,CAAC;IAC9B,oCAAoC;IACpC,OAAO,EAAE,OAAO,CAAC;CAClB;AAED,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,MAAM,EAAE,cAAc,CAAC,UAAU,CAAC,CAAC;IAC5C,QAAQ,CAAC,MAAM,EAAE,cAAc,CAAC,UAAU,CAAC,CAAC;IAC5C,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC,QAAQ,CAAC,CAAC;CAClC;AAED;;;;;;;;;;;;;GAaG;AACH,MAAM,WAAW,iBACf,SAAQ,IAAI,CAAC,eAAe,EAAE,gBAAgB,CAAC,EAAE,eAAe;IAChE,cAAc,CACZ,OAAO,EAAE,MAAM,EACf,OAAO,CAAC,EAAE,qBAAqB,GAC9B,OAAO,CAAC,aAAa,CAAC,CAAC;IAC1B,KAAK,CAAC,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,YAAY,GAAG,cAAc,CAAC;IAChE;;;;OAIG;IACH,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CAC1B;AAED;;;;GAIG;AACH,MAAM,WAAW,gBAAgB;IAC/B,6EAA6E;IAC7E,IAAI,EAAE,MAAM,CAAC;IACb,iEAAiE;IACjE,OAAO,EAAE,MAAM,CAAC;CACjB;AAED;;;GAGG;AACH,MAAM,WAAW,aAAa;IAC5B,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,mEAAmE;AACnE,MAAM,MAAM,eAAe,GAAG,IAAI,CAAC,aAAa,EAAE,aAAa,CAAC,CAAC;AAEjE;;;;;GAKG;AACH,MAAM,WAAW,YAAa,SAAQ,IAAI,CACxC,WAAW,EACX,MAAM,GAAG,OAAO,GAAG,SAAS,CAC7B;IACC,+DAA+D;IAC/D,MAAM,EAAE,gBAAgB,EAAE,CAAC;IAC3B,IAAI,EAAE,eAAe,CAAC;IACtB,KAAK,EAAE,IAAI,CAAC,WAAW,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC,GAAG;QAAE,IAAI,EAAE,eAAe,CAAA;KAAE,CAAC;IACtE,OAAO,EAAE,iBAAiB,CAAC;CAC5B"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"virtual-sandbox.d.ts","sourceRoot":"","sources":["../../../src/lib/sandbox/virtual-sandbox.ts"],"names":[],"mappings":"AAAA,OAAO,EAAQ,KAAK,aAAa,EAAE,KAAK,WAAW,EAAE,MAAM,WAAW,CAAC;AAEvE,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AAEpD,MAAM,WAAW,2BAA2B;IAC1C,EAAE,EAAE,WAAW,CAAC;IAChB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC7B,cAAc,CAAC,EAAE,aAAa,EAAE,CAAC;CAClC;AAED,wBAAsB,oBAAoB,CACxC,OAAO,EAAE,2BAA2B,GACnC,OAAO,CAAC,iBAAiB,CAAC,
|
|
1
|
+
{"version":3,"file":"virtual-sandbox.d.ts","sourceRoot":"","sources":["../../../src/lib/sandbox/virtual-sandbox.ts"],"names":[],"mappings":"AAAA,OAAO,EAAQ,KAAK,aAAa,EAAE,KAAK,WAAW,EAAE,MAAM,WAAW,CAAC;AAEvE,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AAEpD,MAAM,WAAW,2BAA2B;IAC1C,EAAE,EAAE,WAAW,CAAC;IAChB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC7B,cAAc,CAAC,EAAE,aAAa,EAAE,CAAC;CAClC;AAED,wBAAsB,oBAAoB,CACxC,OAAO,EAAE,2BAA2B,GACnC,OAAO,CAAC,iBAAiB,CAAC,CA0C5B"}
|
|
@@ -1,34 +1,22 @@
|
|
|
1
|
-
import type { ChainSummary } from '../chain-summary.ts';
|
|
2
1
|
import type { ContextFragment } from '../fragments.ts';
|
|
3
|
-
import { type BaseWhenCtx } from '../fragments/message/user.ts';
|
|
4
2
|
import type { ContextStore } from '../store/store.ts';
|
|
5
3
|
export interface SavePipelineEngine {
|
|
6
4
|
readonly store: ContextStore;
|
|
7
5
|
readonly chatId: string;
|
|
8
|
-
readonly branchName: string;
|
|
9
6
|
getActiveBranch(): {
|
|
10
7
|
id: string;
|
|
11
8
|
headMessageId: string | null;
|
|
12
9
|
};
|
|
13
10
|
commitHead(headMessageId: string): Promise<void>;
|
|
14
11
|
rewindForUpdate(parentId: string): Promise<void>;
|
|
15
|
-
getChainSummary(): Promise<ChainSummary>;
|
|
16
|
-
buildBaseWhenCtx(chain: ChainSummary): BaseWhenCtx;
|
|
17
12
|
}
|
|
18
13
|
export interface SaveResult {
|
|
19
14
|
headMessageId: string | undefined;
|
|
20
15
|
}
|
|
21
16
|
export declare class SavePipeline {
|
|
22
17
|
#private;
|
|
23
|
-
constructor(engine: SavePipelineEngine, pending: ContextFragment[]
|
|
18
|
+
constructor(engine: SavePipelineEngine, pending: ContextFragment[]);
|
|
24
19
|
applyUpdateBranching(shouldBranch: boolean): Promise<this>;
|
|
25
|
-
/**
|
|
26
|
-
* Fold any fired `target: 'user'` conditional reminders into the last pending
|
|
27
|
-
* user message. `steer` reminders fire mid-loop via the engine's prepareStep
|
|
28
|
-
* hook and `tool-output` reminders wrap at tool-execution time, so neither is
|
|
29
|
-
* handled here — the save pipeline only carries user-message reminders.
|
|
30
|
-
*/
|
|
31
|
-
evaluateUserReminders(): Promise<this>;
|
|
32
20
|
persist(): Promise<SaveResult>;
|
|
33
21
|
}
|
|
34
22
|
//# sourceMappingURL=save-pipeline.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"save-pipeline.d.ts","sourceRoot":"","sources":["../../../src/lib/save/save-pipeline.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,
|
|
1
|
+
{"version":3,"file":"save-pipeline.d.ts","sourceRoot":"","sources":["../../../src/lib/save/save-pipeline.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AACvD,OAAO,KAAK,EAAE,YAAY,EAAe,MAAM,mBAAmB,CAAC;AAEnE,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,KAAK,EAAE,YAAY,CAAC;IAC7B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,eAAe,IAAI;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,aAAa,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE,CAAC;IAChE,UAAU,CAAC,aAAa,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACjD,eAAe,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAClD;AAED,MAAM,WAAW,UAAU;IACzB,aAAa,EAAE,MAAM,GAAG,SAAS,CAAC;CACnC;AAED,qBAAa,YAAY;;gBAKX,MAAM,EAAE,kBAAkB,EAAE,OAAO,EAAE,eAAe,EAAE;IAK5D,oBAAoB,CAAC,YAAY,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC;IAgB1D,OAAO,IAAI,OAAO,CAAC,UAAU,CAAC;CA6CrC"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { ContextFragment } from '../fragments.ts';
|
|
2
2
|
import { type ClassifierOptions, type IClassifier } from '../fragments/reminders/classifier.ts';
|
|
3
3
|
import type { SkillMetadata } from './types.ts';
|
|
4
|
-
export declare function skillsReminder(skillsOrClassifier: SkillMetadata[] | IClassifier<SkillMetadata>, options?: ClassifierOptions):
|
|
4
|
+
export declare function skillsReminder(skillsOrClassifier: SkillMetadata[] | IClassifier<SkillMetadata>, options?: ClassifierOptions): ContextFragment;
|
|
5
5
|
//# sourceMappingURL=skill-reminder.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"skill-reminder.d.ts","sourceRoot":"","sources":["../../../src/lib/skills/skill-reminder.ts"],"names":[],"mappings":"AAAA,OAAO,
|
|
1
|
+
{"version":3,"file":"skill-reminder.d.ts","sourceRoot":"","sources":["../../../src/lib/skills/skill-reminder.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AAEvD,OAAO,EAGL,KAAK,iBAAiB,EACtB,KAAK,WAAW,EACjB,MAAM,sCAAsC,CAAC;AAC9C,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAYhD,wBAAgB,cAAc,CAC5B,kBAAkB,EAAE,aAAa,EAAE,GAAG,WAAW,CAAC,aAAa,CAAC,EAChE,OAAO,CAAC,EAAE,iBAAiB,GAC1B,eAAe,CAajB"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@deepagents/context",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "4.0.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -31,6 +31,11 @@
|
|
|
31
31
|
"types": "./dist/lib/tracing/index.d.ts",
|
|
32
32
|
"import": "./dist/lib/tracing/index.js",
|
|
33
33
|
"default": "./dist/lib/tracing/index.js"
|
|
34
|
+
},
|
|
35
|
+
"./sandbox/strace": {
|
|
36
|
+
"types": "./dist/lib/sandbox/strace/index.d.ts",
|
|
37
|
+
"import": "./dist/lib/sandbox/strace/index.js",
|
|
38
|
+
"default": "./dist/lib/sandbox/strace/index.js"
|
|
34
39
|
}
|
|
35
40
|
},
|
|
36
41
|
"files": [
|
|
@@ -44,7 +49,7 @@
|
|
|
44
49
|
"dependencies": {
|
|
45
50
|
"@ai-sdk/groq": "^3.0.36",
|
|
46
51
|
"@ai-sdk/provider": "^3.0.10",
|
|
47
|
-
"@deepagents/agent": "
|
|
52
|
+
"@deepagents/agent": "4.0.0",
|
|
48
53
|
"ai": "^6.0.174",
|
|
49
54
|
"bash-tool": "^1.3.16",
|
|
50
55
|
"chalk": "^5.6.0",
|
|
@@ -1,83 +0,0 @@
|
|
|
1
|
-
import type { DisposableSandbox } from './types.ts';
|
|
2
|
-
/**
|
|
3
|
-
* A single filesystem mutation observed for one tool call. Ops are
|
|
4
|
-
* deliberately coarse: strace cannot distinguish a brand-new file from an
|
|
5
|
-
* overwrite within a single command (both are `O_CREAT|O_TRUNC`), so
|
|
6
|
-
* content-touching syscalls collapse to `write`. `delete` and `rename` are
|
|
7
|
-
* unambiguous. Reads are not tracked — strace's syscall filter excludes them.
|
|
8
|
-
*/
|
|
9
|
-
export type FileChangeOp = 'write' | 'delete' | 'rename';
|
|
10
|
-
export interface FileChange {
|
|
11
|
-
op: FileChangeOp;
|
|
12
|
-
path: string;
|
|
13
|
-
/** Source path for a `rename`. */
|
|
14
|
-
from?: string;
|
|
15
|
-
timestamp: number;
|
|
16
|
-
}
|
|
17
|
-
export type StraceUnavailableReason = 'ptrace-blocked' | 'strace-missing' | 'trace-unparseable';
|
|
18
|
-
/**
|
|
19
|
-
* Thrown by {@link selfTestStrace} when per-command strace tracking cannot be
|
|
20
|
-
* used. `reason` lets callers branch (fix-image vs fix-caps vs fix-arch). This
|
|
21
|
-
* is a hard failure by design — there is no silent degrade to the snapshot
|
|
22
|
-
* observer.
|
|
23
|
-
*/
|
|
24
|
-
export declare class StraceUnavailableError extends Error {
|
|
25
|
-
readonly reason: StraceUnavailableReason;
|
|
26
|
-
readonly diagnostics: string;
|
|
27
|
-
constructor(reason: StraceUnavailableReason, diagnostics: string);
|
|
28
|
-
}
|
|
29
|
-
export interface ParseStraceOptions {
|
|
30
|
-
destination: string;
|
|
31
|
-
traceFile?: string;
|
|
32
|
-
traceDir?: string;
|
|
33
|
-
}
|
|
34
|
-
/**
|
|
35
|
-
* Parse one command's strace output into a coarse `FileChange[]`. Failed
|
|
36
|
-
* syscalls are skipped; paths are filtered to `destination`; the trace file and
|
|
37
|
-
* `/proc`,`/sys`,`/dev` are excluded. Per-path final state collapses repeated
|
|
38
|
-
* writes to one `write`; a path written then deleted within the command is
|
|
39
|
-
* treated as transient and dropped.
|
|
40
|
-
*/
|
|
41
|
-
export declare function parseStraceTrace(raw: string, options: ParseStraceOptions): FileChange[];
|
|
42
|
-
export interface TraceFileChangesOptions {
|
|
43
|
-
destination: string;
|
|
44
|
-
onFileChanges?: (changes: FileChange[]) => void | Promise<void>;
|
|
45
|
-
/**
|
|
46
|
-
* Called when `onFileChanges` throws on the `spawn` path — its only failure
|
|
47
|
-
* signal, since spawn has no tool result and no exception catcher upstream. A
|
|
48
|
-
* throw on the tool-call path fails the command instead, so it never reaches
|
|
49
|
-
* here. Defaults to `console.warn`.
|
|
50
|
-
*/
|
|
51
|
-
onError?: (error: unknown) => void;
|
|
52
|
-
traceDir?: string;
|
|
53
|
-
}
|
|
54
|
-
/**
|
|
55
|
-
* Decorates a sandbox so each `executeCommand` is traced and its filesystem
|
|
56
|
-
* mutations parsed into a per-call `FileChange[]`. Per-call attribution is
|
|
57
|
-
* structural — one trace file per `callId` — so it is safe under concurrent
|
|
58
|
-
* tool calls. Each call's manifest is surfaced two stateless ways: attached to
|
|
59
|
-
* the tool result via the bash-meta channel (`meta.fileChanges`, hidden from the
|
|
60
|
-
* model) and passed to `onFileChanges`. No buffer is retained.
|
|
61
|
-
*
|
|
62
|
-
* `readFile` passes through unchanged; `writeFiles` is observed directly (it
|
|
63
|
-
* mutates outside strace's view) — each written file under the observation root
|
|
64
|
-
* becomes a `write` change fed to onFileChanges. `dispose` also sweeps the trace
|
|
65
|
-
* dir. `spawn` is traced the same way — its trace is read when the process exits
|
|
66
|
-
* (strace is the top process, so the trace is fully flushed by then). `spawn`
|
|
67
|
-
* carries no bash-meta scope, so its changes go to `onFileChanges` only.
|
|
68
|
-
*
|
|
69
|
-
* The trace file is always swept once parsed. A throwing `onFileChanges` on the
|
|
70
|
-
* tool-call path propagates to `withBashExceptionCatch` one level up: a caller's
|
|
71
|
-
* `BashException` becomes the tool result (via its `format()`), any other error
|
|
72
|
-
* fails the tool call. The `spawn` path has no exception catcher, so its throw is
|
|
73
|
-
* isolated and surfaced only via `onError` (exit result preserved).
|
|
74
|
-
*/
|
|
75
|
-
export declare function traceFileChanges(sandbox: DisposableSandbox, options: TraceFileChangesOptions): DisposableSandbox;
|
|
76
|
-
/**
|
|
77
|
-
* One-time probe at sandbox setup. Runs a known create/write/rename sequence
|
|
78
|
-
* under strace and asserts the trace is clean and parseable. Throws
|
|
79
|
-
* {@link StraceUnavailableError} (hard-fail) when ptrace is blocked, strace is
|
|
80
|
-
* absent, or the trace is garbled (e.g. amd64 under Rosetta).
|
|
81
|
-
*/
|
|
82
|
-
export declare function selfTestStrace(sandbox: DisposableSandbox): Promise<void>;
|
|
83
|
-
//# sourceMappingURL=file-changes.d.ts.map
|