@celestea/tools 2.7.1

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 (140) hide show
  1. package/LICENSE +21 -0
  2. package/dist/args.d.ts +15 -0
  3. package/dist/args.js +55 -0
  4. package/dist/attachments/image-header.d.ts +28 -0
  5. package/dist/attachments/image-header.js +148 -0
  6. package/dist/attachments/store.d.ts +56 -0
  7. package/dist/attachments/store.js +148 -0
  8. package/dist/browser/cdp.d.ts +162 -0
  9. package/dist/browser/cdp.js +269 -0
  10. package/dist/browser/launch.d.ts +92 -0
  11. package/dist/browser/launch.js +232 -0
  12. package/dist/browser/memory-guard.d.ts +55 -0
  13. package/dist/browser/memory-guard.js +210 -0
  14. package/dist/browser/session.d.ts +146 -0
  15. package/dist/browser/session.js +371 -0
  16. package/dist/browser/snapshot.d.ts +69 -0
  17. package/dist/browser/snapshot.js +163 -0
  18. package/dist/browser/types.d.ts +51 -0
  19. package/dist/browser/types.js +8 -0
  20. package/dist/builtin.d.ts +54 -0
  21. package/dist/builtin.js +77 -0
  22. package/dist/desc.d.ts +20 -0
  23. package/dist/desc.js +22 -0
  24. package/dist/disclosure.d.ts +113 -0
  25. package/dist/disclosure.js +141 -0
  26. package/dist/env.d.ts +10 -0
  27. package/dist/env.js +33 -0
  28. package/dist/errors.d.ts +25 -0
  29. package/dist/errors.js +60 -0
  30. package/dist/exposure.d.ts +145 -0
  31. package/dist/exposure.js +244 -0
  32. package/dist/fn-tool.d.ts +14 -0
  33. package/dist/fn-tool.js +14 -0
  34. package/dist/fs/file-io.d.ts +78 -0
  35. package/dist/fs/file-io.js +239 -0
  36. package/dist/guard/path-guard.d.ts +144 -0
  37. package/dist/guard/path-guard.js +289 -0
  38. package/dist/guard/paths.d.ts +35 -0
  39. package/dist/guard/paths.js +100 -0
  40. package/dist/http/errors.d.ts +24 -0
  41. package/dist/http/errors.js +64 -0
  42. package/dist/http/headers.d.ts +19 -0
  43. package/dist/http/headers.js +62 -0
  44. package/dist/http/redirects.d.ts +31 -0
  45. package/dist/http/redirects.js +76 -0
  46. package/dist/http/ssrf.d.ts +105 -0
  47. package/dist/http/ssrf.js +272 -0
  48. package/dist/http/transport.d.ts +50 -0
  49. package/dist/http/transport.js +130 -0
  50. package/dist/index.d.ts +114 -0
  51. package/dist/index.js +129 -0
  52. package/dist/memory/log.d.ts +75 -0
  53. package/dist/memory/log.js +157 -0
  54. package/dist/memory/store.d.ts +47 -0
  55. package/dist/memory/store.js +61 -0
  56. package/dist/platform/exec.d.ts +79 -0
  57. package/dist/platform/exec.js +218 -0
  58. package/dist/platform/index.d.ts +12 -0
  59. package/dist/platform/index.js +12 -0
  60. package/dist/platform/paths.d.ts +51 -0
  61. package/dist/platform/paths.js +60 -0
  62. package/dist/platform/quote.d.ts +72 -0
  63. package/dist/platform/quote.js +102 -0
  64. package/dist/plugin.d.ts +96 -0
  65. package/dist/plugin.js +101 -0
  66. package/dist/process/buffers.d.ts +33 -0
  67. package/dist/process/buffers.js +86 -0
  68. package/dist/process/registry.d.ts +98 -0
  69. package/dist/process/registry.js +282 -0
  70. package/dist/registry.d.ts +52 -0
  71. package/dist/registry.js +161 -0
  72. package/dist/run-code/broker.d.ts +68 -0
  73. package/dist/run-code/broker.js +465 -0
  74. package/dist/run-code/limits.d.ts +69 -0
  75. package/dist/run-code/limits.js +88 -0
  76. package/dist/run-code/lines.d.ts +69 -0
  77. package/dist/run-code/lines.js +199 -0
  78. package/dist/run-code/sdk-ts.d.ts +34 -0
  79. package/dist/run-code/sdk-ts.js +276 -0
  80. package/dist/run-code/sdk.d.ts +39 -0
  81. package/dist/run-code/sdk.js +294 -0
  82. package/dist/sandbox/async.d.ts +10 -0
  83. package/dist/sandbox/async.js +26 -0
  84. package/dist/sandbox/bwrap-argv.d.ts +62 -0
  85. package/dist/sandbox/bwrap-argv.js +113 -0
  86. package/dist/sandbox/bwrap.d.ts +94 -0
  87. package/dist/sandbox/bwrap.js +159 -0
  88. package/dist/sandbox/child.d.ts +38 -0
  89. package/dist/sandbox/child.js +98 -0
  90. package/dist/sandbox/config.d.ts +89 -0
  91. package/dist/sandbox/config.js +149 -0
  92. package/dist/sandbox/fake-sandbox.d.ts +57 -0
  93. package/dist/sandbox/fake-sandbox.js +110 -0
  94. package/dist/sandbox/launch.d.ts +51 -0
  95. package/dist/sandbox/launch.js +134 -0
  96. package/dist/sandbox/limits.d.ts +63 -0
  97. package/dist/sandbox/limits.js +113 -0
  98. package/dist/sandbox/probe.d.ts +46 -0
  99. package/dist/sandbox/probe.js +102 -0
  100. package/dist/sandbox/provider.d.ts +83 -0
  101. package/dist/sandbox/provider.js +126 -0
  102. package/dist/sandbox/rlimit.d.ts +60 -0
  103. package/dist/sandbox/rlimit.js +76 -0
  104. package/dist/sandbox/seccomp.d.ts +48 -0
  105. package/dist/sandbox/seccomp.js +115 -0
  106. package/dist/sandbox/userspace.d.ts +65 -0
  107. package/dist/sandbox/userspace.js +107 -0
  108. package/dist/sandbox/workdir.d.ts +13 -0
  109. package/dist/sandbox/workdir.js +44 -0
  110. package/dist/schema.d.ts +20 -0
  111. package/dist/schema.js +135 -0
  112. package/dist/testing/platform-gates.d.ts +54 -0
  113. package/dist/testing/platform-gates.js +62 -0
  114. package/dist/tool-failure.d.ts +13 -0
  115. package/dist/tool-failure.js +19 -0
  116. package/dist/tools/ask-user.d.ts +32 -0
  117. package/dist/tools/ask-user.js +145 -0
  118. package/dist/tools/browser.d.ts +24 -0
  119. package/dist/tools/browser.js +132 -0
  120. package/dist/tools/http-request.d.ts +31 -0
  121. package/dist/tools/http-request.js +117 -0
  122. package/dist/tools/list-dir.d.ts +9 -0
  123. package/dist/tools/list-dir.js +45 -0
  124. package/dist/tools/load-skill.d.ts +37 -0
  125. package/dist/tools/load-skill.js +76 -0
  126. package/dist/tools/memory.d.ts +46 -0
  127. package/dist/tools/memory.js +131 -0
  128. package/dist/tools/process-control.d.ts +14 -0
  129. package/dist/tools/process-control.js +49 -0
  130. package/dist/tools/read-file.d.ts +11 -0
  131. package/dist/tools/read-file.js +81 -0
  132. package/dist/tools/read-image.d.ts +33 -0
  133. package/dist/tools/read-image.js +144 -0
  134. package/dist/tools/run-code.d.ts +48 -0
  135. package/dist/tools/run-code.js +115 -0
  136. package/dist/tools/run-shell.d.ts +22 -0
  137. package/dist/tools/run-shell.js +81 -0
  138. package/dist/tools/write-file.d.ts +8 -0
  139. package/dist/tools/write-file.js +31 -0
  140. package/package.json +28 -0
@@ -0,0 +1,218 @@
1
+ /**
2
+ * Shell resolution (W885) — WHICH shell runs a `run_shell` / `run_code` command.
3
+ *
4
+ * Product decision (W885): on Windows the priority is
5
+ *
6
+ * gitbash > pwsh > cmd
7
+ *
8
+ * and the choice is **probe-based, never guessed**: every candidate is looked up
9
+ * through the injected `which` / `exists` callbacks (defaults: the filesystem +
10
+ * `PATH`), and when nothing is found the resolver **fails closed** with a
11
+ * structured [ShellNotFoundError] instead of silently picking a shell the host
12
+ * may not have.
13
+ *
14
+ * Linux behaviour is byte-identical to the pre-W885 code: the POSIX answer is
15
+ * the literal `/bin/sh` with argv `["-c", command]` (W883 B8 anchored that
16
+ * shape in `config.ts` / `bwrap-argv.ts`). `$SHELL` is deliberately NOT
17
+ * consulted implicitly — that would change what runs on every Linux host that
18
+ * exports it; an operator pins a shell explicitly with `CELESTEA_SHELL`.
19
+ *
20
+ * Pure and injectable: the platform / env / homedir inputs are arguments (the
21
+ * `celestea-home.ts` pattern), so the win32 branches are unit-testable on
22
+ * Linux.
23
+ */
24
+ import { existsSync } from "node:fs";
25
+ import { envValue, execSuffixes, isWindows, pathApi, pathDelimiter } from "./paths.js";
26
+ /** Env var pinning one explicit shell executable (any platform, fail-closed). */
27
+ export const ENV_SHELL_PIN = "CELESTEA_SHELL";
28
+ /** Structured failure: no usable shell on this host (never a fallback guess). */
29
+ export class ShellNotFoundError extends Error {
30
+ code = "shell_not_found";
31
+ constructor(message) {
32
+ super(message);
33
+ this.name = "ShellNotFoundError";
34
+ }
35
+ }
36
+ /** The argv shape of one shell carrying exactly one command string. */
37
+ export function shellArgv(kind, command) {
38
+ if (kind === "pwsh")
39
+ return ["-NoProfile", "-NonInteractive", "-Command", command];
40
+ if (kind === "cmd")
41
+ return ["/d", "/s", "/c", command];
42
+ // posix + gitbash both speak `sh -c`.
43
+ return ["-c", command];
44
+ }
45
+ /**
46
+ * Resolve the shell that should carry `command` on `platform`.
47
+ *
48
+ * Precedence: an explicit `CELESTEA_SHELL` pin > the platform ladder
49
+ * (POSIX: `/bin/sh`; Windows: gitbash > pwsh > cmd). Nothing found ⇒ throws.
50
+ */
51
+ export function resolveShell(command, input = {}) {
52
+ const chosen = resolveShellKind(input);
53
+ return { ...chosen, argv: shellArgv(chosen.kind, command) };
54
+ }
55
+ /**
56
+ * The shell a command WOULD be given to, without a command (W885: `run_code`
57
+ * needs the kind to quote its interpreter line before it has one).
58
+ */
59
+ export function resolveShellKind(input = {}) {
60
+ const platform = input.platform ?? process.platform;
61
+ const env = input.env ?? process.env;
62
+ const lookup = lookupFor(platform, env, input);
63
+ const pinned = envValue(env, ENV_SHELL_PIN);
64
+ if (pinned !== undefined)
65
+ return pinnedShell(pinned, platform, lookup);
66
+ return isWindows(platform) ? resolveWindows(env, lookup) : resolvePosix();
67
+ }
68
+ /** The default lookups: real filesystem, PATH split per platform delimiter. */
69
+ export function lookupFor(platform, env, input = {}) {
70
+ const exists = input.exists ?? ((path) => existsSync(path));
71
+ return { which: input.which ?? ((bin) => whichInPath(bin, platform, env, exists)), exists };
72
+ }
73
+ function resolvePosix() {
74
+ // Byte-for-byte the pre-W885 answer; `$SHELL` is intentionally ignored (see
75
+ // the module header) so no existing Linux deployment changes behaviour.
76
+ return { kind: "posix", path: "/bin/sh" };
77
+ }
78
+ function resolveWindows(env, lookup) {
79
+ const gitbash = findGitBash(env, lookup);
80
+ if (gitbash !== null)
81
+ return { kind: "gitbash", path: gitbash };
82
+ const pwsh = findPwsh(env, lookup);
83
+ if (pwsh !== null)
84
+ return { kind: "pwsh", path: pwsh };
85
+ const cmd = findCmd(env, lookup);
86
+ if (cmd !== null)
87
+ return { kind: "cmd", path: cmd };
88
+ throw new ShellNotFoundError("no usable shell on this Windows host: looked for bash.exe (Git for Windows: " +
89
+ "%ProgramFiles%\\Git\\bin\\bash.exe, %ProgramFiles(x86)%\\Git\\bin\\bash.exe, " +
90
+ "%LOCALAPPDATA%\\Programs\\Git\\bin\\bash.exe, PATH), pwsh.exe (PATH, " +
91
+ "%ProgramFiles%\\PowerShell\\7\\pwsh.exe) and cmd.exe (%ComSpec%, PATH, %SystemRoot%\\System32). " +
92
+ "Install Git for Windows / PowerShell 7, or pin one with " + ENV_SHELL_PIN + ".");
93
+ }
94
+ /** Git for Windows, in the order the user's priority implies. */
95
+ function findGitBash(env, lookup) {
96
+ const onPath = lookup.which("bash.exe");
97
+ if (onPath !== null)
98
+ return onPath;
99
+ return firstExisting(underRoots(env, GITBASH_ROOTS), lookup.exists);
100
+ }
101
+ function findPwsh(env, lookup) {
102
+ const onPath = lookup.which("pwsh.exe");
103
+ if (onPath !== null)
104
+ return onPath;
105
+ return firstExisting(underRoots(env, PWSH_ROOTS), lookup.exists);
106
+ }
107
+ /**
108
+ * cmd.exe: `%ComSpec%` first (the OS's own answer), then PATH, then the
109
+ * canonical `%SystemRoot%\\System32` location. Never invented beyond that.
110
+ */
111
+ function findCmd(env, lookup) {
112
+ const comspec = envValue(env, "ComSpec");
113
+ if (comspec !== undefined && lookup.exists(comspec))
114
+ return comspec;
115
+ const onPath = lookup.which("cmd.exe");
116
+ if (onPath !== null)
117
+ return onPath;
118
+ return firstExisting(underRoots(env, CMD_ROOTS), lookup.exists);
119
+ }
120
+ /**
121
+ * `[install-root env var, path under it]` pairs, in the order Git for Windows
122
+ * actually uses. Program Files first (the machine-wide installer), then the
123
+ * per-user default — the W883 §2.2 note that a Git installed "for me only"
124
+ * lives under `%LOCALAPPDATA%\Programs`.
125
+ */
126
+ const GITBASH_ROOTS = [
127
+ ["ProgramFiles", ["Git", "bin", "bash.exe"]],
128
+ ["ProgramFiles(x86)", ["Git", "bin", "bash.exe"]],
129
+ ["LOCALAPPDATA", ["Programs", "Git", "bin", "bash.exe"]],
130
+ ];
131
+ /** PowerShell 7's documented install root (pwsh is never under Program Files (x86)). */
132
+ const PWSH_ROOTS = [["ProgramFiles", ["PowerShell", "7", "pwsh.exe"]]];
133
+ /** cmd.exe's canonical location when `%ComSpec%` and PATH are both silent. */
134
+ const CMD_ROOTS = [
135
+ ["SystemRoot", ["System32", "cmd.exe"]],
136
+ ["windir", ["System32", "cmd.exe"]],
137
+ ];
138
+ /** Absolute candidates for `[envVar, segments]` pairs under one install root. */
139
+ function underRoots(env, roots) {
140
+ const api = pathApi("win32");
141
+ const out = [];
142
+ for (const [key, segments] of roots) {
143
+ const root = envValue(env, key);
144
+ if (root !== undefined)
145
+ out.push(api.join(root, ...segments));
146
+ }
147
+ return out;
148
+ }
149
+ function firstExisting(candidates, exists) {
150
+ for (const candidate of candidates)
151
+ if (exists(candidate))
152
+ return candidate;
153
+ return null;
154
+ }
155
+ /**
156
+ * An explicitly pinned shell (`CELESTEA_SHELL`). The kind is derived from the
157
+ * file name so the argv shape matches; an unrecognised name is a structured
158
+ * error rather than a silent guess.
159
+ */
160
+ function pinnedShell(path, platform, lookup) {
161
+ if (!lookup.exists(path))
162
+ throw new ShellNotFoundError(ENV_SHELL_PIN + "='" + path + "' does not exist on this host");
163
+ return { kind: kindOfExecutable(path, platform), path };
164
+ }
165
+ const PINNED_KINDS = new Map([
166
+ ["bash", "gitbash"],
167
+ ["sh", "posix"],
168
+ ["dash", "posix"],
169
+ ["pwsh", "pwsh"],
170
+ ["powershell", "pwsh"],
171
+ ["cmd", "cmd"],
172
+ ]);
173
+ /** `pwsh.exe` -> `pwsh`; the kind a pinned executable's argv shape follows. */
174
+ export function kindOfExecutable(path, platform = process.platform) {
175
+ const base = pathApi(platform).basename(path).toLowerCase().replace(/\.(exe|cmd|bat|com)$/, "");
176
+ const kind = PINNED_KINDS.get(base);
177
+ if (kind === undefined) {
178
+ throw new ShellNotFoundError(ENV_SHELL_PIN + "='" + path + "' is not a recognised shell (expected sh, bash, dash, pwsh, powershell or cmd)");
179
+ }
180
+ // `bash` on a POSIX host is the ordinary POSIX shell; on Windows it is Git Bash.
181
+ return kind === "gitbash" && !isWindows(platform) ? "posix" : kind;
182
+ }
183
+ /**
184
+ * PATH lookup that honours the injected env and the platform's delimiter —
185
+ * `;` plus `PATHEXT` suffixes on Windows, `:` with no suffixes elsewhere. A
186
+ * value that looks like a path is checked directly (the pre-W885 rule).
187
+ */
188
+ export function whichInPath(bin, platform = process.platform, env = {}, exists = existsSync) {
189
+ if (looksLikePath(bin, platform))
190
+ return exists(bin) ? bin : null;
191
+ const api = pathApi(platform);
192
+ for (const dir of searchPath(platform, env)) {
193
+ if (dir === "")
194
+ continue;
195
+ for (const name of candidateNames(bin, platform, env)) {
196
+ const candidate = api.join(dir, name);
197
+ if (exists(candidate))
198
+ return candidate;
199
+ }
200
+ }
201
+ return null;
202
+ }
203
+ /** The PATH entries to search (POSIX keeps the historical /usr/bin:/bin floor). */
204
+ function searchPath(platform, env) {
205
+ const raw = env["PATH"] ?? (isWindows(platform) ? "" : "/usr/bin:/bin");
206
+ return raw.split(pathDelimiter(platform));
207
+ }
208
+ /** Exact name first, then the platform suffixes (Windows only). */
209
+ function candidateNames(bin, platform, env) {
210
+ if (pathApi(platform).extname(bin) !== "")
211
+ return [bin];
212
+ return [bin, ...execSuffixes(platform, env).map((suffix) => bin + suffix)];
213
+ }
214
+ function looksLikePath(bin, platform) {
215
+ if (isWindows(platform))
216
+ return pathApi(platform).isAbsolute(bin) || bin.includes("\\");
217
+ return bin.includes("/");
218
+ }
@@ -0,0 +1,12 @@
1
+ /**
2
+ * W885 — the platform seam barrel (`@celestea/tools/platform` internals).
3
+ *
4
+ * Three concerns, deliberately split so each file stays inside the repo's
5
+ * 400-line / 80-line-per-function shape rules:
6
+ * paths.ts platform ids, path API, PATH delimiter, PATHEXT suffixes
7
+ * exec.ts shell resolution (gitbash > pwsh > cmd) + PATH lookup
8
+ * quote.ts per-shell command quoting + the run_code interpreter line
9
+ */
10
+ export * from "./paths.js";
11
+ export * from "./exec.js";
12
+ export * from "./quote.js";
@@ -0,0 +1,12 @@
1
+ /**
2
+ * W885 — the platform seam barrel (`@celestea/tools/platform` internals).
3
+ *
4
+ * Three concerns, deliberately split so each file stays inside the repo's
5
+ * 400-line / 80-line-per-function shape rules:
6
+ * paths.ts platform ids, path API, PATH delimiter, PATHEXT suffixes
7
+ * exec.ts shell resolution (gitbash > pwsh > cmd) + PATH lookup
8
+ * quote.ts per-shell command quoting + the run_code interpreter line
9
+ */
10
+ export * from "./paths.js";
11
+ export * from "./exec.js";
12
+ export * from "./quote.js";
@@ -0,0 +1,51 @@
1
+ /**
2
+ * W885 (Windows slice 1) — the injectable platform seam.
3
+ *
4
+ * `celestea-home.ts` (W880) set the pattern: a platform decision takes its
5
+ * inputs as ARGUMENTS (`platform` / `env` / `homedir`) and stays a pure
6
+ * function, so the win32 branch is unit-testable on a Linux host and the global
7
+ * `process` is read only where a default is unavoidable. This module is that
8
+ * seam for everything the sandbox/guard/broker layers need.
9
+ *
10
+ * It lives in `@celestea/tools` (not core): `probe.ts`, `path-guard.ts`,
11
+ * `config.ts`, `child.ts` and `broker.ts` — the consumers — are all in this
12
+ * package, the dependency direction stays L1 → L0, and core keeps its leaf
13
+ * promise (W883 §6.2 P0-2 listed both locations as acceptable).
14
+ */
15
+ import { posix } from "node:path";
16
+ /** Everything a platform decision may depend on (injectable; never global). */
17
+ export interface PlatformInput {
18
+ /** Defaults to `process.platform` at the call site only. */
19
+ platform?: NodeJS.Platform | string;
20
+ /** Defaults to `process.env` at the call site only. */
21
+ env?: Record<string, string | undefined>;
22
+ /** Defaults to `os.homedir()` at the call site only. */
23
+ homedir?: string;
24
+ }
25
+ /** true for the Windows platform id (the only place the literal lives). */
26
+ export declare function isWindows(platform?: string): boolean;
27
+ /**
28
+ * The path implementation of a platform (same rule as `celestea-home.ts`):
29
+ * deterministic across hosts, so a win32 test never touches the host's path
30
+ * semantics.
31
+ */
32
+ export declare function pathApi(platform?: string): typeof posix;
33
+ /** `PATH` list separator: `;` on Windows, `:` everywhere else. */
34
+ export declare function pathDelimiter(platform?: string): string;
35
+ /**
36
+ * Executable suffixes tried when resolving a bare name on Windows. The real
37
+ * rule is `PATHEXT` (plus an exact-name match first, which command lookup also
38
+ * allows); the two ARE checked independently and must not be collapsed.
39
+ */
40
+ export declare const WINDOWS_EXEC_SUFFIXES: readonly string[];
41
+ /**
42
+ * The suffixes to try for `bin` on `platform`, from the injected env.
43
+ *
44
+ * `PATHEXT` is honoured when present (the operator's real resolution order) and
45
+ * the defaults are used otherwise. The order inside the list is significant:
46
+ * `.exe` before `.cmd` is what makes `bash.exe` win over a `bash.cmd`
47
+ * shim, which matters for the gitbash > pwsh > cmd priority.
48
+ */
49
+ export declare function execSuffixes(platform?: string, env?: Record<string, string | undefined>): readonly string[];
50
+ /** A non-blank, trimmed env value; `undefined` when unset or blank. */
51
+ export declare function envValue(env: Record<string, string | undefined>, name: string): string | undefined;
@@ -0,0 +1,60 @@
1
+ /**
2
+ * W885 (Windows slice 1) — the injectable platform seam.
3
+ *
4
+ * `celestea-home.ts` (W880) set the pattern: a platform decision takes its
5
+ * inputs as ARGUMENTS (`platform` / `env` / `homedir`) and stays a pure
6
+ * function, so the win32 branch is unit-testable on a Linux host and the global
7
+ * `process` is read only where a default is unavoidable. This module is that
8
+ * seam for everything the sandbox/guard/broker layers need.
9
+ *
10
+ * It lives in `@celestea/tools` (not core): `probe.ts`, `path-guard.ts`,
11
+ * `config.ts`, `child.ts` and `broker.ts` — the consumers — are all in this
12
+ * package, the dependency direction stays L1 → L0, and core keeps its leaf
13
+ * promise (W883 §6.2 P0-2 listed both locations as acceptable).
14
+ */
15
+ import { posix, win32 } from "node:path";
16
+ /** true for the Windows platform id (the only place the literal lives). */
17
+ export function isWindows(platform = process.platform) {
18
+ return platform === "win32";
19
+ }
20
+ /**
21
+ * The path implementation of a platform (same rule as `celestea-home.ts`):
22
+ * deterministic across hosts, so a win32 test never touches the host's path
23
+ * semantics.
24
+ */
25
+ export function pathApi(platform = process.platform) {
26
+ return isWindows(platform) ? win32 : posix;
27
+ }
28
+ /** `PATH` list separator: `;` on Windows, `:` everywhere else. */
29
+ export function pathDelimiter(platform = process.platform) {
30
+ return isWindows(platform) ? ";" : ":";
31
+ }
32
+ /**
33
+ * Executable suffixes tried when resolving a bare name on Windows. The real
34
+ * rule is `PATHEXT` (plus an exact-name match first, which command lookup also
35
+ * allows); the two ARE checked independently and must not be collapsed.
36
+ */
37
+ export const WINDOWS_EXEC_SUFFIXES = [".exe", ".cmd", ".bat", ".com"];
38
+ /**
39
+ * The suffixes to try for `bin` on `platform`, from the injected env.
40
+ *
41
+ * `PATHEXT` is honoured when present (the operator's real resolution order) and
42
+ * the defaults are used otherwise. The order inside the list is significant:
43
+ * `.exe` before `.cmd` is what makes `bash.exe` win over a `bash.cmd`
44
+ * shim, which matters for the gitbash > pwsh > cmd priority.
45
+ */
46
+ export function execSuffixes(platform = process.platform, env = {}) {
47
+ if (!isWindows(platform))
48
+ return [];
49
+ const raw = env["PATHEXT"];
50
+ const parsed = (raw ?? "").split(";").map((entry) => entry.trim().toLowerCase()).filter((entry) => entry !== "");
51
+ return parsed.length > 0 ? parsed : WINDOWS_EXEC_SUFFIXES;
52
+ }
53
+ /** A non-blank, trimmed env value; `undefined` when unset or blank. */
54
+ export function envValue(env, name) {
55
+ const raw = env[name];
56
+ if (typeof raw !== "string")
57
+ return undefined;
58
+ const trimmed = raw.trim();
59
+ return trimmed === "" ? undefined : trimmed;
60
+ }
@@ -0,0 +1,72 @@
1
+ /**
2
+ * Command assembly + quoting per shell (W885) — the arg the CHILD sees must be
3
+ * one string, whatever the shell.
4
+ *
5
+ * Three dialects, three rules, and they are not interchangeable:
6
+ *
7
+ * | shell | argv shape | quote rule |
8
+ * |---------|---------------------------------------------------------|------------|
9
+ * | posix | `sh -c <command>` | none — the command IS the argument (byte-identical to pre-W885) |
10
+ * | gitbash | `bash -c <command>` | POSIX single quotes (same as posix) |
11
+ * | pwsh | `pwsh -NoProfile -NonInteractive -Command <command>` | single quotes; an embedded `'` doubles |
12
+ * | cmd | `cmd /d /s /c <command>` | double quotes; an embedded `"` is backslash-escaped, and a trailing backslash run doubles |
13
+ *
14
+ * The POSIX branch is deliberately a **pass-through**: W885's hard requirement
15
+ * is that Linux behaviour does not change by a single byte, so no quoting is
16
+ * introduced where there was none.
17
+ *
18
+ * Honest limitation (this host is Linux): cmd.exe's parser is not a regular
19
+ * language and has no fully general escaping rule. [quoteCmd] implements the
20
+ * documented "wrap in double quotes, escape an embedded quote, double a
21
+ * trailing backslash" convention, which is correct for the lines this repo
22
+ * builds (`<interpreter> <script path>`) and is unit-tested for the
23
+ * meta-characters below — it is NOT a proof of cmd-safety for arbitrary user
24
+ * commands. The same caveat applies to PowerShell's single-quote rule, which is
25
+ * well-defined but only exercised here against strings, not against a real pwsh.
26
+ */
27
+ import type { ShellKind } from "./exec.js";
28
+ /** Quote `value` for the given shell's parser (POSIX + gitbash: verbatim). */
29
+ export declare function quoteForShell(kind: ShellKind, value: string): string;
30
+ /**
31
+ * One shell WORD — an interpreter, a flag, a path — quoted for `kind`, and
32
+ * quoted ONLY when the dialect needs it.
33
+ *
34
+ * The POSIX "only when needed" rule is what keeps the `run_code` line
35
+ * byte-identical to pre-W885: `python3` and `/usr/bin/node` go through
36
+ * verbatim exactly as they used to, while a word that WOULD be mangled (a space,
37
+ * a quote, a glob) now gets the historical single-quote treatment instead of
38
+ * being pasted in raw. pwsh and cmd quote unconditionally — their parsers have
39
+ * no safe set to rely on.
40
+ */
41
+ export declare function quoteWord(kind: ShellKind, word: string): string;
42
+ /**
43
+ * A path that is ALWAYS quoted for `kind`. The `run_code` script path has been
44
+ * single-quoted on POSIX since before W885 (`broker.ts` `shellQuote`), so this
45
+ * keeps those bytes exactly and gives cmd/pwsh their own dialect.
46
+ */
47
+ export declare function quotePath(kind: ShellKind, path: string): string;
48
+ /**
49
+ * POSIX single-quote a word so an absolute path with spaces stays one word
50
+ * (pre-W885 `broker.ts` behaviour, preserved verbatim).
51
+ */
52
+ export declare function shellQuote(value: string): string;
53
+ /**
54
+ * cmd.exe double-quoting: wrap in `"…"`, escape an embedded quote, and double
55
+ * a trailing run of backslashes (command-line quoting consumes them before the
56
+ * closing quote).
57
+ */
58
+ export declare function quoteCmd(value: string): string;
59
+ /** Interpreters a `run_code` program can be executed with. */
60
+ export type RunCodeLanguageName = "typescript" | "python";
61
+ /** Python interpreters tried on POSIX, in order (`python3` keeps its priority). */
62
+ export declare const PYTHON_CANDIDATES_POSIX: readonly string[];
63
+ /** Python interpreters Windows actually ships (the `py` launcher included). */
64
+ export declare const PYTHON_CANDIDATES_WINDOWS: readonly string[];
65
+ /** The interpreter names to try, most preferred first, for `platform`. */
66
+ export declare function pythonCandidates(platform?: string): readonly string[];
67
+ /**
68
+ * The full command line one `run_code` program is executed with, quoted for
69
+ * `kind`. `interpreter` is an already-resolved absolute path or bare name
70
+ * (see `resolveInterpreter` in `run-code/broker.ts`).
71
+ */
72
+ export declare function runCodeCommand(kind: ShellKind, language: RunCodeLanguageName, interpreter: string, scriptPath: string): string;
@@ -0,0 +1,102 @@
1
+ /**
2
+ * Command assembly + quoting per shell (W885) — the arg the CHILD sees must be
3
+ * one string, whatever the shell.
4
+ *
5
+ * Three dialects, three rules, and they are not interchangeable:
6
+ *
7
+ * | shell | argv shape | quote rule |
8
+ * |---------|---------------------------------------------------------|------------|
9
+ * | posix | `sh -c <command>` | none — the command IS the argument (byte-identical to pre-W885) |
10
+ * | gitbash | `bash -c <command>` | POSIX single quotes (same as posix) |
11
+ * | pwsh | `pwsh -NoProfile -NonInteractive -Command <command>` | single quotes; an embedded `'` doubles |
12
+ * | cmd | `cmd /d /s /c <command>` | double quotes; an embedded `"` is backslash-escaped, and a trailing backslash run doubles |
13
+ *
14
+ * The POSIX branch is deliberately a **pass-through**: W885's hard requirement
15
+ * is that Linux behaviour does not change by a single byte, so no quoting is
16
+ * introduced where there was none.
17
+ *
18
+ * Honest limitation (this host is Linux): cmd.exe's parser is not a regular
19
+ * language and has no fully general escaping rule. [quoteCmd] implements the
20
+ * documented "wrap in double quotes, escape an embedded quote, double a
21
+ * trailing backslash" convention, which is correct for the lines this repo
22
+ * builds (`<interpreter> <script path>`) and is unit-tested for the
23
+ * meta-characters below — it is NOT a proof of cmd-safety for arbitrary user
24
+ * commands. The same caveat applies to PowerShell's single-quote rule, which is
25
+ * well-defined but only exercised here against strings, not against a real pwsh.
26
+ */
27
+ import { isWindows } from "./paths.js";
28
+ /** POSIX: a literal single quote inside single quotes. */
29
+ const POSIX_QUOTE = String.raw `'\''`;
30
+ /** cmd.exe: a literal double quote inside a double-quoted argument. */
31
+ const CMD_QUOTE = String.raw `\"`;
32
+ /** Quote `value` for the given shell's parser (POSIX + gitbash: verbatim). */
33
+ export function quoteForShell(kind, value) {
34
+ if (kind === "cmd")
35
+ return quoteCmd(value);
36
+ if (kind === "pwsh")
37
+ return `'${value.replace(/'/g, "''")}'`;
38
+ return value;
39
+ }
40
+ /** Characters a POSIX shell passes through unchanged (no quoting needed). */
41
+ const SAFE_WORD = /^[A-Za-z0-9_@%+=:,./-]+$/;
42
+ /**
43
+ * One shell WORD — an interpreter, a flag, a path — quoted for `kind`, and
44
+ * quoted ONLY when the dialect needs it.
45
+ *
46
+ * The POSIX "only when needed" rule is what keeps the `run_code` line
47
+ * byte-identical to pre-W885: `python3` and `/usr/bin/node` go through
48
+ * verbatim exactly as they used to, while a word that WOULD be mangled (a space,
49
+ * a quote, a glob) now gets the historical single-quote treatment instead of
50
+ * being pasted in raw. pwsh and cmd quote unconditionally — their parsers have
51
+ * no safe set to rely on.
52
+ */
53
+ export function quoteWord(kind, word) {
54
+ if (kind === "cmd" || kind === "pwsh")
55
+ return quoteForShell(kind, word);
56
+ return SAFE_WORD.test(word) ? word : shellQuote(word);
57
+ }
58
+ /**
59
+ * A path that is ALWAYS quoted for `kind`. The `run_code` script path has been
60
+ * single-quoted on POSIX since before W885 (`broker.ts` `shellQuote`), so this
61
+ * keeps those bytes exactly and gives cmd/pwsh their own dialect.
62
+ */
63
+ export function quotePath(kind, path) {
64
+ if (kind === "cmd" || kind === "pwsh")
65
+ return quoteForShell(kind, path);
66
+ return shellQuote(path);
67
+ }
68
+ /**
69
+ * POSIX single-quote a word so an absolute path with spaces stays one word
70
+ * (pre-W885 `broker.ts` behaviour, preserved verbatim).
71
+ */
72
+ export function shellQuote(value) {
73
+ return `'${value.replace(/'/g, POSIX_QUOTE)}'`;
74
+ }
75
+ /**
76
+ * cmd.exe double-quoting: wrap in `"…"`, escape an embedded quote, and double
77
+ * a trailing run of backslashes (command-line quoting consumes them before the
78
+ * closing quote).
79
+ */
80
+ export function quoteCmd(value) {
81
+ const escaped = value.replace(/"/g, CMD_QUOTE);
82
+ const trailing = /\\+$/.exec(escaped);
83
+ const padded = trailing === null ? escaped : escaped + trailing[0];
84
+ return `"${padded}"`;
85
+ }
86
+ /** Python interpreters tried on POSIX, in order (`python3` keeps its priority). */
87
+ export const PYTHON_CANDIDATES_POSIX = ["python3", "python"];
88
+ /** Python interpreters Windows actually ships (the `py` launcher included). */
89
+ export const PYTHON_CANDIDATES_WINDOWS = ["python.exe", "python3.exe", "py.exe"];
90
+ /** The interpreter names to try, most preferred first, for `platform`. */
91
+ export function pythonCandidates(platform = process.platform) {
92
+ return isWindows(platform) ? PYTHON_CANDIDATES_WINDOWS : PYTHON_CANDIDATES_POSIX;
93
+ }
94
+ /**
95
+ * The full command line one `run_code` program is executed with, quoted for
96
+ * `kind`. `interpreter` is an already-resolved absolute path or bare name
97
+ * (see `resolveInterpreter` in `run-code/broker.ts`).
98
+ */
99
+ export function runCodeCommand(kind, language, interpreter, scriptPath) {
100
+ const flags = language === "python" ? " -uB" : "";
101
+ return `${quoteWord(kind, interpreter)}${flags} ${quotePath(kind, scriptPath)}`;
102
+ }
@@ -0,0 +1,96 @@
1
+ /**
2
+ * The tools plugin — `mount(ctx)` is the only installation path (nothing
3
+ * self-registers at runtime), and it publishes exactly three services:
4
+ *
5
+ * - `ToolRegistryService` → the assembled registry (schema → guard → execute);
6
+ * - `SandboxService` → the execution boundary `run_shell` orchestrates;
7
+ * - `ProcessRegistryService` → the session-scoped background process registry.
8
+ *
9
+ * The guard chain is mounted here because **order is security semantics**: the
10
+ * path whitelist must run before any tool execution, and `CELESTEA_TOOL_GUARD=0`
11
+ * is the only (explicit, documented) way to skip it.
12
+ *
13
+ * `run_code` (W255) is mounted here too, not in `builtinTools`: it needs a
14
+ * late-bound handle on the very registry it will dispatch sub-calls through,
15
+ * which only the assembly can bind — exactly like the runtime compose
16
+ * (`crates/runtime/src/tools.rs`). Pass `runCode: false` to leave it out.
17
+ */
18
+ import { type Plugin, type Sandbox, type Tool, type ToolGuard, type UserQuestionService } from "@celestea/core";
19
+ import type { AttachmentStore } from "./attachments/store.js";
20
+ import { type PathGuardGrants } from "./guard/path-guard.js";
21
+ import { type SsrfGrantView } from "./http/ssrf.js";
22
+ import type { HttpRequestToolOptions } from "./tools/http-request.js";
23
+ import { ProcessRegistry } from "./process/registry.js";
24
+ import { ToolRegistryImpl } from "./registry.js";
25
+ import type { RunCodeEventSink } from "./run-code/broker.js";
26
+ import type { RunCodeConfig } from "./run-code/limits.js";
27
+ import { type SessionFsScope } from "./sandbox/config.js";
28
+ import { type SandboxGrantView } from "./sandbox/provider.js";
29
+ import { RegistryHandle } from "./tools/run-code.js";
30
+ export declare const TOOLS_PLUGIN_NAME = "celestea.tools";
31
+ /** `run_code` wiring (W255): broker limits + the optional sub-call event sink. */
32
+ export interface RunCodeMount {
33
+ /** Broker limits; default = `runCodeConfigFromEnv()` (CELAESTEA_RUN_CODE_TIMEOUT_MS). */
34
+ config?: RunCodeConfig;
35
+ /** Session-log sink for nested sub-call rows (see `RunCodeEventSink`). */
36
+ events?: RunCodeEventSink;
37
+ }
38
+ /**
39
+ * Per-session grants (W516) reaching the assembly point. All three views are
40
+ * widen-only; `undefined` (no `grants.json`) reproduces the env-derived posture
41
+ * byte for byte.
42
+ */
43
+ export interface ToolAssemblyGrants extends PathGuardGrants, SandboxGrantView, SsrfGrantView {
44
+ }
45
+ export interface ToolsPluginOptions {
46
+ /** Tool set; default: the six builtins sharing [processes] + [sandbox]. */
47
+ tools?: readonly Tool[];
48
+ sandbox?: Sandbox;
49
+ processes?: ProcessRegistry;
50
+ /** Guard chain override: `null` disables guarding, `undefined` = env default. */
51
+ guard?: ToolGuard | null;
52
+ env?: NodeJS.ProcessEnv;
53
+ /** Session grants (W516): read from the session's `grants.json` by the host. */
54
+ grants?: ToolAssemblyGrants;
55
+ /**
56
+ * W768: the composing SESSION's own workspace (cwd + containment root for the
57
+ * default sandbox, writable root for the path guard). Omitted/null = the
58
+ * process-wide env posture, which is what a detached or legacy session keeps.
59
+ */
60
+ scope?: SessionFsScope | null;
61
+ /**
62
+ * `run_code` mount: default = mounted; `false` = not registered. The tool is
63
+ * registered *before* its registry handle is bound, so sub-calls ride this
64
+ * assembly's exact pipeline (runtime compose parity).
65
+ */
66
+ runCode?: RunCodeMount | false;
67
+ /**
68
+ * W783: the host's user-question service. Supplied = the default tool set also
69
+ * carries `ask_user_question`; absent = it does not (an embedding with no human
70
+ * answerer must not offer a tool that could only ever hang).
71
+ */
72
+ questions?: UserQuestionService | null;
73
+ /**
74
+ * W804: the session's attachment store. Supplied = the default tool set also
75
+ * carries `read_image`; absent = it does not (no store, no image tool).
76
+ */
77
+ attachments?: AttachmentStore | null;
78
+ /** W804: false ONLY when the model explicitly excludes image input (section 6.6). */
79
+ imageInputAllowed?: boolean;
80
+ /** W804: the target model id, for the read_image refusal text. */
81
+ model?: string;
82
+ }
83
+ /** The wired handles a compose root keeps after mounting the plugin. */
84
+ export interface ToolAssembly {
85
+ registry: ToolRegistryImpl;
86
+ sandbox: Sandbox;
87
+ processes: ProcessRegistry;
88
+ guardMounted: boolean;
89
+ /** Late-bound handle of the mounted `run_code` (`null` when disabled). */
90
+ runCode: RegistryHandle | null;
91
+ }
92
+ /** Build the tool assembly without mounting it (compose roots / tests). */
93
+ export declare function assembleTools(options?: ToolsPluginOptions): ToolAssembly;
94
+ /** The `http_request` options of this assembly (grants merged into allow). */
95
+ export declare function httpOptions(env: NodeJS.ProcessEnv, grants: SsrfGrantView): HttpRequestToolOptions;
96
+ export declare function toolsPlugin(options?: ToolsPluginOptions): Plugin;