@inetafrica/open-claudia 3.5.0 → 3.5.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/CHANGELOG.md CHANGED
@@ -1,5 +1,20 @@
1
1
  # Changelog
2
2
 
3
+ ## 3.5.1
4
+
5
+ Hardening after an adversarial review of 3.5.0, before any of it reaches the fleet. Canary first.
6
+
7
+ - **A CLI believes only its own bot about who it acts for.** `whoami` and the staff `tool-gate` answer are asked only of a loopback that the CLI process itself or one of its ancestors is listening on, and never of one started inside a staff turn. An address in `OC_SEND_URL` is no longer enough: in 3.5.0 a staff turn could run its own server that answered "owner", and the keyring and the other owner-only verbs believed it. Linux reads `/proc`; macOS asks `/bin/ps` and `/usr/sbin/lsof`. Anything that cannot be checked is unattributed, which is never the owner.
8
+ - **A staff member's watch check gets only the shell essentials.** It runs with an allowlist (PATH, HOME, user, locale, terminal, temp dirs, the config dir), its run stamp and `OC_GUARDED_RUN=1`. 3.5.0 dropped credentials by name pattern, so ones like `DATABASE_URL` or `MONGO_URI` still reached staff checks.
9
+ - **The owner's watch checks get the bot's environment again,** so an owner watch that reads a credential from env works as it did before 3.5.0.
10
+ - **A run with an incomplete chat origin is not the owner.** An origin that names a person or room but not the transport and room it came from counts as a non-owner. A run with no origin at all is still bot-internal work.
11
+ - **The staff shell screen is wider, and it is not a boundary.** A staff turn's shell command may not name process environments or argument blocks, the keyring, the bot's config dir, per-process `/proc` paths, or credential stores. Correction to 3.5.0: the shell does not reliably refuse credential reads. A staff turn that has a shell can still read what the bot's OS user can read. Phase 2 takes the shell away from staff turns.
12
+ - **The owner's routes outside a chat turn** (terminal, ssh, system cron), where 3.5.0 refuses owner-only work:
13
+ - the keyring: `/keyring set`, `/keyring get` and `/keyring remove` in the owner's chat;
14
+ - tool, pack, lessons, entity and ideas edits, and `open-claudia agent`: ask in the owner's chat;
15
+ - a write or destructive tool run: run it from a chat turn, or re-run it with the one-shot `--approval <id>` from a card the owner approved.
16
+ - **Tests:** the provider parity test names the suite that timed out and allows 180 s per suite. A loaded CI runner overran 90 s on canary-v3.5.0's first build.
17
+
3
18
  ## 3.5.0
4
19
 
5
20
  Security fixes for the permission system: phase 0 of the redesign in the owner's 25 Sep review. The owner's own chat turns work as before. Staff and external turns lose the ways around the checks. First shipped as a canary on two bots.
@@ -18,7 +33,7 @@ Security fixes for the permission system: phase 0 of the redesign in the owner's
18
33
  - entity, pack, lessons and ideas writes;
19
34
  - every keyring verb, including `list`;
20
35
  - `open-claudia agent`.
21
- From a plain terminal, these now need an owner approval token.
36
+ From a plain terminal these are refused; 3.5.1 lists the owner's routes.
22
37
  - **Follow-ups set in a staff turn run at that person's level, in that room.** This covers wakeups, crons and watches. The job's creator is the attested run. Before, it came from fields the run's own shell could forge, with a fallback to the owner. Watch checks run without the bot's secrets, as their creator.
23
38
  - **People notes and tasks are owner-only through the bot's commands.**
24
39
  - **History stays in the caller's room.** For a non-owner, `recent`, `transcript-search` and `transcript-window` return only the caller's room. `recent-fetch`'s channel filter, which matched nothing, is fixed.
@@ -29,7 +44,7 @@ Security fixes for the permission system: phase 0 of the redesign in the owner's
29
44
  - SSH, cloud and kube credentials;
30
45
  - other processes' environments.
31
46
  - Paths are resolved through symlinks.
32
- - The shell refuses the obvious equivalents.
47
+ - The shell screens a few common equivalents. This is not a boundary (corrected in 3.5.1).
33
48
  - A permission hook that cannot run now refuses on a staff turn instead of letting the tool through.
34
49
  - **Destructive is never a standing permission.**
35
50
  - "Always" on a destructive card is refused.
@@ -7,6 +7,7 @@ const fs = require("fs");
7
7
  const path = require("path");
8
8
  const http = require("http");
9
9
  const configDir = require(path.join(__dirname, "..", "config-dir"));
10
+ const botEndpoint = require(path.join(__dirname, "..", "core", "bot-endpoint"));
10
11
 
11
12
  function readLoopbackInfo() {
12
13
  try {
@@ -71,9 +72,19 @@ function requireContext() {
71
72
  return ctx;
72
73
  }
73
74
 
74
- function postJson(kind, body) {
75
+ // `verified: true` is for answers this process acts on (tool-gate): the
76
+ // request only goes to an endpoint served by this process's own bot (see
77
+ // core/bot-endpoint), never to whatever OC_SEND_URL names.
78
+ function postJson(kind, body, { verified = false } = {}) {
75
79
  const ctx = requireContext();
76
- const u = new URL(ctx.sendUrl);
80
+ let u;
81
+ if (verified) {
82
+ const check = botEndpoint.verifyEndpointCached(ctx.sendUrl);
83
+ if (!check.ok) return Promise.reject(new Error(`the bot's address could not be verified: ${check.reason}`));
84
+ u = new URL(check.url);
85
+ } else {
86
+ u = new URL(ctx.sendUrl);
87
+ }
77
88
  const requestedProject = body && body.project ? String(body.project) : ctx.project;
78
89
  const sameProject = !ctx.project || !requestedProject || requestedProject === ctx.project;
79
90
  const payload = JSON.stringify({
@@ -134,14 +145,17 @@ function postJson(kind, body) {
134
145
  // Who this process acts for, as the RUNNING BOT decides it. Needs no chat
135
146
  // context: a process with no live run behind it (stripped env, detached from
136
147
  // a finished turn, started outside the bot) comes back unattributed, and so
137
- // does every failure to ask. Unattributed is never the owner.
148
+ // does every failure to ask. Unattributed is never the owner. The question
149
+ // only goes to an endpoint served by this process's own bot — a server the
150
+ // env points at is not believed (core/bot-endpoint).
138
151
  const UNATTRIBUTED = Object.freeze({ attributed: false, owner: false, guarded: true, kind: "unattributed" });
139
152
 
140
153
  function whoami({ timeoutMs = 5000 } = {}) {
141
154
  const ctx = context();
142
155
  if (!ctx.runId || !ctx.runAttest || !ctx.sendUrl || !ctx.sendToken) return Promise.resolve(UNATTRIBUTED);
143
- let u;
144
- try { u = new URL(ctx.sendUrl); } catch (_) { return Promise.resolve(UNATTRIBUTED); }
156
+ const check = botEndpoint.verifyEndpointCached(ctx.sendUrl);
157
+ if (!check.ok) return Promise.resolve(Object.freeze({ ...UNATTRIBUTED, reason: check.reason }));
158
+ const u = new URL(check.url);
145
159
  const payload = JSON.stringify({ runId: ctx.runId, originRunId: ctx.runId, runAttest: ctx.runAttest });
146
160
  return new Promise((resolve) => {
147
161
  const req = http.request({
package/bin/owner-gate.js CHANGED
@@ -12,7 +12,8 @@ function hasRunAttest(env = process.env) {
12
12
  }
13
13
 
14
14
  function refusal(what, caller) {
15
- const why = !(caller && caller.attributed) ? "this process is not attributed to an owner chat turn"
15
+ const why = !(caller && caller.attributed)
16
+ ? `this process is not attributed to an owner chat turn${caller && caller.reason ? ` (${caller.reason})` : ""}`
16
17
  : caller.name ? `this turn acts for ${caller.name}, not the owner`
17
18
  : "this turn does not act for the owner";
18
19
  return `⛔ Owner only: ${what} — ${why}.`;
package/bin/tool.js CHANGED
@@ -438,7 +438,7 @@ async function run(args) {
438
438
  const caller = await client.whoami();
439
439
  if (!caller.attributed) {
440
440
  if (!approvalId) {
441
- console.error(`⛔ ${t.name} (${gate.tier}) refused — not attributed to a live chat turn; write actions need a chat turn or an owner approval token.`);
441
+ console.error(`⛔ ${t.name} (${gate.tier}) refused — not attributed to a live chat turn; write actions need a chat turn or an owner approval token.${caller.reason ? ` (${caller.reason})` : ""}`);
442
442
  process.exitCode = 3; return;
443
443
  }
444
444
  } else if (!caller.owner && !approvalId) {
@@ -448,7 +448,7 @@ async function run(args) {
448
448
  if (!ctx.sendUrl || !ctx.sendToken || !ctx.channelId || !ctx.adapterId) throw new Error("no chat context to ask the bot from");
449
449
  decision = await client.postJson("tool-gate", {
450
450
  tool: t.name, verb: tools.invokedVerb(toolArgs), tier: gate.tier, command: commandLine,
451
- });
451
+ }, { verified: true });
452
452
  } catch (e) {
453
453
  decision = { decision: "blocked", reason: `the bot could not vet this action (${e.message})` };
454
454
  }
@@ -0,0 +1,194 @@
1
+ // Which loopback endpoint may a CLI believe about WHO it acts for?
2
+ //
3
+ // Owner-only CLI verbs and staff tool writes are decided by asking the bot
4
+ // (whoami, tool-gate). The address to ask comes from this process's env
5
+ // (OC_SEND_URL), and a staff turn's shell can point that at a server of its
6
+ // own that answers "owner" (Phase 0 review, CRITICAL). So an endpoint is
7
+ // believed for those answers only when:
8
+ // 1. the process LISTENING on it is this process or one of its ancestors —
9
+ // the bot spawns every run, so the real bot is always above the CLI,
10
+ // while a server a turn starts is beside it at best; and
11
+ // 2. nothing above that listener was STARTED with OC_GUARDED_RUN=1 in its
12
+ // environment — the listener was not itself started inside a staff turn
13
+ // (such a server could run the CLI as its own child, making itself an
14
+ // ancestor). A process's start-up environment is fixed by whoever spawned
15
+ // it; a turn's shell cannot rewrite its provider's.
16
+ // Linux reads /proc; macOS asks /bin/ps and /usr/sbin/lsof by absolute path
17
+ // (PATH belongs to the caller). Anything that cannot be established is a
18
+ // refusal, and a refused caller is unattributed — never the owner.
19
+ //
20
+ // Residual, closed by Phase 2's tool-only staff turns: a staff shell that
21
+ // detaches a server from its turn and runs the CLI beneath that server is no
22
+ // longer below the guarded provider, so rule 2 cannot see it.
23
+
24
+ const fs = require("fs");
25
+ const { execFileSync } = require("child_process");
26
+
27
+ const MAX_DEPTH = 64;
28
+ const PS = "/bin/ps";
29
+ const LSOF = "/usr/sbin/lsof";
30
+ const EXEC_TIMEOUT_MS = 5000;
31
+
32
+ function parsePort(url) {
33
+ let parsed;
34
+ try { parsed = new URL(String(url)); } catch (_) { return null; }
35
+ if (parsed.protocol !== "http:") return null;
36
+ if (parsed.hostname !== "127.0.0.1" && parsed.hostname !== "localhost") return null;
37
+ const port = Number(parsed.port);
38
+ return Number.isInteger(port) && port > 0 && port < 65536 ? port : null;
39
+ }
40
+
41
+ // ── Linux: /proc ─────────────────────────────────────────────────────────────
42
+ // The loopback binds 127.0.0.1 itself, never a wildcard, and nothing else can
43
+ // bind that exact address and port while it listens (no SO_REUSEPORT), so the
44
+ // only listener that counts is an IPv4 127.0.0.1 socket. /proc/net/tcp shows
45
+ // the address as little-endian hex.
46
+ const LINUX_LOOPBACK_HEX = "0100007F";
47
+
48
+ function linuxSystem(procRoot = "/proc") {
49
+ return {
50
+ parentOf(pid) {
51
+ const stat = fs.readFileSync(`${procRoot}/${pid}/stat`, "utf8");
52
+ // "pid (comm) state ppid ..." — comm may hold spaces and parentheses.
53
+ const fields = stat.slice(stat.lastIndexOf(")") + 2).split(" ");
54
+ const ppid = Number(fields[1]);
55
+ return Number.isInteger(ppid) ? ppid : null;
56
+ },
57
+ // The pid in `pids` that owns the 127.0.0.1:port listening socket, else null.
58
+ listenerAmong(port, pids) {
59
+ const inodes = new Set();
60
+ const text = fs.readFileSync(`${procRoot}/net/tcp`, "utf8");
61
+ for (const line of text.split("\n").slice(1)) {
62
+ const cols = line.trim().split(/\s+/);
63
+ if (cols.length < 10 || cols[3] !== "0A") continue; // 0A = LISTEN
64
+ const [addr, portHex] = cols[1].split(":");
65
+ if (addr === LINUX_LOOPBACK_HEX && parseInt(portHex, 16) === port) inodes.add(cols[9]);
66
+ }
67
+ if (!inodes.size) return null;
68
+ for (const pid of pids) {
69
+ let fds = [];
70
+ try { fds = fs.readdirSync(`${procRoot}/${pid}/fd`); } catch (_) { continue; }
71
+ for (const fd of fds) {
72
+ let link = "";
73
+ try { link = fs.readlinkSync(`${procRoot}/${pid}/fd/${fd}`); } catch (_) { continue; }
74
+ const match = /^socket:\[(\d+)\]$/.exec(link);
75
+ if (match && inodes.has(match[1])) return pid;
76
+ }
77
+ }
78
+ return null;
79
+ },
80
+ startEnvHas(pid, key, value) {
81
+ const raw = fs.readFileSync(`${procRoot}/${pid}/environ`, "utf8");
82
+ return raw.split("\0").includes(`${key}=${value}`);
83
+ },
84
+ };
85
+ }
86
+
87
+ // ── macOS: /bin/ps + /usr/sbin/lsof ─────────────────────────────────────────
88
+ function run(file, args) {
89
+ return execFileSync(file, args, {
90
+ encoding: "utf8",
91
+ stdio: ["ignore", "pipe", "ignore"],
92
+ timeout: EXEC_TIMEOUT_MS,
93
+ maxBuffer: 64 * 1024 * 1024,
94
+ env: { PATH: "/usr/bin:/bin:/usr/sbin:/sbin", LANG: "C", LC_ALL: "C" },
95
+ });
96
+ }
97
+
98
+ function darwinSystem() {
99
+ let parents = null;
100
+ return {
101
+ parentOf(pid) {
102
+ if (!parents) {
103
+ parents = new Map();
104
+ for (const line of run(PS, ["-Ao", "pid=,ppid="]).split("\n")) {
105
+ const [p, pp] = line.trim().split(/\s+/).map(Number);
106
+ if (Number.isInteger(p) && Number.isInteger(pp)) parents.set(p, pp);
107
+ }
108
+ }
109
+ return parents.has(pid) ? parents.get(pid) : null;
110
+ },
111
+ listenerAmong(port, pids) {
112
+ let out = "";
113
+ try { out = run(LSOF, ["-nP", "-a", "-p", pids.join(","), `-iTCP@127.0.0.1:${port}`, "-sTCP:LISTEN", "-Fpn"]); }
114
+ catch (error) {
115
+ // Exit 1 whenever some listed pid matched nothing (or is another
116
+ // user's); what did match is still on stdout.
117
+ if (!error || error.status !== 1) throw error;
118
+ out = String(error.stdout || "");
119
+ }
120
+ const owners = new Set();
121
+ let current = null;
122
+ for (const line of out.split("\n")) {
123
+ if (line.startsWith("p")) current = Number(line.slice(1));
124
+ else if (line === `n127.0.0.1:${port}` && current != null) owners.add(current);
125
+ }
126
+ for (const pid of pids) if (owners.has(pid)) return pid;
127
+ return null;
128
+ },
129
+ // ps -E prints the command line followed by the environment the process
130
+ // started with; the environment is what follows the plain command line.
131
+ startEnvHas(pid, key, value) {
132
+ const withEnv = run(PS, ["-E", "-ww", "-o", "command=", "-p", String(pid)]).replace(/\n+$/, "");
133
+ const bare = run(PS, ["-ww", "-o", "command=", "-p", String(pid)]).replace(/\n+$/, "");
134
+ if (!withEnv.startsWith(bare)) return false;
135
+ return withEnv.slice(bare.length).split(" ").includes(`${key}=${value}`);
136
+ },
137
+ };
138
+ }
139
+
140
+ function currentSystem() {
141
+ if (process.platform === "linux") return linuxSystem();
142
+ if (process.platform === "darwin") return darwinSystem();
143
+ return null;
144
+ }
145
+
146
+ function ancestry(pid, system) {
147
+ const chain = [pid];
148
+ let current = pid;
149
+ while (chain.length < MAX_DEPTH) {
150
+ const parent = system.parentOf(current);
151
+ if (!Number.isInteger(parent) || parent <= 0 || chain.includes(parent)) break;
152
+ chain.push(parent);
153
+ current = parent;
154
+ }
155
+ return chain;
156
+ }
157
+
158
+ function refuse(reason) {
159
+ return { ok: false, reason };
160
+ }
161
+
162
+ // { ok: true, url, port, listenerPid } when `url` may be believed about who
163
+ // this process acts for; { ok: false, reason } otherwise.
164
+ function verifyEndpoint(url, { pid = process.pid, system = currentSystem() } = {}) {
165
+ const port = parsePort(url);
166
+ if (!port) return refuse("the loopback address is not a 127.0.0.1 endpoint");
167
+ if (!system) return refuse(`cannot inspect processes on ${process.platform}`);
168
+ let chain;
169
+ let listener;
170
+ try {
171
+ chain = ancestry(pid, system);
172
+ listener = system.listenerAmong(port, chain);
173
+ } catch (error) {
174
+ return refuse(`cannot read the process tree (${error && error.message ? error.message : error})`);
175
+ }
176
+ if (listener == null) return refuse("the loopback endpoint is not served by this process or an ancestor");
177
+ for (const above of chain.slice(chain.indexOf(listener) + 1)) {
178
+ let guarded = false;
179
+ // Unreadable = another user's process, never a guarded provider (the bot
180
+ // starts providers as its own user).
181
+ try { guarded = system.startEnvHas(above, "OC_GUARDED_RUN", "1") === true; } catch (_) { guarded = false; }
182
+ if (guarded) return refuse("the loopback endpoint was started inside a guarded (non-owner) run");
183
+ }
184
+ return { ok: true, url: `http://127.0.0.1:${port}`, port, listenerPid: listener };
185
+ }
186
+
187
+ const verified = new Map();
188
+ function verifyEndpointCached(url) {
189
+ const key = String(url || "");
190
+ if (!verified.has(key)) verified.set(key, verifyEndpoint(url));
191
+ return verified.get(key);
192
+ }
193
+
194
+ module.exports = { verifyEndpoint, verifyEndpointCached, parsePort, linuxSystem, darwinSystem, ancestry };
package/core/runner.js CHANGED
@@ -1771,11 +1771,13 @@ async function deliverForegroundResult(result, runContext, state = currentState(
1771
1771
  return { ok: true, chunks: chunks.length };
1772
1772
  }
1773
1773
 
1774
- // The principal a run acts for. With no chat origin (bot-internal work) that
1775
- // is the owner. With a chat origin it is the speaker the relationship layer
1776
- // resolves from the inbound envelope — also for a compaction stage, which
1777
- // summarises that speaker's conversation; if the speaker cannot be resolved
1778
- // the run is treated as a non-owner (fail closed).
1774
+ // The principal a run acts for. With no chat origin at all (bot-internal work)
1775
+ // that is the owner. With a chat origin it is the speaker the relationship
1776
+ // layer resolves from the inbound envelope — also for a compaction stage, which
1777
+ // summarises that speaker's conversation; if the speaker cannot be resolved,
1778
+ // or the origin names someone but not the transport and room it came from, the
1779
+ // run is treated as a non-owner (fail closed).
1780
+ const ORIGIN_FIELDS = Object.freeze(["adapterId", "transport", "channelId", "userId", "canonicalUserId"]);
1779
1781
  function runPrincipal(runContext) {
1780
1782
  const origin = runContext.origin || {};
1781
1783
  const channelId = runContext.channelId != null ? String(runContext.channelId) : origin.channelId;
@@ -1788,7 +1790,10 @@ function runPrincipal(runContext) {
1788
1790
  channelId: channelId || null,
1789
1791
  userId: origin.userId || null,
1790
1792
  };
1791
- if (!base.adapterType || !base.channelId) return { ...base, owner: true };
1793
+ if (!base.adapterType || !base.channelId) {
1794
+ const partial = ORIGIN_FIELDS.some((key) => origin[key] != null && origin[key] !== "");
1795
+ return { ...base, owner: !partial };
1796
+ }
1792
1797
  let speaker = null;
1793
1798
  try { speaker = require("./relationship").speakerFor(base.adapterType, base.channelId, base.userId); }
1794
1799
  catch (error) { speaker = null; }
package/core/scheduler.js CHANGED
@@ -74,15 +74,17 @@ function uniqueId(prefix = "occ") {
74
74
  }
75
75
 
76
76
  // ── watch-check environment ────────────────────────────────────────────────
77
- // A watch's `check` runs a shell command. It used to inherit `env:
78
- // process.env` — every secret the bot process holds (provider credentials,
79
- // bot tokens, the loopback token), so a staff member's check could read them
80
- // all. Build the env explicitly instead: keep the harmless shell essentials
81
- // and the Open Claudia config dir (so `open-claudia ...` resolves its config),
82
- // then drop anything that looks like a credential (a broad denylist pattern)
83
- // plus the known-secret names the provider env-isolation layer already
84
- // strips. A short-lived, creator-scoped run is stamped on top (below) so the
85
- // check's reusable tools still attribute to whoever created the watch.
77
+ // A watch's `check` runs a shell command in the bot's process tree. Whose
78
+ // environment it gets depends on who set the watch:
79
+ // • a staff member's check gets an ALLOWLIST — the shell essentials and the
80
+ // Open Claudia config dir, nothing else — plus OC_GUARDED_RUN=1 so hooks
81
+ // and CLIs below it know they run for a non-owner. A denylist was not
82
+ // enough: any credential whose name matched no pattern (DATABASE_URL,
83
+ // MONGO_URI, …) reached a staff check (Phase 0 review, MEDIUM);
84
+ // • the owner's check keeps the bot's environment as it always had, so an
85
+ // owner watch that reads a credential from env keeps working.
86
+ // Either way a short-lived, creator-scoped run is stamped on top (below) so
87
+ // the check's reusable tools attribute to whoever created the watch.
86
88
  const WATCH_CHECK_ENV_KEEP = new Set([
87
89
  "PATH", "HOME", "USER", "LOGNAME", "SHELL",
88
90
  "LANG", "LANGUAGE", "LC_ALL", "LC_CTYPE", "TZ",
@@ -91,31 +93,19 @@ const WATCH_CHECK_ENV_KEEP = new Set([
91
93
  "OPEN_CLAUDIA_CONFIG_DIR",
92
94
  "SystemRoot", "ComSpec", "PATHEXT", "WINDIR",
93
95
  ]);
94
- const WATCH_CHECK_SECRET_PATTERN = /(TOKEN|SECRET|PASSWORD|PASSWD|API[_-]?KEY|APIKEY|OAUTH|CREDENTIAL|PRIVATE[_-]?KEY|ACCESS[_-]?KEY|CLIENT[_-]?SECRET|SESSION[_-]?KEY|BEARER)/i;
95
- let KNOWN_SECRET_ENV_KEYS = null;
96
- function knownSecretEnvKeys() {
97
- if (KNOWN_SECRET_ENV_KEYS) return KNOWN_SECRET_ENV_KEYS;
98
- const set = new Set();
99
- try { for (const key of require("./providers/env").ALWAYS_STRIPPED_KEYS) set.add(key); } catch (_) {}
100
- KNOWN_SECRET_ENV_KEYS = set;
101
- return set;
102
- }
103
- function isSecretCheckEnvKey(key) {
104
- if (WATCH_CHECK_ENV_KEEP.has(key)) return false;
105
- if (knownSecretEnvKeys().has(key)) return true;
106
- return WATCH_CHECK_SECRET_PATTERN.test(key);
107
- }
108
- function buildWatchCheckEnv(source, stamp = {}) {
96
+ function buildWatchCheckEnv(source, stamp = {}, { owner = false } = {}) {
109
97
  const env = {};
110
98
  for (const [key, value] of Object.entries(source || {})) {
111
99
  if (value == null) continue;
112
- if (isSecretCheckEnvKey(key)) continue;
100
+ if (!owner && !WATCH_CHECK_ENV_KEEP.has(key)) continue;
113
101
  env[key] = String(value);
114
102
  }
115
103
  for (const [key, value] of Object.entries(stamp)) {
116
104
  if (value == null || value === "") continue;
117
105
  env[key] = String(value);
118
106
  }
107
+ if (owner) delete env.OC_GUARDED_RUN;
108
+ else env.OC_GUARDED_RUN = "1";
119
109
  return env;
120
110
  }
121
111
 
@@ -248,7 +238,7 @@ function createSchedulerService(dependencies = {}) {
248
238
  }
249
239
  // Poll a watch's `check`: exit 0 → the watched condition is met, wake the
250
240
  // agent; non-zero → still idle, skip silently. Runs in the job's project
251
- // dir with a HARDENED env — the bot's secrets are dropped (buildWatchCheckEnv)
241
+ // dir with its creator's env (buildWatchCheckEnv: an allowlist for staff)
252
242
  // and a short-lived run is registered for the job's creator so reusable
253
243
  // tools attribute correctly — output discarded (exit code is the contract),
254
244
  // hard-capped by WATCH_CHECK_TIMEOUT_MS. Injectable so tests stay hermetic.
@@ -256,13 +246,14 @@ function createSchedulerService(dependencies = {}) {
256
246
  const cwd = (job && projectDescriptor(job).dir) || workspace || process.cwd();
257
247
  const attest = require("./speaker-attest");
258
248
  const checkRunId = uniqueId("watch-check");
249
+ const principal = watchCheckPrincipal(job);
259
250
  let runAttest = "";
260
- try { runAttest = attest.registerRun(checkRunId, watchCheckPrincipal(job)); } catch (_) { runAttest = ""; }
251
+ try { runAttest = attest.registerRun(checkRunId, principal); } catch (_) { runAttest = ""; }
261
252
  let lb = null;
262
253
  try { lb = require("./loopback").info(); } catch (_) { lb = null; }
263
254
  const stamp = { OC_RUN_ID: checkRunId, OC_RUN_ATTEST: runAttest };
264
255
  if (lb && lb.url) { stamp.OC_SEND_URL = lb.url; stamp.OC_SEND_TOKEN = lb.token; }
265
- const env = buildWatchCheckEnv(process.env, stamp);
256
+ const env = buildWatchCheckEnv(process.env, stamp, { owner: principal.owner === true });
266
257
  let settled = false;
267
258
  const finish = (code, timedOut) => {
268
259
  if (settled) return;
@@ -1080,7 +1071,6 @@ module.exports = {
1080
1071
  parseFallbacks,
1081
1072
  terminalRunSucceeded,
1082
1073
  buildWatchCheckEnv,
1083
- isSecretCheckEnvKey,
1084
1074
  watchCheckPrincipal,
1085
1075
  setAdapters,
1086
1076
  initScheduler,
@@ -606,6 +606,19 @@ function mentionsCredentialPath(cmd) {
606
606
  });
607
607
  }
608
608
 
609
+ // Words a guarded turn's command may not contain at all (Phase 0 review,
610
+ // HIGH): the path-anchored rules above only see one spelling of a read, so the
611
+ // names themselves are refused — process environments and argument blocks, the
612
+ // keyring, the bot's own config dir, and the credential stores above as path
613
+ // components — and so is any per-process /proc path, as the read gate already
614
+ // refuses /proc to Read/Grep/Glob. This narrows the shell; it is not a
615
+ // boundary. Phase 2 takes the shell away from staff turns.
616
+ const SECRET_WORD_RE = /\b(?:environ|procargs\d*|keyring)\b/i;
617
+ const PROC_PROCESS_RE = /\/proc\/(?:\d|self\b|thread-self\b|[$*?[{])/;
618
+ const SECRET_COMPONENT_RE = new RegExp(
619
+ `(?:^|[\\s/'"=:(\`])(?:\\.open|${HOME_SECRET_PATHS.map((rel) => rel.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|")})(?![A-Za-z0-9_-])`,
620
+ );
621
+
609
622
  // Pure: null, or the id of the secret-read pattern the command matches.
610
623
  function secretReadRule(command) {
611
624
  const cmd = String(command || "");
@@ -614,6 +627,8 @@ function secretReadRule(command) {
614
627
  if (psShowsEnvironment(cmd)) return "ps-environment";
615
628
  if (KEYCHAIN_RE.test(cmd)) return "os-keychain";
616
629
  if (mentionsCredentialPath(cmd)) return "credential-store";
630
+ if (PROC_PROCESS_RE.test(cmd)) return "proc-process";
631
+ if (SECRET_WORD_RE.test(cmd) || SECRET_COMPONENT_RE.test(cmd)) return "secret-word";
617
632
  return null;
618
633
  }
619
634
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@inetafrica/open-claudia",
3
- "version": "3.5.0",
3
+ "version": "3.5.1",
4
4
  "description": "An always-on, provider-agnostic coding-agent harness for Claude Code and OpenAI Codex via chat",
5
5
  "main": "bot.js",
6
6
  "bin": {
@@ -11,7 +11,7 @@
11
11
  "setup": "node setup.js",
12
12
  "start": "node bot.js",
13
13
  "pretest": "node test-provider-fixture.js && node test-provider-core-prompt.js && node test-turn-observer.js && node test-provider-prompt-parity.js && node test-provider-env-isolation.js && node test-provider-resume-parity.js && node test-provider-bootstrap.js && node test-provider-registry.js && node test-provider-stream-decoder.js && node test-provider-events.js && node test-provider-claude.js && node test-provider-codex.js && node test-provider-runner.js && node test-provider-refusal-recovery.js && node test-provider-run-lifecycle.js && node test-provider-gateway.js && node test-provider-side-chat.js && node test-single-runtime.js && node test-provider-migration-backup.js && node test-provider-state-migration.js && node test-provider-session-history.js && node test-provider-session-commands.js && node test-provider-compaction.js && node test-provider-scheduler.js && node test-credential-capture.js && node test-cursor-removal.js && node test-utility-provider-policy.js && node test-provider-subagent.js && node test-provider-recall.js && node test-provider-pack-review.js && node test-memory-mutation-queue.js && node test-provider-dream.js && node test-provider-enforcer.js && node test-provider-tool-hooks.js && node test-provider-boot-matrix.js && node test-provider-capabilities.js && node test-provider-language.js && node test-upgrade-path.js && node test-provider-coupling-audit.js && node test-provider-parity-e2e.js && node test-kazee-channel-health.js && node test-kazee-media-caption.js && node test-kazee-context-media.js && node test-kazee-typing-join.js && node test-kazee-inbound-dedup.js && node test-typing-phase-push.js && node test-socket-reconnect.js && node test-speech-loopback.js && node test-prompt-budget.js && node test-agency-ledger.js && node test-agency-deliberate.js && node test-guardrail.js && node test-spaces-comment-content.js && node test-boot-health.js && node test-lexical-floor.js && node test-safe-mode.js && node test-logout.js && node test-kazee-call-audio.js && node test-kazee-call-janus.js && node test-kazee-call-webrtc.js && node test-kazee-call-speech.js && node test-kazee-call-manager.js && node test-call-speech.js && node test-kazee-call-session.js",
14
- "test": "OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node -e \"require('./vault'); console.log('OK')\" && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-runner-watchdog-static.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-usage-accounting.js && node test-codex-rollout.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-recall-engine.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-recall-graph.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-recall-discoverer.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-recall-query.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-walker-score.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-score-gate.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-cokept-warming.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-project-transcripts-smoke.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-transcript-room-scope.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-abilities.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-ability-extraction.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-ability-couse.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-ability-transfer.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-ability-tiers.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-ability-merge-guard.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-learning-e2e.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-pack-nesting.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-pack-search.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-extractive-injection.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-journal-dedupe.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-read-signal.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-tools.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-tool-graph.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-tool-guard.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-read-guard.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-tooling-mode.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-tool-manifest.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-recall-evolution.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-approval-async.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-unified-identity.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-queue-routing.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-persona-packs.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-speaker-persona.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-persona-pipeline.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-recall-relationship-gate.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-relationship.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-enforcer.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-mandate-hygiene.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-external-rule-scopes.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-approval-cards.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-identity-prune.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-kazee-message-id.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-kazee-group-gate.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-kazee-group-context.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-people-owner-seed.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-delivery-contract.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-run-lock.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-typing-heartbeat.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-web-sessions.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-telegram-poll-recovery.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-telegram-409-grace.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-telegram-conflict-heal-guard.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-single-instance.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-streaming-split.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-stream-preview-purpose.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-spaces-autoenable.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-say-loopback.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-journal-union.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-subst-score.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-section-budgets.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-dedup-adjudicator.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-prospective.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-recall-regressions.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-cost-report.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-speaker-context.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-approval-mining.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-tool-certification.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-apprenticeship.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-speaker-attest.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-run-attest.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-tool-gate.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-cli-owner-gates.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-verify-contact.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-loopback-owner-gate.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-watch-check-env.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-intro-adoption.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-grants.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-room-provider-settings.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-store-durability.js"
14
+ "test": "OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node -e \"require('./vault'); console.log('OK')\" && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-runner-watchdog-static.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-usage-accounting.js && node test-codex-rollout.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-recall-engine.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-recall-graph.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-recall-discoverer.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-recall-query.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-walker-score.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-score-gate.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-cokept-warming.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-project-transcripts-smoke.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-transcript-room-scope.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-abilities.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-ability-extraction.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-ability-couse.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-ability-transfer.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-ability-tiers.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-ability-merge-guard.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-learning-e2e.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-pack-nesting.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-pack-search.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-extractive-injection.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-journal-dedupe.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-read-signal.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-tools.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-tool-graph.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-tool-guard.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-read-guard.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-tooling-mode.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-tool-manifest.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-recall-evolution.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-approval-async.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-unified-identity.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-queue-routing.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-persona-packs.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-speaker-persona.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-persona-pipeline.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-recall-relationship-gate.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-relationship.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-enforcer.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-mandate-hygiene.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-external-rule-scopes.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-approval-cards.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-identity-prune.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-kazee-message-id.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-kazee-group-gate.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-kazee-group-context.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-people-owner-seed.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-delivery-contract.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-run-lock.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-typing-heartbeat.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-web-sessions.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-telegram-poll-recovery.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-telegram-409-grace.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-telegram-conflict-heal-guard.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-single-instance.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-streaming-split.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-stream-preview-purpose.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-spaces-autoenable.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-say-loopback.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-journal-union.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-subst-score.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-section-budgets.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-dedup-adjudicator.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-prospective.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-recall-regressions.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-cost-report.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-speaker-context.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-approval-mining.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-tool-certification.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-apprenticeship.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-speaker-attest.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-run-attest.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-bot-endpoint.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-tool-gate.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-cli-owner-gates.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-verify-contact.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-loopback-owner-gate.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-watch-check-env.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-intro-adoption.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-grants.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-room-provider-settings.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-store-durability.js"
15
15
  },
16
16
  "files": [
17
17
  "bot.js",
@@ -514,7 +514,12 @@ assert.match(pkg.description, /provider-agnostic coding-agent harness/i);
514
514
  // setter's level; notes/tasks owner-only and search stays in the room; reads
515
515
  // screened and the hook fails closed on staff turns; no standing destructive
516
516
  // or anonymous grants; canary release tags. Shipped first as canary-v3.5.0.
517
- assert.strictEqual(pkg.version, "3.5.0", "release version must remain unchanged without explicit approval");
517
+ // 3.5.1 under the same approval (Sumeet 2026-09-25 14:43 EAT, phase 0 hardening
518
+ // from the adversarial review before any fleet roll): a CLI believes only its
519
+ // own bot (ancestor listener, not started in a staff turn); staff watch checks
520
+ // get an allowlist env and owner checks the bot's env again; a partial chat
521
+ // origin is a non-owner; wider staff shell screen, documented as no boundary.
522
+ assert.strictEqual(pkg.version, "3.5.1", "release version must remain unchanged without explicit approval");
518
523
  for (const keyword of ["claude", "codex", "coding-agent", "provider-agnostic"]) {
519
524
  assert.ok(pkg.keywords.includes(keyword), `package keyword missing: ${keyword}`);
520
525
  }
@@ -459,7 +459,9 @@ function runProviderChild(kind) {
459
459
  function runSupportSuites(provider) {
460
460
  assert.ok(["claude", "codex"].includes(provider));
461
461
  const passed = new Set();
462
- for (const [key, [script, marker, suiteTimeoutMs = 90000]] of Object.entries(SUPPORT_SUITES)) {
462
+ // 180 s per suite by default: on a loaded CI runner a suite that takes 20 s
463
+ // locally overran 90 s (canary-v3.5.0 build job 44463; the retry passed).
464
+ for (const [key, [script, marker, suiteTimeoutMs = 180000]] of Object.entries(SUPPORT_SUITES)) {
463
465
  const root = fs.mkdtempSync(path.join(os.tmpdir(), `open-claudia-parity-suite-${key}-`));
464
466
  try {
465
467
  fs.mkdirSync(path.join(root, "home"));
@@ -471,7 +473,11 @@ function runSupportSuites(provider) {
471
473
  timeout: suiteTimeoutMs,
472
474
  maxBuffer: 8 * 1024 * 1024,
473
475
  });
474
- assert.ifError(result.error);
476
+ if (result.error) {
477
+ // Name the suite: a bare "spawnSync ETIMEDOUT" does not say which one.
478
+ const tail = `${result.stdout || ""}\n${result.stderr || ""}`.trim().split(/\r?\n/).slice(-20).join("\n");
479
+ assert.fail(`${key} (${script}) did not finish: ${result.error.code || result.error.message} after ${suiteTimeoutMs} ms\n${tail}`);
480
+ }
475
481
  assert.strictEqual(result.status, 0, `${script}\n${result.stderr || result.stdout}`);
476
482
  assert.ok(`${result.stdout}\n${result.stderr}`.includes(marker), `${script}: missing ${marker}`);
477
483
  passed.add(key);