@theokit/sdk 4.19.4 → 4.20.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/dist/sandbox/bwrap.d.cts +75 -0
- package/dist/sandbox/bwrap.d.ts +75 -0
- package/dist/sandbox/index.cjs +473 -12
- package/dist/sandbox/index.cjs.map +1 -1
- package/dist/sandbox/index.d.cts +3 -0
- package/dist/sandbox/index.d.ts +3 -0
- package/dist/sandbox/index.js +456 -14
- package/dist/sandbox/index.js.map +1 -1
- package/dist/sandbox/linux-sandbox.d.cts +88 -0
- package/dist/sandbox/linux-sandbox.d.ts +88 -0
- package/dist/sandbox/seccomp.d.cts +10 -0
- package/dist/sandbox/seccomp.d.ts +10 -0
- package/package.json +2 -2
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* M53 — bubblewrap argv + honest detection, faithful to Codex's Linux sandbox
|
|
3
|
+
* (`codex-rs/linux-sandbox/src/bwrap.rs` + `codex-rs/sandboxing/src/bwrap.rs`).
|
|
4
|
+
*
|
|
5
|
+
* HONEST SCOPE: filesystem confinement + network isolation via bwrap, PLUS the second stage —
|
|
6
|
+
* a cBPF seccomp syscall filter (`agents/sandbox/seccomp.ts`), wired in `agents/sandbox/backend.ts`
|
|
7
|
+
* via `restrictedSeccompPath()`. Portado no M63; este bloco afirmava o contrário até o M67 e
|
|
8
|
+
* SUBDECLARAVA a postura de segurança real. Limite honesto que permanece: o filtro é **x86_64**
|
|
9
|
+
* (guarda de arquitetura recusa instalar em outra arch, com WARN, e o confinamento de FS/rede do
|
|
10
|
+
* bwrap segue valendo) **e** só é instalado quando a rede está restrita (`backend.ts:87`, fiel a
|
|
11
|
+
* `landlock.rs:96-117`): com rede ligada não há filtro de syscall, apenas o confinamento de FS do
|
|
12
|
+
* bwrap. `danger-full-access` pula o bwrap por completo, espelhando `bwrap.rs:245-252`.
|
|
13
|
+
* Deltas versus o Codex seguem documentados em docs/CODEX-PARITY.md.
|
|
14
|
+
*/
|
|
15
|
+
/**
|
|
16
|
+
* Os tres modos canonicos do Codex. Definidos AQUI porque sao vocabulario do sandbox, nao da
|
|
17
|
+
* configuracao do consumidor: `danger-full-access` significa "nao embrulhe", e essa e uma decisao do
|
|
18
|
+
* subsistema de confinamento.
|
|
19
|
+
*/
|
|
20
|
+
export type SandboxMode = "read-only" | "workspace-write" | "danger-full-access";
|
|
21
|
+
export interface BwrapArgvOptions {
|
|
22
|
+
/** Workspace root — the single RW bind under `workspace-write` (protocol.rs:1189-1200). */
|
|
23
|
+
cwd: string;
|
|
24
|
+
/** `true` removes `--unshare-net` (policy `network_access`, default false). */
|
|
25
|
+
network?: boolean;
|
|
26
|
+
/** Injectable for tests; defaults to a real `existsSync` check on `<cwd>/.git`. */
|
|
27
|
+
gitDirExists?: boolean;
|
|
28
|
+
/**
|
|
29
|
+
* When present, emit `--clearenv` and re-inject ONLY these vars (Codex env_clear model,
|
|
30
|
+
* `exec_env.rs:25-31`). Closes the denylist gap: a secret in an oddly-named var never reaches the
|
|
31
|
+
* sandboxed child. Omitted ⇒ inherit the parent env (backward-compatible; SDK scrub still applies).
|
|
32
|
+
*/
|
|
33
|
+
env?: Record<string, string>;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Pure argv builder. Returns the bwrap flags ending in `--` (caller appends `/bin/sh -c <cmd>`),
|
|
37
|
+
* or `null` when the policy skips the sandbox entirely (`danger-full-access`).
|
|
38
|
+
*/
|
|
39
|
+
export declare function buildBwrapArgv(mode: SandboxMode, opts: BwrapArgvOptions): string[] | null;
|
|
40
|
+
/** Injectable probes — each mirrors one Codex availability check. */
|
|
41
|
+
export interface BwrapProbes {
|
|
42
|
+
/** `which bwrap` outside the cwd (anti-hijack, sandboxing/src/bwrap.rs:168-191). */
|
|
43
|
+
which: () => string | null;
|
|
44
|
+
/** `bwrap --help` text — must advertise `--perms` (launcher.rs:108-124). */
|
|
45
|
+
helpText: (bin: string) => string | null;
|
|
46
|
+
/** Active user-namespace probe with timeout (sandboxing/src/bwrap.rs:74-136). */
|
|
47
|
+
userns: (bin: string) => boolean;
|
|
48
|
+
}
|
|
49
|
+
export type BwrapDetection = {
|
|
50
|
+
ok: true;
|
|
51
|
+
bin: string;
|
|
52
|
+
} | {
|
|
53
|
+
ok: false;
|
|
54
|
+
reason: string;
|
|
55
|
+
};
|
|
56
|
+
/** Honest detection — fail-closed on every probe; NEVER throws (callers WARN + fall back). */
|
|
57
|
+
export declare function detectBwrap(probes?: BwrapProbes): BwrapDetection;
|
|
58
|
+
/** Quantas sondagens reais rodaram. Seam de TESTE — o gate de performance conta isto. */
|
|
59
|
+
export declare function realProbeCount(): number;
|
|
60
|
+
/** Real probes used in production. */
|
|
61
|
+
export declare const realProbes: BwrapProbes;
|
|
62
|
+
/**
|
|
63
|
+
* `detectBwrap` com memoização — o que a produção deve chamar.
|
|
64
|
+
*
|
|
65
|
+
* Note que `detectBwrap` em si **não** memoiza, de propósito: ele aceita probes injetados, e memoizar
|
|
66
|
+
* ali faria um teste com probes falsos envenenar o cache do processo para todos os outros.
|
|
67
|
+
*
|
|
68
|
+
* A revalidação do positivo NÃO é uma re-sondagem: `detectBwrap` gasta três probes (subprocesso
|
|
69
|
+
* `which` + `--help` + namespace de usuário). Aqui só se confirma que o binário validado continua no
|
|
70
|
+
* lugar. Se sumiu, o memo é rebaixado a negativo com o motivo dito — nunca promovido a positivo, que
|
|
71
|
+
* exigiria a sondagem cara de volta.
|
|
72
|
+
*/
|
|
73
|
+
export declare function detectBwrapMemoizado(probes?: BwrapProbes): BwrapDetection;
|
|
74
|
+
/** Seam de TESTE — limpa o memo. Produção nunca chama (ver m71-custo-por-turn#ADR-1). */
|
|
75
|
+
export declare function resetBwrapMemo(): void;
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* M53 — bubblewrap argv + honest detection, faithful to Codex's Linux sandbox
|
|
3
|
+
* (`codex-rs/linux-sandbox/src/bwrap.rs` + `codex-rs/sandboxing/src/bwrap.rs`).
|
|
4
|
+
*
|
|
5
|
+
* HONEST SCOPE: filesystem confinement + network isolation via bwrap, PLUS the second stage —
|
|
6
|
+
* a cBPF seccomp syscall filter (`agents/sandbox/seccomp.ts`), wired in `agents/sandbox/backend.ts`
|
|
7
|
+
* via `restrictedSeccompPath()`. Portado no M63; este bloco afirmava o contrário até o M67 e
|
|
8
|
+
* SUBDECLARAVA a postura de segurança real. Limite honesto que permanece: o filtro é **x86_64**
|
|
9
|
+
* (guarda de arquitetura recusa instalar em outra arch, com WARN, e o confinamento de FS/rede do
|
|
10
|
+
* bwrap segue valendo) **e** só é instalado quando a rede está restrita (`backend.ts:87`, fiel a
|
|
11
|
+
* `landlock.rs:96-117`): com rede ligada não há filtro de syscall, apenas o confinamento de FS do
|
|
12
|
+
* bwrap. `danger-full-access` pula o bwrap por completo, espelhando `bwrap.rs:245-252`.
|
|
13
|
+
* Deltas versus o Codex seguem documentados em docs/CODEX-PARITY.md.
|
|
14
|
+
*/
|
|
15
|
+
/**
|
|
16
|
+
* Os tres modos canonicos do Codex. Definidos AQUI porque sao vocabulario do sandbox, nao da
|
|
17
|
+
* configuracao do consumidor: `danger-full-access` significa "nao embrulhe", e essa e uma decisao do
|
|
18
|
+
* subsistema de confinamento.
|
|
19
|
+
*/
|
|
20
|
+
export type SandboxMode = "read-only" | "workspace-write" | "danger-full-access";
|
|
21
|
+
export interface BwrapArgvOptions {
|
|
22
|
+
/** Workspace root — the single RW bind under `workspace-write` (protocol.rs:1189-1200). */
|
|
23
|
+
cwd: string;
|
|
24
|
+
/** `true` removes `--unshare-net` (policy `network_access`, default false). */
|
|
25
|
+
network?: boolean;
|
|
26
|
+
/** Injectable for tests; defaults to a real `existsSync` check on `<cwd>/.git`. */
|
|
27
|
+
gitDirExists?: boolean;
|
|
28
|
+
/**
|
|
29
|
+
* When present, emit `--clearenv` and re-inject ONLY these vars (Codex env_clear model,
|
|
30
|
+
* `exec_env.rs:25-31`). Closes the denylist gap: a secret in an oddly-named var never reaches the
|
|
31
|
+
* sandboxed child. Omitted ⇒ inherit the parent env (backward-compatible; SDK scrub still applies).
|
|
32
|
+
*/
|
|
33
|
+
env?: Record<string, string>;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Pure argv builder. Returns the bwrap flags ending in `--` (caller appends `/bin/sh -c <cmd>`),
|
|
37
|
+
* or `null` when the policy skips the sandbox entirely (`danger-full-access`).
|
|
38
|
+
*/
|
|
39
|
+
export declare function buildBwrapArgv(mode: SandboxMode, opts: BwrapArgvOptions): string[] | null;
|
|
40
|
+
/** Injectable probes — each mirrors one Codex availability check. */
|
|
41
|
+
export interface BwrapProbes {
|
|
42
|
+
/** `which bwrap` outside the cwd (anti-hijack, sandboxing/src/bwrap.rs:168-191). */
|
|
43
|
+
which: () => string | null;
|
|
44
|
+
/** `bwrap --help` text — must advertise `--perms` (launcher.rs:108-124). */
|
|
45
|
+
helpText: (bin: string) => string | null;
|
|
46
|
+
/** Active user-namespace probe with timeout (sandboxing/src/bwrap.rs:74-136). */
|
|
47
|
+
userns: (bin: string) => boolean;
|
|
48
|
+
}
|
|
49
|
+
export type BwrapDetection = {
|
|
50
|
+
ok: true;
|
|
51
|
+
bin: string;
|
|
52
|
+
} | {
|
|
53
|
+
ok: false;
|
|
54
|
+
reason: string;
|
|
55
|
+
};
|
|
56
|
+
/** Honest detection — fail-closed on every probe; NEVER throws (callers WARN + fall back). */
|
|
57
|
+
export declare function detectBwrap(probes?: BwrapProbes): BwrapDetection;
|
|
58
|
+
/** Quantas sondagens reais rodaram. Seam de TESTE — o gate de performance conta isto. */
|
|
59
|
+
export declare function realProbeCount(): number;
|
|
60
|
+
/** Real probes used in production. */
|
|
61
|
+
export declare const realProbes: BwrapProbes;
|
|
62
|
+
/**
|
|
63
|
+
* `detectBwrap` com memoização — o que a produção deve chamar.
|
|
64
|
+
*
|
|
65
|
+
* Note que `detectBwrap` em si **não** memoiza, de propósito: ele aceita probes injetados, e memoizar
|
|
66
|
+
* ali faria um teste com probes falsos envenenar o cache do processo para todos os outros.
|
|
67
|
+
*
|
|
68
|
+
* A revalidação do positivo NÃO é uma re-sondagem: `detectBwrap` gasta três probes (subprocesso
|
|
69
|
+
* `which` + `--help` + namespace de usuário). Aqui só se confirma que o binário validado continua no
|
|
70
|
+
* lugar. Se sumiu, o memo é rebaixado a negativo com o motivo dito — nunca promovido a positivo, que
|
|
71
|
+
* exigiria a sondagem cara de volta.
|
|
72
|
+
*/
|
|
73
|
+
export declare function detectBwrapMemoizado(probes?: BwrapProbes): BwrapDetection;
|
|
74
|
+
/** Seam de TESTE — limpa o memo. Produção nunca chama (ver m71-custo-por-turn#ADR-1). */
|
|
75
|
+
export declare function resetBwrapMemo(): void;
|
package/dist/sandbox/index.cjs
CHANGED
|
@@ -1,10 +1,242 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
var child_process = require('child_process');
|
|
4
|
-
var
|
|
4
|
+
var fs = require('fs');
|
|
5
5
|
var path = require('path');
|
|
6
|
+
var os = require('os');
|
|
7
|
+
var promises = require('fs/promises');
|
|
6
8
|
|
|
7
|
-
|
|
9
|
+
function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
|
|
10
|
+
|
|
11
|
+
var path__default = /*#__PURE__*/_interopDefault(path);
|
|
12
|
+
|
|
13
|
+
// src/sandbox/bwrap.ts
|
|
14
|
+
function buildBwrapArgv(mode, opts) {
|
|
15
|
+
if (mode === "danger-full-access") return null;
|
|
16
|
+
const cwd = path__default.default.resolve(opts.cwd);
|
|
17
|
+
const gitDir = path__default.default.join(cwd, ".git");
|
|
18
|
+
const hasGit = opts.gitDirExists ?? fs.existsSync(gitDir);
|
|
19
|
+
const argv = [
|
|
20
|
+
// core, always (bwrap.rs:318-332; user+pid namespaces explicit so it works as root in containers)
|
|
21
|
+
"--new-session",
|
|
22
|
+
"--die-with-parent",
|
|
23
|
+
"--unshare-user",
|
|
24
|
+
"--unshare-pid",
|
|
25
|
+
// full-read filesystem base (bwrap.rs:446-452)
|
|
26
|
+
"--ro-bind",
|
|
27
|
+
"/",
|
|
28
|
+
"/",
|
|
29
|
+
"--dev",
|
|
30
|
+
"/dev",
|
|
31
|
+
"--proc",
|
|
32
|
+
"/proc"
|
|
33
|
+
];
|
|
34
|
+
if (!opts.network) argv.push("--unshare-net");
|
|
35
|
+
if (opts.env) argv.push("--clearenv");
|
|
36
|
+
const setenv = {
|
|
37
|
+
...opts.env ?? {},
|
|
38
|
+
// the flag signals the child that network is unshared (spawn.rs:20,79)
|
|
39
|
+
...opts.network ? {} : { CODEX_SANDBOX_NETWORK_DISABLED: "1" }
|
|
40
|
+
};
|
|
41
|
+
for (const [k, v] of Object.entries(setenv)) argv.push("--setenv", k, v);
|
|
42
|
+
if (mode === "workspace-write") {
|
|
43
|
+
argv.push("--bind", cwd, cwd, "--bind", "/tmp", "/tmp");
|
|
44
|
+
if (hasGit) argv.push("--ro-bind", gitDir, gitDir);
|
|
45
|
+
}
|
|
46
|
+
argv.push("--chdir", cwd, "--");
|
|
47
|
+
return argv;
|
|
48
|
+
}
|
|
49
|
+
function detectBwrap(probes = realProbes) {
|
|
50
|
+
try {
|
|
51
|
+
const bin = probes.which();
|
|
52
|
+
if (!bin) return { ok: false, reason: "bwrap not found in PATH" };
|
|
53
|
+
const help = probes.helpText(bin);
|
|
54
|
+
if (!help?.includes("--perms")) {
|
|
55
|
+
return { ok: false, reason: `bwrap at ${bin} lacks --perms support (too old)` };
|
|
56
|
+
}
|
|
57
|
+
if (!probes.userns(bin)) {
|
|
58
|
+
return { ok: false, reason: "user namespaces unavailable (container/kernel restriction)" };
|
|
59
|
+
}
|
|
60
|
+
return { ok: true, bin };
|
|
61
|
+
} catch (err) {
|
|
62
|
+
return {
|
|
63
|
+
ok: false,
|
|
64
|
+
reason: `bwrap probe failed: ${err instanceof Error ? err.message : String(err)}`
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
var sondagensReais = 0;
|
|
69
|
+
function realProbeCount() {
|
|
70
|
+
return sondagensReais;
|
|
71
|
+
}
|
|
72
|
+
var realProbes = {
|
|
73
|
+
which: () => {
|
|
74
|
+
sondagensReais++;
|
|
75
|
+
try {
|
|
76
|
+
const out = child_process.execFileSync("which", ["bwrap"], { encoding: "utf8", timeout: 2e3 }).trim();
|
|
77
|
+
if (!out || out.startsWith(process.cwd() + path__default.default.sep)) return null;
|
|
78
|
+
return out;
|
|
79
|
+
} catch {
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
},
|
|
83
|
+
helpText: (bin) => {
|
|
84
|
+
try {
|
|
85
|
+
return child_process.execFileSync(bin, ["--help"], { encoding: "utf8", timeout: 2e3 });
|
|
86
|
+
} catch (err) {
|
|
87
|
+
const e = err;
|
|
88
|
+
return [e.stdout, e.stderr].filter(Boolean).join("\n") || null;
|
|
89
|
+
}
|
|
90
|
+
},
|
|
91
|
+
userns: (bin) => {
|
|
92
|
+
try {
|
|
93
|
+
child_process.execFileSync(bin, ["--unshare-user", "--unshare-net", "--ro-bind", "/", "/", "/bin/true"], {
|
|
94
|
+
timeout: 500,
|
|
95
|
+
stdio: "ignore"
|
|
96
|
+
});
|
|
97
|
+
return true;
|
|
98
|
+
} catch {
|
|
99
|
+
return false;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
var memo;
|
|
104
|
+
function detectBwrapMemoizado(probes = realProbes) {
|
|
105
|
+
memo ??= detectBwrap(probes);
|
|
106
|
+
if (memo.ok && !fs.existsSync(memo.bin)) {
|
|
107
|
+
memo = { ok: false, reason: `bwrap disappeared from ${memo.bin} after detection` };
|
|
108
|
+
}
|
|
109
|
+
return memo;
|
|
110
|
+
}
|
|
111
|
+
function resetBwrapMemo() {
|
|
112
|
+
memo = void 0;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// src/internal/security/redact.ts
|
|
116
|
+
var REDACT_ENABLED = readEnvOnce();
|
|
117
|
+
function readEnvOnce() {
|
|
118
|
+
const raw = process.env.THEOKIT_REDACT_SECRETS;
|
|
119
|
+
if (raw === void 0) return true;
|
|
120
|
+
return ["1", "true", "yes", "on"].includes(raw.toLowerCase());
|
|
121
|
+
}
|
|
122
|
+
var warnedOptOut = false;
|
|
123
|
+
if (!REDACT_ENABLED && !warnedOptOut) {
|
|
124
|
+
process.stderr.write(
|
|
125
|
+
"[theokit-sdk] Secret redaction is DISABLED via THEOKIT_REDACT_SECRETS. Credentials may leak into errors, telemetry, logs, transcripts.\n"
|
|
126
|
+
);
|
|
127
|
+
warnedOptOut = true;
|
|
128
|
+
}
|
|
129
|
+
var BUILTIN_PATTERNS = [
|
|
130
|
+
// T5.4: 30+ vendor prefixes (was 12 pre-T5.4). Order matters — more
|
|
131
|
+
// specific prefixes precede generic ones (e.g., sk-ant-admin01 before
|
|
132
|
+
// sk-ant-, sk-proj- before sk-). PEM block deliberately first so its
|
|
133
|
+
// multi-line span runs before any per-line patterns can fire.
|
|
134
|
+
/-----BEGIN[ ]+(?:RSA |EC |DSA |OPENSSH |ENCRYPTED |)PRIVATE KEY-----[\s\S]+?-----END[ ]+(?:RSA |EC |DSA |OPENSSH |ENCRYPTED |)PRIVATE KEY-----/g,
|
|
135
|
+
// JWT — exact 3-segment base64url. Dotted; the body floor of 4 chars per
|
|
136
|
+
// segment matches the minimum legal payload while skipping `a.b.c` noise.
|
|
137
|
+
/eyJ[A-Za-z0-9_-]{4,}\.eyJ[A-Za-z0-9_-]{4,}\.[A-Za-z0-9_-]{4,}/g,
|
|
138
|
+
// Azure Storage SAS — match the sig= component (URL-encoded base64).
|
|
139
|
+
/(?<=[?&]sig=)[A-Za-z0-9%+/]{20,}/g,
|
|
140
|
+
// Anthropic
|
|
141
|
+
/sk-ant-admin01-[A-Za-z0-9_-]{10,}/g,
|
|
142
|
+
// Anthropic admin keys (must precede sk-ant-)
|
|
143
|
+
/sk-ant-[A-Za-z0-9_-]{10,}/g,
|
|
144
|
+
// Anthropic regular
|
|
145
|
+
// OpenAI family + clones (sk- generic must come AFTER all sk-foo- variants)
|
|
146
|
+
/sk-proj-[A-Za-z0-9_-]{10,}/g,
|
|
147
|
+
// OpenAI project key (must precede sk- generic)
|
|
148
|
+
/sk-[A-Za-z0-9_-]{10,}/g,
|
|
149
|
+
// OpenAI / OpenRouter / DeepInfra / Together / DeepSeek
|
|
150
|
+
// Provider prefixes (alphabetized for maintainability)
|
|
151
|
+
/AIza[A-Za-z0-9_-]{35}/g,
|
|
152
|
+
// Google API key
|
|
153
|
+
/AKIA[A-Z0-9]{16}/g,
|
|
154
|
+
// AWS access key
|
|
155
|
+
/fw_[A-Za-z0-9]{20,}/g,
|
|
156
|
+
// Fireworks
|
|
157
|
+
/glpat-[A-Za-z0-9_-]{20}/g,
|
|
158
|
+
// GitLab PAT
|
|
159
|
+
/ghp_[A-Za-z0-9]{36}/g,
|
|
160
|
+
// GitHub PAT classic
|
|
161
|
+
/github_pat_[A-Za-z0-9_]{82}/g,
|
|
162
|
+
// GitHub PAT fine-grained
|
|
163
|
+
/gsk_[A-Za-z0-9]{20,}/g,
|
|
164
|
+
// Groq
|
|
165
|
+
/hf_[A-Za-z0-9]{20,}/g,
|
|
166
|
+
// HuggingFace
|
|
167
|
+
/\bpa-[A-Za-z0-9_-]{20,}/g,
|
|
168
|
+
// Voyage AI (word-boundary to skip CSS / kebab IDs)
|
|
169
|
+
/pcsk_[A-Za-z0-9_-]{20,}/g,
|
|
170
|
+
// Pinecone
|
|
171
|
+
/pplx-[A-Za-z0-9_-]{20,}/g,
|
|
172
|
+
// Perplexity
|
|
173
|
+
/r8_[A-Za-z0-9_-]{20,}/g,
|
|
174
|
+
// Replicate
|
|
175
|
+
/rk_live_[A-Za-z0-9]{20,}/g,
|
|
176
|
+
// Stripe restricted
|
|
177
|
+
/sk_live_[A-Za-z0-9]{20,}/g,
|
|
178
|
+
// Stripe secret
|
|
179
|
+
/sntrys_[A-Za-z0-9]{40,}/g,
|
|
180
|
+
// Sentry user auth
|
|
181
|
+
/xai-[A-Za-z0-9_-]{20,}/g,
|
|
182
|
+
// xAI (Grok)
|
|
183
|
+
/xox[bpasr]-[A-Za-z0-9-]{10,}/g,
|
|
184
|
+
//Slack tokens
|
|
185
|
+
// Additional unique-prefix tokens with low false-positive risk
|
|
186
|
+
/npm_[A-Za-z0-9]{36}/g,
|
|
187
|
+
// npm access token
|
|
188
|
+
/SG\.[A-Za-z0-9_-]{22}\.[A-Za-z0-9_-]{43}/g,
|
|
189
|
+
// SendGrid
|
|
190
|
+
/\bSK[A-Za-z0-9]{32}\b/g,
|
|
191
|
+
// Twilio API SID (word-boundary to skip CSS class noise)
|
|
192
|
+
/\bkey-[a-f0-9]{32}\b/g,
|
|
193
|
+
// Mailgun (hex-only narrows false positives)
|
|
194
|
+
/MT[A-Za-z0-9_-]{23}\.[A-Za-z0-9_-]{6}\.[A-Za-z0-9_-]{27}/g,
|
|
195
|
+
// Discord bot
|
|
196
|
+
/\b(?:sdk|mob)-[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}\b/g
|
|
197
|
+
// LaunchDarkly
|
|
198
|
+
];
|
|
199
|
+
var BEARER_PATTERN = /\b(Bearer\s+)([A-Za-z0-9_\-.+/=]{8,})/g;
|
|
200
|
+
var PARAM_PATTERN = /(\b(?:access_token|api_key|api-key|client_secret|credential|credentials|id_token|jwt|password|private_key|refresh_token|secret|service_account|session_token|token|x-api-key)\b["']?\s*[:=]\s*["']?)([A-Za-z0-9_\-.+/]+)/gi;
|
|
201
|
+
var _extraPatterns = [];
|
|
202
|
+
function maskToken(token) {
|
|
203
|
+
if (token.length < 18) return "***";
|
|
204
|
+
return `${token.slice(0, 6)}...${token.slice(-4)}`;
|
|
205
|
+
}
|
|
206
|
+
var MASK_SHAPE = /^.{6}\.\.\..{4}$/s;
|
|
207
|
+
function coerceToString(value) {
|
|
208
|
+
if (typeof value === "string") return value;
|
|
209
|
+
if (value === null || value === void 0) return null;
|
|
210
|
+
if (typeof value === "object") {
|
|
211
|
+
try {
|
|
212
|
+
const s = JSON.stringify(value);
|
|
213
|
+
return s === void 0 ? null : s;
|
|
214
|
+
} catch {
|
|
215
|
+
return "[unredactable: circular]";
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
return String(value);
|
|
219
|
+
}
|
|
220
|
+
function redactSecrets(text, opts) {
|
|
221
|
+
const coerced = coerceToString(text);
|
|
222
|
+
if (coerced === null) return "";
|
|
223
|
+
if (!REDACT_ENABLED) return coerced;
|
|
224
|
+
let s = coerced;
|
|
225
|
+
for (const re of BUILTIN_PATTERNS) {
|
|
226
|
+
s = s.replace(re, (m) => maskToken(m));
|
|
227
|
+
}
|
|
228
|
+
for (const re of _extraPatterns) {
|
|
229
|
+
s = s.replace(re, (m) => maskToken(m));
|
|
230
|
+
}
|
|
231
|
+
{
|
|
232
|
+
s = s.replace(BEARER_PATTERN, (_, prefix) => `${prefix}***`);
|
|
233
|
+
s = s.replace(PARAM_PATTERN, (whole, prefix, value) => {
|
|
234
|
+
if (MASK_SHAPE.test(value)) return whole;
|
|
235
|
+
return `${prefix}***`;
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
return s;
|
|
239
|
+
}
|
|
8
240
|
|
|
9
241
|
// src/internal/runtime/lifecycle/env-policy.ts
|
|
10
242
|
var SECRET_PATTERNS = [
|
|
@@ -91,15 +323,15 @@ var SandboxBackend = class {
|
|
|
91
323
|
env: config.env ?? "inherit-scrubbed"
|
|
92
324
|
};
|
|
93
325
|
}
|
|
94
|
-
async readFile(
|
|
95
|
-
const result = await this.execute(`cat ${this.shellEscape(
|
|
326
|
+
async readFile(path2) {
|
|
327
|
+
const result = await this.execute(`cat ${this.shellEscape(path2)}`);
|
|
96
328
|
if (result.exitCode !== 0) {
|
|
97
329
|
throw new Error(`readFile failed: ${result.stderr}`);
|
|
98
330
|
}
|
|
99
331
|
return result.stdout;
|
|
100
332
|
}
|
|
101
|
-
async writeFile(
|
|
102
|
-
await this.uploadFile(
|
|
333
|
+
async writeFile(path2, content) {
|
|
334
|
+
await this.uploadFile(path2, content);
|
|
103
335
|
}
|
|
104
336
|
async glob(pattern, cwd) {
|
|
105
337
|
const dir = cwd ?? this.config.workDir ?? ".";
|
|
@@ -109,16 +341,16 @@ var SandboxBackend = class {
|
|
|
109
341
|
if (result.exitCode !== 0) return [];
|
|
110
342
|
return result.stdout.trim().split("\n").filter(Boolean);
|
|
111
343
|
}
|
|
112
|
-
async grep(pattern,
|
|
113
|
-
const target =
|
|
344
|
+
async grep(pattern, path2) {
|
|
345
|
+
const target = path2 ?? ".";
|
|
114
346
|
const result = await this.execute(
|
|
115
347
|
`grep -rn ${this.shellEscape(pattern)} ${this.shellEscape(target)} 2>/dev/null`
|
|
116
348
|
);
|
|
117
349
|
if (result.exitCode !== 0) return [];
|
|
118
350
|
return result.stdout.trim().split("\n").filter(Boolean);
|
|
119
351
|
}
|
|
120
|
-
async listDir(
|
|
121
|
-
const result = await this.execute(`ls -1 ${this.shellEscape(
|
|
352
|
+
async listDir(path2) {
|
|
353
|
+
const result = await this.execute(`ls -1 ${this.shellEscape(path2)}`);
|
|
122
354
|
if (result.exitCode !== 0) return [];
|
|
123
355
|
return result.stdout.trim().split("\n").filter(Boolean);
|
|
124
356
|
}
|
|
@@ -176,13 +408,227 @@ var LocalSandbox = class extends SandboxBackend {
|
|
|
176
408
|
timedOut
|
|
177
409
|
};
|
|
178
410
|
}
|
|
179
|
-
async uploadFile(
|
|
180
|
-
const fullPath =
|
|
411
|
+
async uploadFile(path2, content) {
|
|
412
|
+
const fullPath = path2.startsWith("/") ? path2 : `${this.config.workDir}/${path2}`;
|
|
181
413
|
await promises.mkdir(path.dirname(fullPath), { recursive: true });
|
|
182
414
|
await promises.writeFile(fullPath, content, "utf-8");
|
|
183
415
|
}
|
|
184
416
|
};
|
|
185
417
|
|
|
418
|
+
// src/sandbox/seccomp.ts
|
|
419
|
+
var BPF_LD = 0;
|
|
420
|
+
var BPF_W = 0;
|
|
421
|
+
var BPF_ABS = 32;
|
|
422
|
+
var BPF_JMP = 5;
|
|
423
|
+
var BPF_JEQ = 16;
|
|
424
|
+
var BPF_JGE = 48;
|
|
425
|
+
var BPF_K = 0;
|
|
426
|
+
var BPF_RET = 6;
|
|
427
|
+
var LD_ABS_W = BPF_LD | BPF_W | BPF_ABS;
|
|
428
|
+
var JEQ_K = BPF_JMP | BPF_JEQ | BPF_K;
|
|
429
|
+
var JGE_K = BPF_JMP | BPF_JGE | BPF_K;
|
|
430
|
+
var RET_K = BPF_RET | BPF_K;
|
|
431
|
+
var OFF_NR = 0;
|
|
432
|
+
var OFF_ARCH = 4;
|
|
433
|
+
var OFF_ARG0 = 16;
|
|
434
|
+
var AUDIT_ARCH_X86_64 = 3221225534;
|
|
435
|
+
var X32_BIT = 1073741824;
|
|
436
|
+
var SECCOMP_RET_ALLOW = 2147418112;
|
|
437
|
+
var SECCOMP_RET_ERRNO = 327680;
|
|
438
|
+
var EPERM = 1;
|
|
439
|
+
var RET_EPERM = SECCOMP_RET_ERRNO | EPERM & 65535;
|
|
440
|
+
var SECCOMP_RET_KILL_PROCESS = 2147483648;
|
|
441
|
+
var AF_UNIX = 1;
|
|
442
|
+
var ALWAYS_DENIED = [101, 310, 311, 425, 426, 427];
|
|
443
|
+
var NETWORK_DENIED = [42, 43, 288, 49, 50, 52, 51, 48, 44, 307, 299, 55, 54];
|
|
444
|
+
var SOCKET_SYSCALLS = [41, 53];
|
|
445
|
+
var stmt = (code, k) => ({ code, jt: 0, jf: 0, k });
|
|
446
|
+
var jmp = (code, k, jt, jf) => ({ code, jt, jf, k });
|
|
447
|
+
function buildSeccompFilter(opts) {
|
|
448
|
+
const insns = [];
|
|
449
|
+
const ALLOW = /* @__PURE__ */ Symbol("ALLOW");
|
|
450
|
+
const DENY = /* @__PURE__ */ Symbol("DENY");
|
|
451
|
+
const KILL = /* @__PURE__ */ Symbol("KILL");
|
|
452
|
+
const body = [];
|
|
453
|
+
const push = (code, k, jt = 0, jf = 0) => {
|
|
454
|
+
body.push({ code, k, jt, jf });
|
|
455
|
+
};
|
|
456
|
+
push(LD_ABS_W, OFF_ARCH);
|
|
457
|
+
push(JEQ_K, AUDIT_ARCH_X86_64, 0, KILL);
|
|
458
|
+
push(LD_ABS_W, OFF_NR);
|
|
459
|
+
push(JGE_K, X32_BIT, KILL, 0);
|
|
460
|
+
const denied = opts.networkRestricted ? [...ALWAYS_DENIED, ...NETWORK_DENIED] : [...ALWAYS_DENIED];
|
|
461
|
+
for (const nr of denied) push(JEQ_K, nr, DENY, 0);
|
|
462
|
+
if (opts.networkRestricted) {
|
|
463
|
+
for (const sysno of SOCKET_SYSCALLS) {
|
|
464
|
+
push(JEQ_K, sysno, 0, 2);
|
|
465
|
+
push(LD_ABS_W, OFF_ARG0);
|
|
466
|
+
push(JEQ_K, AF_UNIX, ALLOW, DENY);
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
push(RET_K, SECCOMP_RET_ALLOW);
|
|
470
|
+
const allowIdx = body.length - 1;
|
|
471
|
+
push(RET_K, RET_EPERM);
|
|
472
|
+
const denyIdx = body.length - 1;
|
|
473
|
+
push(RET_K, SECCOMP_RET_KILL_PROCESS);
|
|
474
|
+
const killIdx = body.length - 1;
|
|
475
|
+
const resolve = (i, t) => {
|
|
476
|
+
const abs = t === ALLOW ? allowIdx : t === DENY ? denyIdx : t === KILL ? killIdx : i + 1 + t;
|
|
477
|
+
const off = abs - (i + 1);
|
|
478
|
+
if (off < 0 || off > 255) throw new RangeError(`seccomp jump out of range at ${i}: ${off}`);
|
|
479
|
+
return off;
|
|
480
|
+
};
|
|
481
|
+
for (const [i, b] of body.entries()) {
|
|
482
|
+
if (b.code === JEQ_K || b.code === JGE_K) {
|
|
483
|
+
insns.push(jmp(b.code, b.k, resolve(i, b.jt), resolve(i, b.jf)));
|
|
484
|
+
} else {
|
|
485
|
+
insns.push(stmt(b.code, b.k));
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
const buf = Buffer.alloc(insns.length * 8);
|
|
489
|
+
insns.forEach((ins, i) => {
|
|
490
|
+
buf.writeUInt16LE(ins.code, i * 8);
|
|
491
|
+
buf.writeUInt8(ins.jt, i * 8 + 2);
|
|
492
|
+
buf.writeUInt8(ins.jf, i * 8 + 3);
|
|
493
|
+
buf.writeUInt32LE(ins.k >>> 0, i * 8 + 4);
|
|
494
|
+
});
|
|
495
|
+
return buf;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
// src/sandbox/linux-sandbox.ts
|
|
499
|
+
function shellQuote(s) {
|
|
500
|
+
return `'${s.replaceAll("'", `'\\''`)}'`;
|
|
501
|
+
}
|
|
502
|
+
function wrapCommandForSandbox(mode, opts, command) {
|
|
503
|
+
const argv = buildBwrapArgv(mode, { cwd: opts.cwd, network: opts.network, env: opts.env });
|
|
504
|
+
if (argv === null) return null;
|
|
505
|
+
const bin = opts.bin ?? "bwrap";
|
|
506
|
+
const seccompArgv = opts.seccompPath !== void 0 ? ["--seccomp", "3"] : [];
|
|
507
|
+
const base = `${shellQuote(bin)} ${[...argv.slice(0, -1), ...seccompArgv, "--"].map(shellQuote).join(" ")} /bin/sh -c ${shellQuote(command)}`;
|
|
508
|
+
return opts.seccompPath !== void 0 ? `${base} 3< ${shellQuote(opts.seccompPath)}` : base;
|
|
509
|
+
}
|
|
510
|
+
var ENV_ALLOWLIST = [
|
|
511
|
+
"PATH",
|
|
512
|
+
"HOME",
|
|
513
|
+
"LANG",
|
|
514
|
+
"LC_ALL",
|
|
515
|
+
"LC_CTYPE",
|
|
516
|
+
"TERM",
|
|
517
|
+
"USER",
|
|
518
|
+
"TMPDIR",
|
|
519
|
+
"SHELL"
|
|
520
|
+
];
|
|
521
|
+
function allowlistedEnv(source = process.env) {
|
|
522
|
+
const out = {};
|
|
523
|
+
for (const k of ENV_ALLOWLIST) {
|
|
524
|
+
const v = source[k];
|
|
525
|
+
if (v !== void 0) out[k] = v;
|
|
526
|
+
}
|
|
527
|
+
return out;
|
|
528
|
+
}
|
|
529
|
+
var LinuxSandbox = class extends LocalSandbox {
|
|
530
|
+
mode;
|
|
531
|
+
network;
|
|
532
|
+
cwd;
|
|
533
|
+
bin;
|
|
534
|
+
env;
|
|
535
|
+
/** M63 — path to the cBPF seccomp program written host-side; passed to `bwrap --seccomp 3` via a
|
|
536
|
+
* shell redirect. `undefined` when the network is unrestricted OR generation failed (honest fallback). */
|
|
537
|
+
seccompPath;
|
|
538
|
+
constructor(config, opts) {
|
|
539
|
+
super(config);
|
|
540
|
+
this.mode = opts.mode;
|
|
541
|
+
this.network = opts.network ?? false;
|
|
542
|
+
this.cwd = config.workDir ?? process.cwd();
|
|
543
|
+
this.bin = opts.bin ?? "bwrap";
|
|
544
|
+
this.env = opts.env ?? allowlistedEnv();
|
|
545
|
+
this.seccompPath = this.network ? void 0 : restrictedSeccompPath();
|
|
546
|
+
}
|
|
547
|
+
/** Extracted for test visibility — delegates to the pure `wrapCommandForSandbox` (M57, single wrap SoT). */
|
|
548
|
+
wrapCommand(command) {
|
|
549
|
+
return wrapCommandForSandbox(
|
|
550
|
+
this.mode,
|
|
551
|
+
{
|
|
552
|
+
cwd: this.cwd,
|
|
553
|
+
network: this.network,
|
|
554
|
+
env: this.env,
|
|
555
|
+
bin: this.bin,
|
|
556
|
+
seccompPath: this.seccompPath
|
|
557
|
+
},
|
|
558
|
+
command
|
|
559
|
+
);
|
|
560
|
+
}
|
|
561
|
+
execute(command, opts) {
|
|
562
|
+
const wrapped = this.wrapCommand(command);
|
|
563
|
+
if (wrapped === null) return super.execute(command, opts);
|
|
564
|
+
return super.execute(wrapped, opts);
|
|
565
|
+
}
|
|
566
|
+
};
|
|
567
|
+
var warnedNonX64 = false;
|
|
568
|
+
function seccompPathForArch(arch, warn) {
|
|
569
|
+
if (arch !== "x64") {
|
|
570
|
+
if (!warnedNonX64) {
|
|
571
|
+
warnedNonX64 = true;
|
|
572
|
+
warn(
|
|
573
|
+
`[sandbox] seccomp syscall filter unsupported on ${arch} (x86_64 only in v1) \u2014 running without the filter; bwrap FS/network confinement still applies.`
|
|
574
|
+
);
|
|
575
|
+
}
|
|
576
|
+
return void 0;
|
|
577
|
+
}
|
|
578
|
+
try {
|
|
579
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "ab-seccomp-"));
|
|
580
|
+
const path2 = path.join(dir, "filter.bpf");
|
|
581
|
+
fs.writeFileSync(path2, buildSeccompFilter({ networkRestricted: true }));
|
|
582
|
+
const cleanup = () => fs.rmSync(dir, { recursive: true, force: true });
|
|
583
|
+
process.once("exit", cleanup);
|
|
584
|
+
process.once("SIGINT", cleanup);
|
|
585
|
+
process.once("SIGTERM", cleanup);
|
|
586
|
+
return path2;
|
|
587
|
+
} catch (err) {
|
|
588
|
+
warn(
|
|
589
|
+
`[sandbox] seccomp filter unavailable (${err instanceof Error ? err.message : String(err)}) \u2014 running without syscall filter (bwrap FS/network confinement still applies).`
|
|
590
|
+
);
|
|
591
|
+
return void 0;
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
var seccompFilterPath;
|
|
595
|
+
function restrictedSeccompPath() {
|
|
596
|
+
if (seccompFilterPath !== void 0) return seccompFilterPath ?? void 0;
|
|
597
|
+
const path2 = seccompPathForArch(process.arch, (m) => console.warn(redactSecrets(m)));
|
|
598
|
+
seccompFilterPath = path2 ?? null;
|
|
599
|
+
return path2;
|
|
600
|
+
}
|
|
601
|
+
var warnedUnavailable = false;
|
|
602
|
+
function resetSandboxWarnLatch() {
|
|
603
|
+
warnedUnavailable = false;
|
|
604
|
+
}
|
|
605
|
+
function resolveSandboxPosture(opts) {
|
|
606
|
+
if (opts.mode === "danger-full-access") {
|
|
607
|
+
return { mode: opts.mode, enforced: false, detail: "no confinement (danger-full-access)" };
|
|
608
|
+
}
|
|
609
|
+
const detection = (opts.detect ?? detectBwrapMemoizado)();
|
|
610
|
+
if (!detection.ok) {
|
|
611
|
+
return { mode: opts.mode, enforced: false, detail: `tool-gating only \u2014 ${detection.reason}` };
|
|
612
|
+
}
|
|
613
|
+
return { mode: opts.mode, enforced: true, detail: "kernel (bwrap)" };
|
|
614
|
+
}
|
|
615
|
+
function createSandboxBackend(opts) {
|
|
616
|
+
const config = { workDir: opts.workDir, timeoutMs: opts.timeoutMs };
|
|
617
|
+
if (opts.mode === "danger-full-access") return new LocalSandbox(config);
|
|
618
|
+
const detection = (opts.detect ?? detectBwrapMemoizado)();
|
|
619
|
+
if (!detection.ok) {
|
|
620
|
+
if (!warnedUnavailable) {
|
|
621
|
+
warnedUnavailable = true;
|
|
622
|
+
const warn = opts.warn ?? ((m) => console.warn(redactSecrets(m)));
|
|
623
|
+
warn(
|
|
624
|
+
`[sandbox] OS-level enforcement unavailable (${detection.reason}) \u2014 falling back to tool-level gating only (sandbox_mode=${opts.mode}).`
|
|
625
|
+
);
|
|
626
|
+
}
|
|
627
|
+
return new LocalSandbox(config);
|
|
628
|
+
}
|
|
629
|
+
return new LinuxSandbox(config, { mode: opts.mode, network: opts.network, bin: detection.bin });
|
|
630
|
+
}
|
|
631
|
+
|
|
186
632
|
// src/errors.ts
|
|
187
633
|
var TheokitAgentError = class extends Error {
|
|
188
634
|
name = "TheokitAgentError";
|
|
@@ -248,12 +694,27 @@ async function provisionRepo(sandboxOrOpts, maybeOpts) {
|
|
|
248
694
|
return { repoDir };
|
|
249
695
|
}
|
|
250
696
|
|
|
697
|
+
exports.LinuxSandbox = LinuxSandbox;
|
|
251
698
|
exports.LocalSandbox = LocalSandbox;
|
|
252
699
|
exports.RepoProvisionError = RepoProvisionError;
|
|
253
700
|
exports.SandboxBackend = SandboxBackend;
|
|
254
701
|
exports.SandboxNotAvailableError = SandboxNotAvailableError;
|
|
255
702
|
exports.SandboxSecurityError = SandboxSecurityError;
|
|
703
|
+
exports.allowlistedEnv = allowlistedEnv;
|
|
704
|
+
exports.buildBwrapArgv = buildBwrapArgv;
|
|
705
|
+
exports.buildSeccompFilter = buildSeccompFilter;
|
|
706
|
+
exports.createSandboxBackend = createSandboxBackend;
|
|
707
|
+
exports.detectBwrap = detectBwrap;
|
|
708
|
+
exports.detectBwrapMemoizado = detectBwrapMemoizado;
|
|
256
709
|
exports.provisionRepo = provisionRepo;
|
|
710
|
+
exports.realProbeCount = realProbeCount;
|
|
711
|
+
exports.realProbes = realProbes;
|
|
712
|
+
exports.resetBwrapMemo = resetBwrapMemo;
|
|
713
|
+
exports.resetSandboxWarnLatch = resetSandboxWarnLatch;
|
|
257
714
|
exports.resolveSandbox = resolveSandbox;
|
|
715
|
+
exports.resolveSandboxPosture = resolveSandboxPosture;
|
|
716
|
+
exports.restrictedSeccompPath = restrictedSeccompPath;
|
|
717
|
+
exports.seccompPathForArch = seccompPathForArch;
|
|
718
|
+
exports.wrapCommandForSandbox = wrapCommandForSandbox;
|
|
258
719
|
//# sourceMappingURL=index.cjs.map
|
|
259
720
|
//# sourceMappingURL=index.cjs.map
|