@illuminis/comprism 0.1.2 → 0.1.4

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 (131) hide show
  1. package/README.md +98 -36
  2. package/out/agent/command.d.ts +75 -4
  3. package/out/agent/command.js +220 -25
  4. package/out/agent/render.d.ts +17 -2
  5. package/out/agent/render.js +157 -14
  6. package/out/agent/session.d.ts +141 -2
  7. package/out/agent/session.js +735 -149
  8. package/out/commands/agents.d.ts +2 -0
  9. package/out/commands/agents.js +79 -0
  10. package/out/commands/ask.d.ts +1 -1
  11. package/out/commands/ask.js +78 -11
  12. package/out/commands/commands-thin.js +79 -17
  13. package/out/commands/config.d.ts +1 -0
  14. package/out/commands/config.js +138 -0
  15. package/out/commands/cost.d.ts +1 -0
  16. package/out/commands/cost.js +167 -0
  17. package/out/commands/hooks.d.ts +1 -0
  18. package/out/commands/hooks.js +83 -0
  19. package/out/commands/install.d.ts +44 -1
  20. package/out/commands/install.js +198 -4
  21. package/out/commands/instructions.d.ts +1 -0
  22. package/out/commands/instructions.js +113 -0
  23. package/out/commands/integrations.d.ts +3 -0
  24. package/out/commands/integrations.js +215 -0
  25. package/out/commands/jobs.d.ts +5 -0
  26. package/out/commands/jobs.js +157 -0
  27. package/out/commands/login.js +188 -36
  28. package/out/commands/memory.d.ts +3 -0
  29. package/out/commands/memory.js +113 -0
  30. package/out/commands/permissions.d.ts +1 -0
  31. package/out/commands/permissions.js +94 -0
  32. package/out/commands/plugins.d.ts +4 -0
  33. package/out/commands/plugins.js +192 -0
  34. package/out/commands/privacy.d.ts +1 -0
  35. package/out/commands/privacy.js +57 -0
  36. package/out/commands/providerKey.d.ts +32 -0
  37. package/out/commands/providerKey.js +108 -0
  38. package/out/commands/repl.d.ts +8 -1
  39. package/out/commands/repl.js +1207 -118
  40. package/out/commands/report.d.ts +39 -0
  41. package/out/commands/report.js +115 -0
  42. package/out/commands/review.d.ts +5 -0
  43. package/out/commands/review.js +223 -0
  44. package/out/commands/sessions.d.ts +23 -0
  45. package/out/commands/sessions.js +115 -0
  46. package/out/commands/settings.d.ts +3 -1
  47. package/out/commands/settings.js +18 -16
  48. package/out/commands/skills.d.ts +21 -0
  49. package/out/commands/skills.js +207 -0
  50. package/out/commands/unattended.d.ts +7 -0
  51. package/out/commands/unattended.js +351 -0
  52. package/out/commands/update.d.ts +1 -0
  53. package/out/commands/update.js +123 -0
  54. package/out/commands/worktrees.d.ts +5 -0
  55. package/out/commands/worktrees.js +186 -0
  56. package/out/executor/browser.d.ts +14 -0
  57. package/out/executor/browser.js +270 -0
  58. package/out/executor/diagnostics.d.ts +2 -0
  59. package/out/executor/diagnostics.js +181 -0
  60. package/out/executor/files.js +270 -40
  61. package/out/executor/git.js +42 -29
  62. package/out/executor/hooks.d.ts +42 -58
  63. package/out/executor/hooks.js +89 -182
  64. package/out/executor/index.d.ts +21 -5
  65. package/out/executor/index.js +160 -14
  66. package/out/executor/paths.d.ts +6 -1
  67. package/out/executor/paths.js +34 -6
  68. package/out/executor/sandbox.d.ts +40 -0
  69. package/out/executor/sandbox.js +299 -0
  70. package/out/executor/shell.d.ts +49 -4
  71. package/out/executor/shell.js +302 -56
  72. package/out/executor/toolservers.d.ts +20 -0
  73. package/out/executor/toolservers.js +189 -0
  74. package/out/executor/worktree.d.ts +9 -0
  75. package/out/executor/worktree.js +119 -0
  76. package/out/graph/read-python.js +2 -1
  77. package/out/lib/attach.d.ts +56 -12
  78. package/out/lib/attach.js +230 -63
  79. package/out/lib/clipboard.d.ts +23 -0
  80. package/out/lib/clipboard.js +182 -0
  81. package/out/lib/commandlist.d.ts +20 -0
  82. package/out/lib/commandlist.js +58 -0
  83. package/out/lib/decision.d.ts +22 -0
  84. package/out/lib/decision.js +50 -0
  85. package/out/lib/fingerprint.d.ts +25 -0
  86. package/out/lib/fingerprint.js +58 -0
  87. package/out/lib/gateway.d.ts +186 -2
  88. package/out/lib/gateway.js +59 -4
  89. package/out/lib/history.d.ts +24 -0
  90. package/out/lib/history.js +137 -0
  91. package/out/lib/ide.d.ts +19 -0
  92. package/out/lib/ide.js +131 -0
  93. package/out/lib/keyboard.d.ts +95 -0
  94. package/out/lib/keyboard.js +383 -0
  95. package/out/lib/machine.d.ts +21 -0
  96. package/out/lib/machine.js +91 -0
  97. package/out/lib/notify.d.ts +4 -0
  98. package/out/lib/notify.js +52 -0
  99. package/out/lib/output.d.ts +48 -0
  100. package/out/lib/output.js +108 -0
  101. package/out/lib/project-ops.d.ts +19 -0
  102. package/out/lib/project-ops.js +146 -0
  103. package/out/lib/project.d.ts +28 -0
  104. package/out/lib/project.js +114 -0
  105. package/out/lib/prompt.js +15 -2
  106. package/out/lib/queue.d.ts +13 -0
  107. package/out/lib/queue.js +116 -0
  108. package/out/lib/readiness.d.ts +19 -0
  109. package/out/lib/readiness.js +170 -1
  110. package/out/lib/self.d.ts +23 -0
  111. package/out/lib/self.js +124 -0
  112. package/out/lib/sessions.d.ts +19 -0
  113. package/out/lib/sessions.js +221 -0
  114. package/out/lib/stdin.d.ts +32 -0
  115. package/out/lib/stdin.js +117 -0
  116. package/out/lib/store.d.ts +40 -0
  117. package/out/lib/store.js +138 -0
  118. package/out/lib/sync.d.ts +18 -0
  119. package/out/lib/sync.js +81 -0
  120. package/out/lib/ui.d.ts +2 -4
  121. package/out/lib/ui.js +31 -25
  122. package/out/lib/voice.js +24 -0
  123. package/out/postinstall.js +42 -14
  124. package/out/providers/anthropic.d.ts +22 -0
  125. package/out/providers/anthropic.js +80 -0
  126. package/out/providers/index.d.ts +8 -0
  127. package/out/providers/index.js +83 -0
  128. package/out/providers/openai.d.ts +11 -0
  129. package/out/providers/openai.js +57 -0
  130. package/out/thin.js +609 -21
  131. package/package.json +9 -49
@@ -33,7 +33,7 @@ var __importStar = (this && this.__importStar) || (function () {
33
33
  };
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
- exports.NativeExecutor = exports.NATIVE_CAPABILITIES = exports.guessTestCommand = exports.stopEverything = void 0;
36
+ exports.NativeExecutor = exports.NATIVE_CAPABILITIES = exports.guessTestCommand = exports.stopForeground = exports.stopEverything = void 0;
37
37
  /**
38
38
  * The executor: one implementation of every action, for every native client.
39
39
  *
@@ -50,19 +50,28 @@ exports.NativeExecutor = exports.NATIVE_CAPABILITIES = exports.guessTestCommand
50
50
  * what it cost are all the server's business. This performs and reports.
51
51
  */
52
52
  const fs = __importStar(require("fs"));
53
+ const path = __importStar(require("path"));
53
54
  const files_1 = require("./files");
54
55
  const documents_1 = require("./documents");
55
56
  const hooks = __importStar(require("./hooks"));
57
+ const browser_1 = require("./browser");
56
58
  const notebook_1 = require("./notebook");
59
+ const diagnostics_1 = require("./diagnostics");
60
+ const worktree_1 = require("./worktree");
61
+ const paths_1 = require("./paths");
57
62
  const git_1 = require("./git");
63
+ const toolservers_1 = require("./toolservers");
58
64
  const shell_1 = require("./shell");
59
65
  Object.defineProperty(exports, "guessTestCommand", { enumerable: true, get: function () { return shell_1.guessTestCommand; } });
60
66
  Object.defineProperty(exports, "stopEverything", { enumerable: true, get: function () { return shell_1.stopEverything; } });
67
+ Object.defineProperty(exports, "stopForeground", { enumerable: true, get: function () { return shell_1.stopForeground; } });
61
68
  /** Everything a machine with a shell can do. Sent to the server on hello, and
62
69
  * the catalog offered to the model is narrowed to it, so a client is never
63
70
  * told about an action it cannot perform. */
64
71
  exports.NATIVE_CAPABILITIES = [
65
72
  "read_file", "read_notebook", "read_pdf", "list_dir", "file_map", "glob", "grep",
73
+ // The project's own checker for a changed file (manual 5.11).
74
+ "read_diagnostics",
66
75
  "write_file", "edit_file", "multi_edit", "create_dir", "delete_file",
67
76
  "move_file", "apply_patch",
68
77
  // Bytes, for a document the server built. Never offered to the model; the
@@ -78,7 +87,27 @@ exports.NATIVE_CAPABILITIES = [
78
87
  // on the server and are never offered to a client.
79
88
  "git_push",
80
89
  "todo_write",
90
+ // A helper's own copy of the project (manual 5.26). Never offered to the
91
+ // model: the service opens, reads and removes copies for editing helpers.
92
+ "worktree_open", "worktree_diff", "worktree_close",
93
+ // Tool servers on this machine the person approved (manual 9.10). Never
94
+ // offered to the model: the service lists and calls them by name.
95
+ "tool_server_list", "tool_server_call",
96
+ // The hooks of plugins installed here (manual 9.15), handed over by the
97
+ // service at the start of a job. Never offered to the model.
98
+ "plugin_hooks",
99
+ // This machine runs the hooks the service names, with each action and at
100
+ // the moments of a job (manual 10.9). Never offered to the model.
101
+ "run_hooks",
102
+ // A whole file, byte for byte, for a checkpoint (manual 7.3). Never offered
103
+ // to the model: `read_file` pages a long file and trims its last line break.
104
+ "read_raw",
81
105
  ];
106
+ /** What a helper may do inside its own copy (manual 5.26). */
107
+ const IN_COPY = new Set([
108
+ "read_file", "list_dir", "file_map", "glob", "grep",
109
+ "write_file", "edit_file", "multi_edit", "apply_patch", "create_dir", "git_status", "git_diff",
110
+ ]);
82
111
  /** How a package is added, per ecosystem.
83
112
  *
84
113
  * Named rather than passed through as a raw command, because "install a
@@ -101,42 +130,133 @@ function installCommand(root, pkg, manager, dev) {
101
130
  default: return null;
102
131
  }
103
132
  }
133
+ /** A file's exact contents and their SHA-256, for a checkpoint (manual 7.3).
134
+ * Refused, never trimmed, when it is too large or not text. */
135
+ function readRaw(root, rel) {
136
+ const crypto = require("crypto");
137
+ const base = fs.realpathSync(root);
138
+ const where = path.resolve(base, rel);
139
+ if (!where.startsWith(base + path.sep))
140
+ return { isError: true, content: `${rel} is outside this project.` };
141
+ let bytes;
142
+ try {
143
+ bytes = fs.readFileSync(where);
144
+ }
145
+ catch {
146
+ return { isError: true, content: `${rel} does not exist.`, facts: { missing: 1 } };
147
+ }
148
+ if (bytes.length > 400_000)
149
+ return { isError: true, content: `${rel} is too large to copy.`, facts: { too_large: 1 } };
150
+ let text;
151
+ try {
152
+ text = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
153
+ }
154
+ catch {
155
+ return { isError: true, content: `${rel} is not text, so it cannot be copied.`, facts: { binary: 1 } };
156
+ }
157
+ return { isError: false, content: text, summary: `${bytes.length} bytes`,
158
+ facts: { sha256: crypto.createHash("sha256").update(bytes).digest("hex") } };
159
+ }
104
160
  class NativeExecutor {
105
- capabilities = exports.NATIVE_CAPABILITIES;
161
+ /** What this machine can do. The browser actions only where a browser is
162
+ * installed (manual 10.16); the service offers them only when switched on. */
163
+ capabilities = (0, browser_1.findBrowser)() ? [...exports.NATIVE_CAPABILITIES, ...browser_1.BROWSER_ACTIONS] : exports.NATIVE_CAPABILITIES;
164
+ /** The job's browser, started on first use (10.16). */
165
+ browser;
166
+ /** Close what this job opened on the machine: its browser. */
167
+ close() {
168
+ this.browser?.close();
169
+ this.browser = undefined;
170
+ }
106
171
  root;
107
172
  onOutput;
108
173
  /** The test command, once it has been worked out or supplied. Remembered so a
109
174
  * job that had to be told does not have to be told again. */
110
175
  testCommand;
111
- /** What this project asks to happen around the agent's actions, read once. */
112
- hooks;
113
176
  /** Whether the last test run passed. The grading signal the whole product
114
177
  * rests on, kept so a session can report it when the job ends. */
115
178
  lastTestsPassed;
179
+ /** The operating system's sandbox, set when the service turns it on for a
180
+ * job (manual 4.12). Absent, commands run with the person's own access. */
181
+ sandbox;
116
182
  constructor(opts) {
117
183
  this.root = fs.realpathSync(opts.root);
184
+ this.toolServers = new toolservers_1.ToolServers(this.root);
185
+ (0, paths_1.allowFolders)(this.root, opts.added ?? []);
118
186
  this.onOutput = opts.onOutput;
119
187
  }
120
- async execute(name, a) {
121
- // What this project asks to happen around the agent's actions. Read once
122
- // and kept: reading the file on every action would mean a project could
123
- // change its own rules mid job, which is not a property anybody wants.
124
- if (this.hooks === undefined)
125
- this.hooks = hooks.hooksFor(this.root);
188
+ /** One executor per helper copy, rooted there and nowhere else. */
189
+ inCopies = new Map();
190
+ /** Tool servers this job started (manual 9.10), stopped when it ends. */
191
+ toolServers;
192
+ async execute(name, a, plan) {
193
+ // An action for a helper's copy runs there, rooted in the copy, so it can
194
+ // never touch the person's own folder (manual 5.26).
195
+ if (typeof a._worktree === "string") {
196
+ const id = a._worktree;
197
+ const where = (0, worktree_1.worktreePath)(id);
198
+ if (!where)
199
+ return { isError: true, content: `There is no copy called ${id}.` };
200
+ if (!IN_COPY.has(name))
201
+ return { isError: true, content: `Refused: a helper cannot ${name} in its copy.` };
202
+ let inner = this.inCopies.get(id);
203
+ if (!inner) {
204
+ inner = new NativeExecutor({ root: where });
205
+ this.inCopies.set(id, inner);
206
+ }
207
+ const { _worktree: _dropped, ...rest } = a;
208
+ void _dropped;
209
+ return inner.execute(name, rest, plan);
210
+ }
211
+ // A tool server's list or call is carried, never gated by the project's
212
+ // own hooks: the service already decided whether it may happen.
213
+ if (name === "plugin_hooks") {
214
+ // An older service handing over plugin hooks in bulk. This machine runs
215
+ // the hooks the service names with each action instead, so nothing is
216
+ // kept here and nothing is matched here (manual 10.9).
217
+ const given = Array.isArray(a.hooks) ? a.hooks.length : 0;
218
+ return { isError: false, content: `${given} plugin hooks noted; the service names them per action` };
219
+ }
220
+ if (name === "read_raw") {
221
+ return readRaw(this.root, String(a.path ?? ""));
222
+ }
223
+ if (name === "run_hooks") {
224
+ // A moment of the job: job_start, job_end or waiting (manual 10.9).
225
+ const given = Array.isArray(a.hooks) ? a.hooks : [];
226
+ const done = await hooks.runHooks(this.root, given, {
227
+ when: String(a.when ?? ""), facts: (a.facts ?? {}),
228
+ });
229
+ const lines = hooks.reportLines(done);
230
+ return {
231
+ isError: done.failed.length > 0,
232
+ content: lines.join("\n") || "no hooks ran",
233
+ summary: done.failed.length ? `hook failed: ${done.failed.join("; ")}`
234
+ : `${done.runs.length} ${String(a.when)} hook${done.runs.length === 1 ? "" : "s"} ran`,
235
+ };
236
+ }
237
+ if (name === "tool_server_list") {
238
+ return this.toolServers.list(String(a.server ?? ""), String(a.command ?? ""));
239
+ }
240
+ if (name === "tool_server_call") {
241
+ return this.toolServers.call(String(a.server ?? ""), String(a.command ?? ""), String(a.tool ?? ""), (a.arguments ?? {}));
242
+ }
243
+ // The hooks the service named for this action, run in its order (10.9).
126
244
  // A `before` hook may REFUSE this action, and that is the one place a
127
245
  // project's own script changes what happens. It can only ever refuse: there
128
246
  // is no exit code that approves something the agent was not already allowed
129
247
  // to do, because a permission system inside the repository would live
130
248
  // exactly where an attacker who got that far already is.
131
- const gate = await hooks.before(this.root, name, a, this.hooks);
249
+ const gate = await hooks.runHooks(this.root, plan?.before ?? [], { when: "before", action: name, args: a });
132
250
  if (gate.refusedBy) {
133
- return { isError: true, content: gate.detail ?? "A check in this project refused it." };
251
+ return { isError: true, content: gate.detail ?? "A hook in this project refused it.",
252
+ summary: gate.detail };
134
253
  }
135
254
  const shell = {
136
255
  root: this.root,
137
256
  cwd: typeof a.cwd === "string" ? a.cwd : undefined,
138
257
  timeoutMs: typeof a.timeout_s === "number" ? a.timeout_s * 1000 : undefined,
139
258
  onOutput: this.onOutput,
259
+ sandbox: this.sandbox,
140
260
  };
141
261
  try {
142
262
  const done = await this.perform(name, a, shell);
@@ -144,13 +264,24 @@ class NativeExecutor {
144
264
  // has already happened, and the overwhelming case is a formatter, which is
145
265
  // worth knowing about and not worth losing the work over.
146
266
  if (!done.isError) {
147
- const ran = await hooks.after(this.root, name, a, this.hooks);
267
+ const ran = await hooks.runHooks(this.root, plan?.after ?? [], { when: "after", action: name, args: a });
268
+ const shown = hooks.reportLines(ran);
148
269
  if (ran.failed.length) {
270
+ // Said on the action's own line, so the person sees it, and told to
271
+ // the model as what it is: the project's hook, not the file.
149
272
  return {
150
273
  ...done,
151
- content: `${done.content}\n\n[This project's own checks reported: ${ran.failed.join("; ")}]`,
274
+ summary: `${done.summary ? `${done.summary}; ` : ""}hook: ${ran.failed.join("; ")}`,
275
+ // The service words the action line from these (manual 10.9).
276
+ facts: { ...(done.facts ?? {}), hook_note: ran.failed.join("; ") },
277
+ content: `${done.content}\n\n[CompletionPrism ran this project's after hooks for this `
278
+ + `action, as its settings ask. They reported: ${ran.failed.join("; ")}. This note is `
279
+ + `from the tool, not part of the result.]`,
152
280
  };
153
281
  }
282
+ // Each hook's output, for the full view (manual 10.10).
283
+ if (shown.length)
284
+ return { ...done, content: `${done.content}\n\n${shown.join("\n")}` };
154
285
  }
155
286
  return done;
156
287
  }
@@ -164,8 +295,15 @@ class NativeExecutor {
164
295
  };
165
296
  }
166
297
  }
298
+ async browse(name, a) {
299
+ if (!this.browser)
300
+ this.browser = new browser_1.Browser(this.root);
301
+ return this.browser.perform(name, a);
302
+ }
167
303
  async perform(name, a, shell) {
168
304
  {
305
+ if (name.startsWith("browser_"))
306
+ return await this.browse(name, a);
169
307
  switch (name) {
170
308
  case "run_command":
171
309
  return await (0, shell_1.runCommand)(String(a.command ?? ""), shell);
@@ -204,6 +342,14 @@ class NativeExecutor {
204
342
  return await (0, documents_1.readPdf)(this.root, String(a.path ?? ""), a.pages, shell);
205
343
  case "read_notebook":
206
344
  return (0, notebook_1.readNotebook)(this.root, String(a.path ?? ""), a.include_output !== false);
345
+ case "read_diagnostics":
346
+ return await (0, diagnostics_1.readDiagnostics)(this.root, a.path, a.severity, shell);
347
+ case "worktree_open": return await (0, worktree_1.openWorktree)(String(a.id ?? ""), shell);
348
+ case "worktree_diff": return await (0, worktree_1.diffWorktree)(String(a.id ?? ""), shell);
349
+ case "worktree_close": {
350
+ this.inCopies.delete(String(a.id ?? ""));
351
+ return await (0, worktree_1.closeWorktree)(String(a.id ?? ""), shell);
352
+ }
207
353
  case "git_status": return await (0, git_1.gitStatus)(shell);
208
354
  case "git_diff": return await (0, git_1.gitDiff)({ ...shell, path: a.path, staged: Boolean(a.staged) });
209
355
  case "git_log": return await (0, git_1.gitLog)({ ...shell, path: a.path, limit: a.limit });
@@ -1,7 +1,12 @@
1
1
  export declare class OutsideWorkspace extends Error {
2
2
  readonly attempted: string;
3
- constructor(attempted: string);
3
+ /** `real` is where the path actually leads, when that differs from what was
4
+ * asked for: a link is refused by naming its real location (manual 2.4). */
5
+ constructor(attempted: string, real?: string);
4
6
  }
7
+ export declare function allowFolders(root: string, added: string[]): void;
8
+ /** Every folder a job in `root` may touch, the project first. */
9
+ export declare function allowedFolders(root: string): string[];
5
10
  export declare function isSecret(p: string): boolean;
6
11
  /**
7
12
  * An absolute path inside the workspace, or a refusal.
@@ -34,6 +34,8 @@ var __importStar = (this && this.__importStar) || (function () {
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.OutsideWorkspace = void 0;
37
+ exports.allowFolders = allowFolders;
38
+ exports.allowedFolders = allowedFolders;
37
39
  exports.isSecret = isSecret;
38
40
  exports.resolveInside = resolveInside;
39
41
  /**
@@ -63,14 +65,40 @@ const fs = __importStar(require("fs"));
63
65
  const path = __importStar(require("path"));
64
66
  class OutsideWorkspace extends Error {
65
67
  attempted;
66
- constructor(attempted) {
67
- super(`Refused: ${attempted} is outside this project. The agent works only ` +
68
- `inside the folder that was opened.`);
68
+ /** `real` is where the path actually leads, when that differs from what was
69
+ * asked for: a link is refused by naming its real location (manual 2.4). */
70
+ constructor(attempted, real) {
71
+ super(`Refused: ${attempted} is outside this project` +
72
+ (real && real !== attempted ? ` (it leads to ${real})` : "") +
73
+ `. The agent works only inside the folders that were opened.`);
69
74
  this.attempted = attempted;
70
75
  this.name = "OutsideWorkspace";
71
76
  }
72
77
  }
73
78
  exports.OutsideWorkspace = OutsideWorkspace;
79
+ /**
80
+ * Folders the person added for this job (manual 2.3), keyed by the project's
81
+ * real root. Registered by the executor that owns the job, so every existing
82
+ * `resolveInside(root, ...)` call checks all allowed folders without each one
83
+ * having to carry the list. Only folders the person named reach here.
84
+ */
85
+ const addedFolders = new Map();
86
+ function allowFolders(root, added) {
87
+ const realRoot = fs.realpathSync(root);
88
+ const real = added.map((a) => fs.realpathSync(a)).filter((a) => a !== realRoot);
89
+ if (real.length)
90
+ addedFolders.set(realRoot, [...new Set(real)]);
91
+ else
92
+ addedFolders.delete(realRoot);
93
+ }
94
+ /** Every folder a job in `root` may touch, the project first. */
95
+ function allowedFolders(root) {
96
+ const realRoot = fs.realpathSync(root);
97
+ return [realRoot, ...(addedFolders.get(realRoot) ?? [])];
98
+ }
99
+ function inside(real, folder) {
100
+ return real === folder || real.startsWith(folder.endsWith(path.sep) ? folder : folder + path.sep);
101
+ }
74
102
  /** Files never read, whatever the mode. Mirrors the server's own list. */
75
103
  const SECRET = /^(\.env(\..*)?|.*\.pem|.*\.key|.*\.p12|.*\.pfx|id_rsa.*|id_ed25519.*|.*\.keystore|credentials|\.npmrc|\.netrc|\.git-credentials)$/i;
76
104
  function isSecret(p) {
@@ -116,9 +144,9 @@ function resolveInside(root, rel, mustExist = false) {
116
144
  }
117
145
  // The comparison, with a separator on the end. Without it `/work/project-two`
118
146
  // passes as being inside `/work/project`, because one string starts with the
119
- // other.
120
- if (real !== realRoot && !real.startsWith(realRoot + path.sep)) {
121
- throw new OutsideWorkspace(rel);
147
+ // other. Every allowed folder is checked, and nothing else.
148
+ if (!allowedFolders(realRoot).some((folder) => inside(real, folder))) {
149
+ throw new OutsideWorkspace(rel, real);
122
150
  }
123
151
  // Return the requested path, not the probe: the caller wants to write to the
124
152
  // file it named, and the probe may be an ancestor of it.
@@ -0,0 +1,40 @@
1
+ export type SandboxSystem = "macOS" | "Linux";
2
+ /** Which sandbox this machine offers, or why none. */
3
+ export declare function availability(): {
4
+ system: SandboxSystem | null;
5
+ why: string;
6
+ };
7
+ /** Does this host match an allowed address: exact, or `*.example.com`. */
8
+ export declare function hostAllowed(host: string, allow: string[]): boolean;
9
+ export declare class Sandbox {
10
+ readonly system: SandboxSystem;
11
+ private readonly writable;
12
+ private allow;
13
+ private proxy;
14
+ private port;
15
+ /** Hosts refused since the last `takeBlocked`, for the result to name. */
16
+ private blocked;
17
+ constructor(system: SandboxSystem, root: string, added: string[]);
18
+ /** The allowed internet addresses, as the service sent them. */
19
+ setAllowed(allow: string[]): void;
20
+ /** Where the gatekeeper listens on Linux: a socket file the relay inside
21
+ * the sandbox connects to, and a folder for the relay's ready marks. */
22
+ private socketPath;
23
+ private marks;
24
+ private calls;
25
+ /** Start the gatekeeper. Idempotent. */
26
+ start(): Promise<void>;
27
+ close(): void;
28
+ /** The hosts refused since the last call. */
29
+ takeBlocked(): string[];
30
+ private profile;
31
+ /** The program and arguments that run `command` inside the sandbox, and
32
+ * the variables that send its web traffic to the gatekeeper. */
33
+ wrap(command: string, cwd: string): {
34
+ file: string;
35
+ args: string[];
36
+ env: Record<string, string>;
37
+ };
38
+ /** What was blocked, in the manual's words, or null (manual 4.12). */
39
+ explain(output: string, hosts: string[]): string | null;
40
+ }