seisin 0.6.5 → 0.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.
package/README.md CHANGED
@@ -168,12 +168,14 @@ own fixtures:
168
168
 
169
169
  Others are the sandbox's, and differ between the two built in:
170
170
 
171
- | | `srt` (the default) | `tartaros` |
171
+ | | `tartaros` (the default) | `srt` |
172
172
  |---|---|---|
173
- | a process the agent leaves running | **macOS:** it outlives the run, whether it double-forks, ignores signals under `nohup` or calls `setsid` (measured 2026-10-16). **Linux:** the PID namespace ends with the run (read from its source) | **macOS:** the run's process group is killed; one that calls `setsid` outlives it (measured 2026-10-16). **Linux:** the run's cgroup is killed whole (read from its source) |
174
- | deleting inside the territory | write includes delete; the ask is upstream as [issue #545](https://github.com/anthropics/sandbox-runtime/issues/545) | not measured here |
175
- | `[network] deny_addresses` | enforced | not yet: a role that has it runs under `srt`, and `check` says why |
176
- | what it is | Anthropic's, described by its authors as a research preview | by the author of seisin, alpha |
173
+ | a process the agent leaves running | **macOS:** the run's process group is killed; one that calls `setsid` outlives it (measured 2026-10-16). **Linux:** the run's cgroup is killed whole (read from its source) | **macOS:** it outlives the run, whether it double-forks, ignores signals under `nohup` or calls `setsid` (measured 2026-10-16). **Linux:** the PID namespace ends with the run (read from its source) |
174
+ | deleting inside the territory | not measured here | write includes delete; the ask is upstream as [issue #545](https://github.com/anthropics/sandbox-runtime/issues/545) |
175
+ | `[network] deny_addresses` | not yet: a role that has it runs under `srt` where it is installed, and `check` says why | enforced |
176
+ | `local_binding = true` | refused (it grants ports one at a time): a role that has it runs under `srt` where it is installed | enforced |
177
+ | `local_ports`, a client that skips the proxy (a database driver) | reaches the listed port: the kernel grants it | denied: only a client that honours the proxy gets there |
178
+ | what it is | by the author of seisin, alpha | Anthropic's, described by its authors as a research preview |
177
179
 
178
180
  `seisin check` says which sandbox each role gets before anything runs.
179
181
 
@@ -196,18 +198,22 @@ or more.
196
198
  npm install -g seisin
197
199
  ```
198
200
 
199
- That pulls in [`@anthropic-ai/sandbox-runtime`](https://github.com/anthropics/sandbox-runtime), the default sandbox: Seatbelt on macOS, bubblewrap on Linux. No containers, no VMs, no daemon. Its one other dependency is optional: [tartaros](https://github.com/carlostapiacl/tartaros), prebuilt for macOS and Linux, which can enforce instead, as can a sandbox of your own ([docs/sandboxes.md](https://github.com/carlostapiacl/seisin/blob/main/docs/sandboxes.md)).
201
+ That pulls in [tartaros](https://github.com/carlostapiacl/tartaros), the default sandbox, prebuilt for macOS and Linux: Seatbelt on macOS, Landlock and seccomp on Linux. No containers, no VMs, no daemon, and no other dependency. [`@anthropic-ai/sandbox-runtime`](https://github.com/anthropics/sandbox-runtime) (`srt`) is used where you install it apart, after tartaros: for a role tartaros cannot hold (a blanket `local_binding`, `deny_addresses`, a Linux host without Landlock), or anywhere `[sandbox] provider` names it. A sandbox of your own can enforce too ([docs/sandboxes.md](https://github.com/carlostapiacl/seisin/blob/main/docs/sandboxes.md)).
200
202
 
201
- **On Linux you also need three system packages.** Without them the runtime refuses to start
203
+ ```bash
204
+ npm install -g @anthropic-ai/sandbox-runtime@0.0.81 # optional: srt, for what tartaros cannot hold
205
+ ```
206
+
207
+ **For srt on Linux you also need three system packages.** Without them srt refuses to start
202
208
  rather than run unconfined:
203
209
 
204
210
  ```bash
205
211
  apt install bubblewrap ripgrep socat # or your distro's equivalent
206
212
  ```
207
213
 
208
- **On Ubuntu 24.04 and newer**, unprivileged user namespaces are off by default and
209
- bubblewrap needs one. Every run dies with `bwrap: No permissions to create new
210
- namespace`:
214
+ **On Ubuntu 24.04 and newer**, unprivileged user namespaces are off by default. tartaros
215
+ runs without them (with the limits in its docs/limits.md); bubblewrap, and so srt, needs one.
216
+ Every srt run dies with `bwrap: No permissions to create new namespace`:
211
217
 
212
218
  ```bash
213
219
  sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0
@@ -472,8 +478,8 @@ its own situation. **[The log, `review`, the console, the MCP server and walls
472
478
  explanation — never the boundary
473
479
  ```
474
480
 
475
- `sandbox-runtime` is the default enforcer, not the only one. `[sandbox] provider` picks another
476
- (`tartaros`, or any sandbox declared as a command) per role, and only where it enforces the whole
481
+ tartaros is the default enforcer, not the only one. `[sandbox] provider` picks another
482
+ (`srt`, or any sandbox declared as a command) per role, and only where it enforces the whole
477
483
  policy: see [docs/sandboxes.md](https://github.com/carlostapiacl/seisin/blob/main/docs/sandboxes.md).
478
484
 
479
485
  That split keeps seisin small. Because the kernel is the boundary, seisin does not have to be airtight, only *legible*. A leaky explainer costs you a confusing log line. A leaky enforcer costs you the repo.
@@ -508,7 +514,8 @@ writable: each run gets a temp directory of its own instead.
508
514
 
509
515
  ## Prior art
510
516
 
511
- [`sandbox-runtime`](https://github.com/anthropics/sandbox-runtime) is the enforcement seisin sits on.
517
+ [tartaros](https://github.com/carlostapiacl/tartaros) is the enforcement seisin sits on, and
518
+ [`sandbox-runtime`](https://github.com/anthropics/sandbox-runtime) the one it was built on.
512
519
  OpenShell, hazmat and nono isolate the agent from your machine; containers and microVMs isolate a
513
520
  process from the host. seisin separates roles from each other inside one repository, and names the
514
521
  owner when the kernel denies something. They compose: the agent seisin confines can run inside any
@@ -517,8 +524,8 @@ of them. **[Each one, and why not a container →](https://github.com/carlostapi
517
524
  ## Status
518
525
 
519
526
  CI runs the full suite on macOS and `ubuntu-latest`, Node 18/20/22/24, on every pull request and
520
- push to `main`. On 2026-11-02, on Node 24: macOS **1133 tests, 1124 passing, 9 skipped**;
521
- Linux **1132, 1103 passing, 29 skipped**. The skips are platform tests; the job fails if the 26
527
+ push to `main`. On 2026-11-04, on Node 24: macOS **1135 tests, 1126 passing, 9 skipped**;
528
+ Linux **1134, 1105 passing, 29 skipped**. The skips are platform tests; the job fails if the 26
522
529
  core boundary tests in `test/sandbox.test.js` skip. A release is published only from CI, after
523
530
  that passes, with npm provenance. [Which claim was measured where](https://github.com/carlostapiacl/seisin/blob/main/docs/what-it-has-been-put-through.md#where-each-claim-was-actually-run),
524
531
  and [what running agents behind it cost](https://github.com/carlostapiacl/seisin/blob/main/docs/field-notes.md).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "seisin",
3
- "version": "0.6.5",
3
+ "version": "0.7.1",
4
4
  "description": "A permission layer for AI coding agents: role-scoped folders and keys, enforced by the OS, and when it denies something it tells you whose file it was.",
5
5
  "keywords": [
6
6
  "ai-agents",
@@ -42,10 +42,10 @@
42
42
  "node": ">=18"
43
43
  },
44
44
  "dependencies": {
45
- "@anthropic-ai/sandbox-runtime": "0.0.81"
45
+ "@carlostapiacl/tartaros": "0.1.0-alpha.24"
46
46
  },
47
- "optionalDependencies": {
48
- "@carlostapiacl/tartaros": "0.1.0-alpha.18"
47
+ "devDependencies": {
48
+ "@anthropic-ai/sandbox-runtime": "0.0.81"
49
49
  },
50
50
  "scripts": {
51
51
  "test": "node --test test/*.test.js",
package/src/cli.js CHANGED
@@ -7,9 +7,9 @@
7
7
  * `commands/` and every decision lives in a module beside it, so that a change
8
8
  * to how something is printed cannot change what is allowed.
9
9
  *
10
- * Enforcement is not ours. @anthropic-ai/sandbox-runtime asks the operating
11
- * system — Seatbelt on macOS, bubblewrap on Linux — and the kernel does not
12
- * care how a command was spelled. What seisin adds is the part a kernel can
10
+ * Enforcement is not ours. The sandbox (tartaros by default, or
11
+ * sandbox-runtime) asks the operating system — Seatbelt on macOS, Landlock or
12
+ * bubblewrap on Linux — and the kernel does not care how a command was spelled. What seisin adds is the part a kernel can
13
13
  * never know: which role a path belongs to, and therefore who to ask next.
14
14
  */
15
15
  import { readFileSync } from "node:fs";
@@ -105,7 +105,7 @@ Any command takes --policy <file> (the policy, rooted at the working directory)
105
105
  both outside the repo (docs/runs.md).
106
106
  seisin help limits says what seisin cannot do, in any repo.
107
107
 
108
- Enforcement comes from the sandbox (sandbox-runtime by default, or tartaros), which asks the OS.
108
+ Enforcement comes from the sandbox (tartaros by default, or sandbox-runtime), which asks the OS.
109
109
  seisin decides what to ask for, and says whose file it was when the answer is no.
110
110
  `;
111
111
 
@@ -11,7 +11,7 @@ const say = out;
11
11
  import { measureSystemLog } from "../violations.js";
12
12
  import { join } from "node:path";
13
13
  import { settingsFor } from "../srt-settings.js";
14
- import { weigh } from "../sandboxes/index.js";
14
+ import { weigh, DEFAULT_PROVIDERS } from "../sandboxes/index.js";
15
15
  import { runsRoot } from "../rundir.js";
16
16
  import { channelName } from "../spool.js";
17
17
 
@@ -81,7 +81,7 @@ export function sandboxes(config, only = null, { platform = process.platform, qu
81
81
  const out = quiet ? () => {} : say;
82
82
  const sock = join(runsRoot(), "check", channelName(platform));
83
83
  const rows = [];
84
- out(`\n${C.b}sandboxes${C.off} (${(config.sandbox?.provider ?? ["srt"]).join(" → ")})\n`);
84
+ out(`\n${C.b}sandboxes${C.off} (${(config.sandbox?.provider ?? DEFAULT_PROVIDERS).join(" → ")})\n`);
85
85
  for (const r of Object.values(config.roles)) {
86
86
  if (only && r.name !== only) continue;
87
87
  let settings;
@@ -32,6 +32,7 @@ import { EXPECTED } from "../kinds.js";
32
32
  import { toolchainBins } from "../territory.js";
33
33
  import { writePathsOf, realOrSelf, SOCKET_MAX } from "../grants.js";
34
34
  import { runCanary, CANARY_EXIT } from "../canary.js";
35
+ import { scratchWarnings } from "../inspect/machine.js";
35
36
  import { append, logPath } from "../log.js";
36
37
  import { flagsOf } from "../help.js";
37
38
  import { WILD } from "../paths.js";
@@ -317,6 +318,12 @@ export async function run(config, argv) {
317
318
  `env ${Object.keys(env).length} kept, ${dropped.length} dropped · read ${config.read}` +
318
319
  `${observe ? ` · ${C.yellow}OBSERVING — the repo is writable, the network is NOT${C.off}${C.dim}` : ""}${C.off}\n`
319
320
  );
321
+ // `check` says this, and a run is where it costs: the role writes another role's
322
+ // files and the banner above reads as if it could not.
323
+ if (!observe) {
324
+ const around = scratchWarnings(config).find((w) => w.kind === "scratch");
325
+ if (around) err(`${C.yellow}seisin: ${around.headline}. ${around.detail}${C.off}\n`);
326
+ }
320
327
 
321
328
  // Redaction needs the output to pass through this process, and piping breaks
322
329
  // anything that draws its own screen. So it turns itself off on a terminal
package/src/config.js CHANGED
@@ -526,7 +526,7 @@ const BUILTIN_SANDBOXES = ["srt", "tartaros"];
526
526
  * `[sandbox]`: which sandbox enforces the roles, and any declared by command.
527
527
  *
528
528
  * `provider` is one name or a list in order of preference; absent, it is
529
- * `srt`, as it always was. A name is one seisin ships or one declared under
529
+ * `["tartaros", "srt"]` (DEFAULT_PROVIDERS; `srt` alone before 0.7). A name is one seisin ships or one declared under
530
530
  * `[sandbox.providers.<name>]` — an unknown one is refused here, never read as
531
531
  * the default, because "the config asks for tartaros and the run used srt"
532
532
  * without a word is the silent substitution sandboxes/index.js exists to stop.
@@ -29,15 +29,17 @@ export function injectWarnings(config, roles) {
29
29
  const allowed = r.network ?? config.allowedDomains ?? [];
30
30
 
31
31
  // Which sandbox masks is the sandbox's to say (sandboxes/, `masks`): on macOS
32
- // only tartaros does, so a provider list without it cannot run the role here.
32
+ // only tartaros does without leaving the real value where the agent can read
33
+ // it, so a provider list without it cannot run the role here.
33
34
  const names = sandboxesOf(config).map((s) => s.name);
34
35
  if (process.platform === "darwin" && !names.includes("tartaros"))
35
36
  out.push({
36
37
  kind: "inject-platform",
37
38
  role: r.name,
38
39
  headline: `${r.name}: key_mode = "inject" needs tartaros on macOS — [sandbox] provider is ${names.join(", ")}`,
39
- detail: `On macOS sandbox-runtime does not mask credentials yet (anthropics/sandbox-runtime#593), so ` +
40
- `this role will not run here. Add tartaros: [sandbox] provider = ["tartaros", "srt"]. Or run it on ` +
40
+ detail: `On macOS sandbox-runtime masks credentials, but it holds the real value in its own environment, ` +
41
+ `which the agent can read from inside the sandbox (anthropics/sandbox-runtime#593 takes it out of ` +
42
+ `there). So this role will not run here. Add tartaros: [sandbox] provider = ["tartaros", "srt"]. Or run it on ` +
41
43
  `Linux, or use "env"/"run-file" here.`,
42
44
  });
43
45
  else if (process.platform !== "linux" && process.platform !== "darwin")
package/src/keys.js CHANGED
@@ -30,8 +30,9 @@
30
30
  * policy holds a reviewable reference instead of a path, and every resolution
31
31
  * is logged without its value (`event: "key"`, see commands/run.js) — but it is not "the agent never sees it". That
32
32
  * is `inject`, which costs terminating the role's TLS with a CA of your own and
33
- * needs a sandbox that masks credentials: srt on Linux, tartaros on Linux and
34
- * macOS (see `injectGate` below, and `masks` on each sandbox).
33
+ * needs a sandbox that masks credentials without leaving the real value where
34
+ * the agent can read it: srt on Linux, tartaros on Linux and macOS (see
35
+ * `injectGate` below, and `masks` on each sandbox).
35
36
  *
36
37
  * ## Why the value is never written down
37
38
  *
@@ -526,7 +527,8 @@ export function modeOf(config, role, entry) {
526
527
  * the agent a sentinel and substitutes the real value on egress to the
527
528
  * credential's declared hosts. Which sandbox can do that is the sandbox's to
528
529
  * say (`masks` in sandboxes/): srt on Linux, tartaros on Linux and macOS
529
- * (srt's macOS path is tracked upstream, anthropics/sandbox-runtime#593). A
530
+ * (on macOS srt masks, but holds the real value in an environment the agent can
531
+ * read; anthropics/sandbox-runtime#593 takes it out of there). A
530
532
  * sandbox that does not mask is never handed the role, because the real value
531
533
  * is in the environment it is started with. Here only the platforms where no
532
534
  * sandbox masks are refused outright.
@@ -3,8 +3,9 @@
3
3
  *
4
4
  * seisin decides the policy; a sandbox enforces it. There are three ways in:
5
5
  *
6
- * - `srt` (built in, the default): @anthropic-ai/sandbox-runtime;
7
- * - `tartaros` (built in): reads the settings file itself (`--settings`);
6
+ * - `tartaros` (built in, the default, installed with seisin): reads the
7
+ * settings file itself (`--settings`);
8
+ * - `srt` (built in, installed apart): @anthropic-ai/sandbox-runtime;
8
9
  * - any name declared under `[sandbox.providers.<name>]`: a command that
9
10
  * takes the settings file (command.js, docs/sandboxes.md).
10
11
  *
@@ -23,17 +24,25 @@
23
24
  import { mkdtempSync, writeFileSync, rmSync } from "node:fs";
24
25
  import { join } from "node:path";
25
26
  import { tmpdir } from "node:os";
26
- import { srt } from "./srt.js";
27
+ import { srt, SRT_PACKAGE, SRT_VERSION } from "./srt.js";
27
28
  import { machOf } from "../browsers.js";
28
29
  import { tartaros } from "./tartaros.js";
29
30
  import { commandSandbox } from "./command.js";
30
31
 
31
32
  export const BUILTIN_SANDBOXES = { srt, tartaros };
32
- export const DEFAULT_SANDBOX = "srt";
33
+ export const DEFAULT_SANDBOX = "tartaros";
34
+ /**
35
+ * Without `[sandbox] provider`: tartaros, which installs with seisin, then srt
36
+ * where it is installed apart (it is not a dependency since 0.7). A role
37
+ * tartaros cannot hold (a blanket local binding, a host's missing Landlock)
38
+ * runs under srt if it is there and holds all of it, and otherwise does not
39
+ * run, with what each one lacked.
40
+ */
41
+ export const DEFAULT_PROVIDERS = [DEFAULT_SANDBOX, "srt"];
33
42
 
34
43
  /** The sandboxes the config names, in its order, as provider objects. */
35
44
  export function sandboxesOf(config) {
36
- const names = config.sandbox?.provider ?? [DEFAULT_SANDBOX];
45
+ const names = config.sandbox?.provider ?? DEFAULT_PROVIDERS;
37
46
  return names.map((n) => BUILTIN_SANDBOXES[n] ?? commandSandbox(n, config.sandbox.providers[n].command, config.sandbox.providers[n].check, { tagsDenials: config.sandbox.providers[n].tagsDenials }));
38
47
  }
39
48
 
@@ -91,7 +100,15 @@ export function weigh(config, settings, { platform = process.platform, role = nu
91
100
  const ruled = requests.length && !sandbox.requestRules
92
101
  ? [`requests (${requests.map((q) => q.host).join(", ")}): ${sandbox.name} cannot limit the methods and paths of a host from its settings, tartaros can`]
93
102
  : [];
94
- return { sandbox, bin, unmet: [...(masking ? [masking] : []), ...unmet, ...browser, ...exemption, ...ruled], warnings, missing: bin ? null : sandbox.missing() };
103
+ // `control_files`: a family the policy hands a role that this sandbox
104
+ // protects whatever the settings say (tartaros's trust-handoff set holds
105
+ // `.vscode` at any depth), so the role could not do what it was given.
106
+ const families = role ? (config.roles?.[role]?.controlFiles ?? []) : [];
107
+ const kept = families.filter((f) => (sandbox.keepsControlFiles ?? []).includes(f));
108
+ const control = kept.length
109
+ ? [`control_files (${kept.join(", ")}): ${sandbox.name} protects those files at any depth whatever the policy says, so the role could not edit them`]
110
+ : [];
111
+ return { sandbox, bin, unmet: [...(masking ? [masking] : []), ...unmet, ...browser, ...exemption, ...ruled, ...control], warnings, missing: bin ? null : sandbox.missing() };
95
112
  });
96
113
  const chosen = options.find((o) => o.bin && o.unmet.length === 0) ?? null;
97
114
  return { chosen, options };
@@ -114,7 +131,7 @@ export function chooseSandbox(config, settings, { platform = process.platform, r
114
131
  const { chosen, options } = weigh(config, settings, { platform, role, file });
115
132
  if (chosen) return { sandbox: chosen.sandbox, bin: chosen.bin, options };
116
133
  // The one sandbox there is, missing: said exactly as it always was.
117
- if (options.length === 1 && options[0].missing && options[0].sandbox.name === DEFAULT_SANDBOX)
134
+ if (options.length === 1 && options[0].missing)
118
135
  return { error: new Error(options[0].missing), options };
119
136
  return { error: new Error(refusal(role, options)), options };
120
137
  }
@@ -125,6 +142,6 @@ export function refusal(role, options) {
125
142
  : ` - ${o.sandbox.name} cannot enforce all of it:\n${o.unmet.map((u) => ` ${u}`).join("\n")}`);
126
143
  return `no sandbox in [sandbox] provider can hold ${role ? `role ${role}` : "this role"} as its policy is written, so it was not started:\n` +
127
144
  `${lines.join("\n")}\n` +
128
- ` A sandbox that enforces less than the policy is never used in its place. Add one that can (provider = [..., "srt"]), ` +
129
- `or change the role.`;
145
+ ` A sandbox that enforces less than the policy is never used in its place. Install one that can ` +
146
+ `(srt: npm install -g ${SRT_PACKAGE}@${SRT_VERSION}), name it in [sandbox] provider, or change the role.`;
130
147
  }
@@ -1,10 +1,15 @@
1
1
  /**
2
2
  * `srt`: @anthropic-ai/sandbox-runtime, the sandbox seisin was built on.
3
3
  *
4
- * The default, and the reference every other provider is measured against:
5
- * the settings seisin generates (srt-settings.js, one level up) ARE this runtime's
6
- * settings file, so it enforces all of them by construction and `unmet` is
7
- * always empty. What a run hands it is the file and the command, nothing else.
4
+ * The reference every other provider is measured against: the settings seisin
5
+ * generates (srt-settings.js, one level up) ARE this runtime's settings file, so
6
+ * it enforces all of them by construction and `unmet` is always empty. What a
7
+ * run hands it is the file and the command, nothing else.
8
+ *
9
+ * Not a dependency since 0.7: tartaros is the default and installs with seisin.
10
+ * srt is used where it is installed apart (globally, or in the project), after
11
+ * tartaros or where `[sandbox] provider` names it, and the version it is tested
12
+ * with is the one in seisin's devDependencies.
8
13
  */
9
14
  import { existsSync, readFileSync, realpathSync } from "node:fs";
10
15
  import { join, dirname, delimiter } from "node:path";
@@ -47,6 +52,15 @@ export function resolveSrt({ from = HERE, path = process.env.PATH } = {}) {
47
52
  }
48
53
 
49
54
  const RUNTIME = "@anthropic-ai/sandbox-runtime";
55
+ export const SRT_PACKAGE = RUNTIME;
56
+ /** The srt seisin is tested with (its devDependency), for the install hint. */
57
+ export const SRT_VERSION = (() => {
58
+ try {
59
+ return JSON.parse(readFileSync(join(HERE, "..", "..", "package.json"), "utf8")).devDependencies?.[RUNTIME] ?? "latest";
60
+ } catch {
61
+ return "latest";
62
+ }
63
+ })();
50
64
 
51
65
  /**
52
66
  * The `srt` of the sandbox-runtime that resolves from `from`, or null.
@@ -148,11 +162,17 @@ export function withMachPreload(bin, file, inner) {
148
162
  export const srt = {
149
163
  name: "srt",
150
164
  locate: () => resolveSrt(),
151
- missing: () => `sandbox runtime not found. ${installHint()}`,
165
+ missing: () => `sandbox-runtime (srt) not found: it is not installed with seisin since 0.7. ` +
166
+ `Install it with: npm install -g ${RUNTIME}@${SRT_VERSION}`,
152
167
  unmet: () => [],
153
- // Masked credentials: on Linux; on macOS not yet (anthropics/sandbox-runtime#593).
168
+ // Masked credentials: on Linux. On macOS srt masks too, but it reads the real
169
+ // value from its own environment, and from inside its sandbox the agent can
170
+ // read that environment back (measured with 0.0.81).
171
+ // anthropics/sandbox-runtime#593 takes the value out of there.
154
172
  masks: (platform) => platform === "linux" ? null
155
- : `masked credentials (key_mode = "inject") on ${platform}: sandbox-runtime does not mask them there yet (anthropics/sandbox-runtime#593); tartaros does`,
173
+ : platform === "darwin"
174
+ ? `masked credentials (key_mode = "inject") on darwin: sandbox-runtime masks them, but holds the real value in its own environment, which the agent can read from inside the sandbox (anthropics/sandbox-runtime#593 takes it out of there); tartaros does not keep it there`
175
+ : `masked credentials (key_mode = "inject") on ${platform}: sandbox-runtime does not mask them there; tartaros does`,
156
176
  // `--` before the command, and it is not cosmetic. Without it the runtime's
157
177
  // own argument parser reaches into what the agent was invoked with and eats
158
178
  // anything resembling one of its flags: `claude --settings`, `claude -c`.
@@ -31,11 +31,10 @@ export const TARTAROS_PACKAGE = "@carlostapiacl/tartaros";
31
31
  * the order srt is found in (resolveSrt in sandboxes/srt.js), for the same reason. A tartaros built
32
32
  * once and left on PATH goes on answering after the pinned one has its fixes.
33
33
  *
34
- * The pinned one is an optional dependency: a package per platform with the
35
- * binary in it, and a main package whose binaryPath() finds that one whether
34
+ * The pinned one is a dependency: a main package whose binaryPath() finds the
35
+ * binary of this platform's package (an optional dependency of its own) whether
36
36
  * npm nested it under the main package (a global install, measured) or hoisted
37
- * it. An install that left it out (`--omit=optional`, a platform without a
38
- * binary) falls through to PATH.
37
+ * it. A platform without a binary, or `--omit=optional`, falls through to PATH.
39
38
  */
40
39
  export function resolveTartaros({ from = HERE, path = process.env.PATH } = {}) {
41
40
  const pinned = dependencyTartaros(from);
@@ -116,7 +115,10 @@ export function tartarosSettings(settings, { machRegister = [], handoffExempt =
116
115
  export const tartaros = {
117
116
  name: "tartaros",
118
117
  locate: () => resolveTartaros(),
119
- missing: () => `tartaros not found: neither ${TARTAROS_PACKAGE}, which installs with seisin unless optional dependencies are left out, nor tartaros on PATH.`,
118
+ missing: () => `tartaros not found: neither ${TARTAROS_PACKAGE}, which installs with seisin (it has no binary for ${process.platform}-${process.arch}), nor tartaros on PATH.`,
119
+ // Its trust-handoff set holds the editors' folders (`.vscode`, `.idea`) at any
120
+ // depth, whatever the settings say: a role given them cannot edit them here.
121
+ keepsControlFiles: ["ide"],
120
122
  asksFile: true,
121
123
  // Masked credentials: tartaros says whether it masks them, through explain
122
124
  // (0.1.0-alpha.16 and later do; earlier ones refuse the field).