@inetafrica/open-claudia 3.4.24 → 3.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -53,4 +53,85 @@ function verify(canonicalUserId, attest) {
53
53
  try { return crypto.timingSafeEqual(a, b); } catch (e) { return false; }
54
54
  }
55
55
 
56
- module.exports = { mint, verify };
56
+ // ── Run attestation ─────────────────────────────────────────────────────────
57
+ // The id attest above says "the runner stamped this canonical id". It is not
58
+ // bound to a run: a copy lifted from another process's environment stays valid
59
+ // for the whole boot. And the CLIs that decided the speaker locally (tool,
60
+ // keyring, entity) read OC_CHANNEL_* from their own env, so a shell that
61
+ // simply unset or rewrote those variables was treated as the owner.
62
+ //
63
+ // A run attestation binds a LIVE run to the principal it acts for, decided in
64
+ // the bot process: the runner registers every agent run it spawns
65
+ // (runId -> principal) and stamps OC_RUN_ID + OC_RUN_ATTEST into the run's
66
+ // env; the loopback answers "who is this?" only for a registered, still-live
67
+ // run whose attest matches. Anything else (no attest, a wrong one, a run that
68
+ // has ended, a detached process that outlived its turn) is UNATTRIBUTED, and
69
+ // unattributed never carries owner powers.
70
+ const RUN_TTL_MS = 12 * 60 * 60 * 1000;
71
+ const runs = new Map();
72
+
73
+ function runSignature(runId) {
74
+ return crypto.createHmac("sha256", secret()).update(`run\0${runId}`).digest("hex");
75
+ }
76
+
77
+ function principalOf(principal = {}) {
78
+ // Bot-internal work with no speaker (compaction, owner jobs) acts for the
79
+ // owner; a run is guarded exactly when it acts for someone who is not.
80
+ const owner = principal.owner === true;
81
+ const text = (value) => (value == null || value === "" ? null : String(value));
82
+ return {
83
+ kind: text(principal.kind) || "turn",
84
+ owner,
85
+ guarded: !owner,
86
+ canonicalUserId: text(principal.canonicalUserId),
87
+ name: text(principal.name),
88
+ adapterId: text(principal.adapterId),
89
+ adapterType: text(principal.adapterType),
90
+ channelId: text(principal.channelId),
91
+ userId: text(principal.userId),
92
+ room: text(principal.room),
93
+ purpose: text(principal.purpose),
94
+ };
95
+ }
96
+
97
+ function pruneRuns(now = Date.now()) {
98
+ for (const [id, record] of runs) {
99
+ if (now - record.registeredAt > RUN_TTL_MS) runs.delete(id);
100
+ }
101
+ }
102
+
103
+ // Returns the attest to stamp as OC_RUN_ATTEST. Re-registering a run (a retry
104
+ // of the same runId) replaces its principal.
105
+ function registerRun(runId, principal = {}) {
106
+ const id = String(runId || "");
107
+ if (!id) return "";
108
+ pruneRuns();
109
+ runs.set(id, { ...principalOf(principal), runId: id, registeredAt: Date.now() });
110
+ return runSignature(id);
111
+ }
112
+
113
+ function endRun(runId) {
114
+ runs.delete(String(runId || ""));
115
+ }
116
+
117
+ // The principal of a live run whose attest matches, or null (fail closed).
118
+ function verifyRun(runId, attest) {
119
+ const id = String(runId || "");
120
+ const sig = String(attest || "");
121
+ if (!id || !sig) return null;
122
+ const record = runs.get(id);
123
+ if (!record) return null;
124
+ if (Date.now() - record.registeredAt > RUN_TTL_MS) { runs.delete(id); return null; }
125
+ const a = Buffer.from(sig, "utf-8");
126
+ const b = Buffer.from(runSignature(id), "utf-8");
127
+ if (a.length !== b.length) return null;
128
+ try { if (!crypto.timingSafeEqual(a, b)) return null; } catch (e) { return null; }
129
+ return { ...record };
130
+ }
131
+
132
+ function liveRuns() {
133
+ pruneRuns();
134
+ return Array.from(runs.values(), (record) => ({ ...record }));
135
+ }
136
+
137
+ module.exports = { mint, verify, registerRun, endRun, verifyRun, liveRuns, principalOf };
@@ -15,12 +15,16 @@
15
15
  // it) slips through — accepted as best-effort; the dream's KPI pass reads the
16
16
  // guard log to spot chronic bypassing.
17
17
  //
18
- // Fail-open doctrine: any error in the gate itself must NEVER brick the
19
- // agent's shell — parse failures, fs errors, unknown payloads all allow.
18
+ // Fail direction: on owner and no-context turns any error in the gate itself
19
+ // must NEVER brick the agent's tools — parse failures, fs errors, unknown
20
+ // payloads all allow. On GUARDED (non-owner) turns the same failures deny:
21
+ // a check that could not run must not widen a staff member's reach.
20
22
 
21
23
  const fs = require("fs");
24
+ const os = require("os");
22
25
  const path = require("path");
23
26
  const CONFIG_DIR = require("../config-dir");
27
+ const { resolveConfigDir } = require("./config-dir");
24
28
 
25
29
  const GUARD_LOG = process.env.TOOL_GUARD_LOG || path.join(CONFIG_DIR, "tool-guard.jsonl");
26
30
  const SETTINGS_FILE = process.env.DENY_GATE_SETTINGS || path.join(CONFIG_DIR, "deny-gate.settings.json");
@@ -309,34 +313,393 @@ function externalControlPlaneDenial(command) {
309
313
  );
310
314
  }
311
315
 
316
+ // ── guarded-speaker read gate ──────────────────────────────────────────────
317
+ // Claude Code's own file-reading tools never pass through a shell, so on a
318
+ // guarded turn they could open the bot's keyring, every session transcript,
319
+ // or ~/.ssh. On a guarded turn a read whose target resolves inside a
320
+ // protected root is denied, and a search (Grep/Glob/LS) is denied when its
321
+ // root is a protected root, inside one, or ABOVE one — searching $HOME or /
322
+ // walks straight through them. Owner and no-context turns never get here.
323
+ const READ_FILE_TOOLS = new Set(["Read", "NotebookRead"]);
324
+ const READ_SEARCH_TOOLS = new Set(["Grep", "Glob", "LS"]); // LS/NotebookRead: older Claude Code builds
325
+ const READ_TOOLS = new Set([...READ_FILE_TOOLS, ...READ_SEARCH_TOOLS]);
326
+
327
+ // Credential stores under the user's home. One list feeds both the read gate
328
+ // and the guarded shell check below, so the two cannot drift apart.
329
+ const HOME_SECRET_PATHS = [
330
+ ".claude", // Claude Code credentials and every session transcript (the owner's private chats)
331
+ ".claude.json", // Claude Code account state and MCP server config (can carry API keys)
332
+ ".codex", // Codex auth.json and session rollouts
333
+ ".ssh", // SSH private keys
334
+ ".aws", // AWS access keys
335
+ ".kube", // Kubernetes cluster credentials
336
+ ".docker", // container registry auth (config.json)
337
+ ".config/gcloud", // Google Cloud credentials
338
+ ".netrc", // plaintext host passwords (curl, git, ftp)
339
+ ".git-credentials", // git credential-store tokens
340
+ ".npmrc", // npm registry tokens
341
+ ];
342
+
343
+ function homeDirs() {
344
+ const out = [];
345
+ for (const get of [() => os.homedir(), () => os.userInfo().homedir]) {
346
+ try {
347
+ const home = get();
348
+ if (home && !out.includes(path.resolve(home))) out.push(path.resolve(home));
349
+ } catch (e) { /* no resolvable home */ }
350
+ }
351
+ return out;
352
+ }
353
+
354
+ // Every root a guarded turn may not read. Absolute paths; symlinks are
355
+ // resolved at match time, on both sides.
356
+ function protectedReadRoots() {
357
+ const roots = [];
358
+ const add = (value) => {
359
+ const raw = String(value || "").trim();
360
+ if (!raw) return;
361
+ const abs = path.resolve(raw);
362
+ if (!roots.includes(abs)) roots.push(abs);
363
+ };
364
+ // Open Claudia's own state: keyring, vault, approvals, grants, transcripts,
365
+ // people, identities, audit logs, crons/jobs, tool state — and the packs
366
+ // (procedure notes), which Phase 1 serves through the bot instead.
367
+ add(resolveConfigDir());
368
+ add(CONFIG_DIR);
369
+ for (const home of homeDirs()) {
370
+ // The default config dir: another bot on this host, or this bot's state
371
+ // from before OPEN_CLAUDIA_CONFIG_DIR moved it.
372
+ add(path.join(home, ".open-claudia"));
373
+ for (const rel of HOME_SECRET_PATHS) add(path.join(home, rel));
374
+ }
375
+ // The same Claude Code / Codex stores when relocated for this process.
376
+ add(process.env.CLAUDE_CONFIG_DIR);
377
+ add(process.env.CODEX_HOME);
378
+ // Linux: /proc/<pid>/environ exposes other processes' environment, which
379
+ // for the bot process means its channel and provider tokens.
380
+ add("/proc");
381
+ // The bot env file named in the system prompt. The rest of the installed
382
+ // package is public source and stays readable; any other .env file inside
383
+ // it is matched by name (isPackageEnvFile).
384
+ add(path.join(PACKAGE_ROOT, ".env"));
385
+ return roots;
386
+ }
387
+
388
+ // Inbound chat attachments are saved under the config dir (files/, media/)
389
+ // and every photo/document turn tells the agent to Read them there, so a
390
+ // guarded turn keeps reading single files there exactly as today. Never a
391
+ // search root: a Grep/Glob over these folders would sweep every attachment
392
+ // from every room.
393
+ function attachmentDirs() {
394
+ const out = [];
395
+ for (const dir of [resolveConfigDir(), CONFIG_DIR]) {
396
+ for (const sub of ["files", "media"]) {
397
+ const p = path.join(path.resolve(dir), sub);
398
+ if (!out.includes(p)) out.push(p);
399
+ }
400
+ }
401
+ return out;
402
+ }
403
+
404
+ const CASE_INSENSITIVE_PATHS = process.platform === "darwin" || process.platform === "win32";
405
+
406
+ function comparablePath(value) {
407
+ const s = String(value).normalize("NFC");
408
+ return CASE_INSENSITIVE_PATHS ? s.toLowerCase() : s;
409
+ }
410
+
411
+ // Prefix match on whole path segments: /a/b contains /a/b/c, not /a/bc.
412
+ function isSameOrInside(child, parent) {
413
+ const c = comparablePath(child);
414
+ const p = comparablePath(parent);
415
+ if (c === p) return true;
416
+ return c.startsWith(p.endsWith(path.sep) ? p : p + path.sep);
417
+ }
418
+
419
+ // Where the OS actually lands for an absolute path: symlinks followed and a
420
+ // `..` applied AFTER them, as open(2) does. fs.realpathSync.native, because
421
+ // the JS realpath folds `..` lexically first (so `link/../x` would escape).
422
+ // A path that does not exist yet resolves through its deepest existing
423
+ // ancestor.
424
+ function physicalPath(rawAbs) {
425
+ const parts = String(rawAbs).split(path.sep);
426
+ for (let keep = parts.length; keep > 0; keep -= 1) {
427
+ const head = parts.slice(0, keep).join(path.sep) || path.sep;
428
+ try {
429
+ const real = fs.realpathSync.native(head);
430
+ const rest = parts.slice(keep).filter(Boolean);
431
+ return rest.length ? path.join(real, ...rest) : real;
432
+ } catch (e) { /* missing or unreadable — try the parent */ }
433
+ }
434
+ return path.resolve(rawAbs);
435
+ }
436
+
437
+ function expandHome(raw) {
438
+ const value = String(raw);
439
+ const home = os.homedir();
440
+ if (value === "~" || value === "$HOME" || value === "${HOME}") return home;
441
+ for (const prefix of ["~/", "$HOME/", "${HOME}/"]) {
442
+ if (value.startsWith(prefix)) return home + path.sep + value.slice(prefix.length);
443
+ }
444
+ return value;
445
+ }
446
+
447
+ // Both views of a tool path: lexical (what a caller that normalises first
448
+ // opens) and physical (what open(2) reaches). A check must pass on both.
449
+ function pathForms(raw, cwd) {
450
+ const expanded = expandHome(raw);
451
+ const rawAbs = path.isAbsolute(expanded) ? expanded : cwd + path.sep + expanded;
452
+ const forms = [path.resolve(cwd, expanded), physicalPath(rawAbs)];
453
+ return forms.filter((form, index) => forms.indexOf(form) === index);
454
+ }
455
+
456
+ function protectedRootForms() {
457
+ const forms = [];
458
+ for (const root of protectedReadRoots()) {
459
+ for (const form of [root, physicalPath(root)]) if (!forms.includes(form)) forms.push(form);
460
+ }
461
+ return forms;
462
+ }
463
+
464
+ const PACKAGE_ENV_FILE_RE = /^\.env(?:\..+)?$/i;
465
+
466
+ function isPackageEnvFile(form) {
467
+ if (!PACKAGE_ENV_FILE_RE.test(path.basename(form))) return false;
468
+ return [PACKAGE_ROOT, physicalPath(PACKAGE_ROOT)].some((root) => isSameOrInside(form, root));
469
+ }
470
+
471
+ // True when reading the file at `raw` (relative to `cwd`) would reach
472
+ // protected state. Attachment folders are exempt unless the path escapes
473
+ // them, or they themselves resolve onto protected state.
474
+ function isProtectedReadPath(raw, cwd = process.cwd()) {
475
+ const base = path.resolve(String(cwd || process.cwd()));
476
+ const roots = protectedRootForms();
477
+ const exempt = attachmentDirs()
478
+ .flatMap((dir) => [dir, physicalPath(dir)])
479
+ .filter((dir) => !roots.some((root) => isSameOrInside(root, dir)));
480
+ return pathForms(raw, base).some((form) => isPackageEnvFile(form)
481
+ || (roots.some((root) => isSameOrInside(form, root))
482
+ && !exempt.some((dir) => isSameOrInside(form, dir))));
483
+ }
484
+
485
+ // True when a search rooted at `raw` would enter protected state: the root is
486
+ // a protected root, inside one, or an ancestor of one.
487
+ function isProtectedSearchRoot(raw, cwd = process.cwd()) {
488
+ const base = path.resolve(String(cwd || process.cwd()));
489
+ return searchFormsProtected(pathForms(raw, base));
490
+ }
491
+
492
+ function searchFormsProtected(forms) {
493
+ const roots = protectedRootForms();
494
+ return forms.some((form) => isPackageEnvFile(form)
495
+ || roots.some((root) => isSameOrInside(form, root) || isSameOrInside(root, form)));
496
+ }
497
+
498
+ // Glob: where the walk actually starts. The literal (wildcard-free) leading
499
+ // segments of the pattern, taken from the pattern itself when it is absolute
500
+ // (or ~-anchored), else from the search root; every `..` after the first
501
+ // wildcard may climb one level higher, so the base climbs with it.
502
+ const GLOB_MAGIC_RE = /[*?[\]{}()!+@\\]/;
503
+
504
+ function globSearchBases(pattern, searchRoot) {
505
+ const expanded = expandHome(pattern);
506
+ const literal = [];
507
+ let climb = 0;
508
+ let wild = false;
509
+ for (const segment of expanded.split("/")) {
510
+ if (!wild && !GLOB_MAGIC_RE.test(segment)) { literal.push(segment); continue; }
511
+ wild = true;
512
+ if (segment === "..") climb += 1;
513
+ }
514
+ const prefix = literal.join("/");
515
+ const start = path.isAbsolute(expanded)
516
+ ? (prefix || path.sep)
517
+ : (prefix ? searchRoot + path.sep + prefix : searchRoot);
518
+ return pathForms(start, searchRoot).map((form) => {
519
+ let out = form;
520
+ for (let i = 0; i < climb; i += 1) out = path.dirname(out);
521
+ return out;
522
+ });
523
+ }
524
+
525
+ function isRegularFile(raw, cwd) {
526
+ try { return fs.statSync(path.resolve(cwd, expandHome(raw))).isFile(); }
527
+ catch (e) { return false; }
528
+ }
529
+
530
+ // Guarded-turn verdict for one read-tool call:
531
+ // { malformed } | { deny, target, search }.
532
+ function evaluateGuardedRead(toolName, input, cwd) {
533
+ if (!input || typeof input !== "object") return { malformed: true };
534
+ const base = path.resolve(typeof cwd === "string" && cwd ? cwd : process.cwd());
535
+ if (READ_FILE_TOOLS.has(toolName)) {
536
+ const target = toolName === "NotebookRead" ? input.notebook_path : input.file_path;
537
+ if (typeof target !== "string" || !target) return { malformed: true };
538
+ return { deny: isProtectedReadPath(target, base), target, search: false };
539
+ }
540
+ if (input.path != null && typeof input.path !== "string") return { malformed: true };
541
+ if (toolName === "LS" && !input.path) return { malformed: true };
542
+ if (toolName === "Glob" && (typeof input.pattern !== "string" || !input.pattern)) return { malformed: true };
543
+ const root = input.path || base;
544
+ // A Grep over one file is a read of that file.
545
+ if (toolName === "Grep" && isRegularFile(root, base)) {
546
+ return { deny: isProtectedReadPath(root, base), target: root, search: false };
547
+ }
548
+ if (isProtectedSearchRoot(root, base)) return { deny: true, target: root, search: true };
549
+ if (toolName === "Glob"
550
+ && searchFormsProtected(globSearchBases(input.pattern, path.resolve(base, expandHome(root))))) {
551
+ return { deny: true, target: input.pattern, search: true };
552
+ }
553
+ return { deny: false, target: root, search: true };
554
+ }
555
+
556
+ function protectedReadDenial(toolName, verdict, speaker) {
557
+ const who = speaker && speaker.name ? ` (${speaker.name})` : "";
558
+ const target = String(verdict.target).slice(0, 200);
559
+ return verdict.search
560
+ ? `⛔ External-speaker guard: ${toolName} over ${target} is blocked on this turn — it would reach the bot's private state or credentials, and the speaker${who} is not the owner.\nSearch a narrower folder inside the project instead.`
561
+ : `⛔ External-speaker guard: ${toolName} of ${target} is blocked on this turn — it is part of the bot's private state or credentials, and the speaker${who} is not the owner.\nAnswer without it.`;
562
+ }
563
+
564
+ // ── guarded-speaker secret reads from the shell ────────────────────────────
565
+ // A deliberately small list of obvious ways a shell reads another process's
566
+ // environment (the bot's carries its tokens) or a credential store. Their own
567
+ // environment (`env`, `printenv`) is left alone. Phase 2 replaces shell text
568
+ // matching with tool-only staff turns; until then this closes the literal
569
+ // forms of what the read gate above denies to Read/Grep/Glob.
570
+ const KEYCHAIN_RE = /\bsecurity\s+(?:find-generic-password|find-internet-password|dump-keychain|export)\b/;
571
+ const PS_RE = /(?:^|[|;&(`'"\n])\s*(?:sudo\s+)?(?:[^\s|;&()`'"]*\/)?ps(?=\s|$)([^|;&\n)`]*)/g;
572
+
573
+ // ps prints other processes' environment with BSD `e` (ps eww, ps auxe) or
574
+ // macOS -E. Linux `-e` only selects all processes and stays free.
575
+ function psShowsEnvironment(cmd) {
576
+ for (const match of cmd.matchAll(PS_RE)) {
577
+ const args = match[1].split(/\s+/).map((arg) => arg.replace(/^['"]+|['"]+$/g, "")).filter(Boolean);
578
+ let valueNext = false;
579
+ for (const arg of args) {
580
+ if (valueNext) { valueNext = false; continue; }
581
+ if (/^-[A-Za-z]*E/.test(arg)) return true;
582
+ if (/^-[A-Za-z]*[oOpUuGgtCq]$/.test(arg)) { valueNext = true; continue; }
583
+ if (/^[A-Za-z]+$/.test(arg) && arg.includes("e")) return true;
584
+ }
585
+ }
586
+ return false;
587
+ }
588
+
589
+ function credentialPathAnchors() {
590
+ const anchors = [];
591
+ const prefixes = ["~/", "$HOME/", "${HOME}/", ...homeDirs().map((home) => home + path.sep)];
592
+ for (const prefix of prefixes) for (const rel of HOME_SECRET_PATHS) anchors.push(prefix + rel);
593
+ for (const moved of [process.env.CLAUDE_CONFIG_DIR, process.env.CODEX_HOME]) {
594
+ if (moved && String(moved).trim()) anchors.push(path.resolve(String(moved).trim()));
595
+ }
596
+ return anchors;
597
+ }
598
+
599
+ function mentionsCredentialPath(cmd) {
600
+ return credentialPathAnchors().some((anchor) => {
601
+ for (let at = cmd.indexOf(anchor); at !== -1; at = cmd.indexOf(anchor, at + 1)) {
602
+ const next = cmd[at + anchor.length];
603
+ if (next === undefined || !/[A-Za-z0-9_-]/.test(next)) return true;
604
+ }
605
+ return false;
606
+ });
607
+ }
608
+
609
+ // Pure: null, or the id of the secret-read pattern the command matches.
610
+ function secretReadRule(command) {
611
+ const cmd = String(command || "");
612
+ if (!cmd.trim() || isAuthorizedChannelCommand(cmd)) return null;
613
+ if (cmd.includes("/proc/") && cmd.includes("environ")) return "proc-environ";
614
+ if (psShowsEnvironment(cmd)) return "ps-environment";
615
+ if (KEYCHAIN_RE.test(cmd)) return "os-keychain";
616
+ if (mentionsCredentialPath(cmd)) return "credential-store";
617
+ return null;
618
+ }
619
+
620
+ function externalSecretReadDenial(command) {
621
+ const rule = secretReadRule(command);
622
+ if (!rule) return null;
623
+ const speaker = guardedSpeakerFromEnv();
624
+ if (!speaker) return null;
625
+ return {
626
+ rule,
627
+ message:
628
+ `⛔ External-speaker guard: this command reads another process's environment or a credential store, and the speaker${speaker.name ? ` (${speaker.name})` : ""} is not the owner.\n` +
629
+ `Answer without it.`,
630
+ };
631
+ }
632
+
633
+ // A hook call that could not be checked. Owner and no-context turns fail
634
+ // OPEN as they always have; a guarded turn fails CLOSED.
635
+ function hookFailure(toolName, reason) {
636
+ let speaker = null;
637
+ try { speaker = guardedSpeakerFromEnv(); }
638
+ catch (e) { speaker = process.env.OC_CHANNEL_ADAPTER && process.env.OC_CHANNEL_ID ? { name: "" } : null; }
639
+ if (!speaker) return { exit: 0, stderr: "" };
640
+ logGuardEvent({ kind: "deny", rule: "external-guard-failure", tool: String(toolName || "").slice(0, 60), reason });
641
+ return {
642
+ exit: 2,
643
+ stderr: `⛔ External-speaker guard: this ${toolName || "tool"} call could not be checked, so it is blocked on this turn (the speaker is not the owner).`,
644
+ };
645
+ }
646
+
647
+ // Claude sends the shell command as a string; accept an argv array too.
648
+ function shellCommandOf(input) {
649
+ if (!input || typeof input !== "object") return null;
650
+ if (typeof input.command === "string") return input.command;
651
+ if (Array.isArray(input.command) && input.command.every((part) => typeof part === "string")) {
652
+ return input.command.join(" ");
653
+ }
654
+ return null;
655
+ }
656
+
312
657
  // Hook entry, pure enough to test: takes the PreToolUse stdin payload (object
313
658
  // or JSON string), returns { exit, stderr }. exit 2 = deny (stderr is fed back
314
- // to the model); exit 0 = allow. Anything unexpected → allow (fail-open).
659
+ // to the model); exit 0 = allow. Anything unexpected → allow on owner and
660
+ // no-context turns (fail-open), deny on guarded turns (fail-closed).
315
661
  function runHook(payload) {
316
662
  let data = payload;
317
663
  try {
318
664
  if (typeof payload === "string") data = JSON.parse(payload);
319
665
  } catch (e) {
320
- return { exit: 0, stderr: "" };
666
+ return hookFailure("", "unparseable payload");
321
667
  }
668
+ let toolName = "";
322
669
  try {
670
+ if (!data || typeof data !== "object" || typeof data.tool_name !== "string" || !data.tool_name) {
671
+ return hookFailure("", "malformed payload");
672
+ }
673
+ toolName = data.tool_name;
323
674
  // File-mutation tools are gated by SPEAKER, not by command content — and
324
675
  // deliberately outside the relaxed tooling-mode bypass below: tooling mode
325
676
  // is an owner workflow preference, this is a security control.
326
- if (data && FILE_MUTATION_TOOLS.has(data.tool_name)) {
327
- const denial = externalFileMutationDenial(data.tool_name);
677
+ if (FILE_MUTATION_TOOLS.has(toolName)) {
678
+ const denial = externalFileMutationDenial(toolName);
328
679
  if (!denial) return { exit: 0, stderr: "" };
329
680
  const input = data.tool_input || {};
330
681
  logGuardEvent({
331
682
  kind: "deny",
332
683
  rule: "external-file-write",
333
- tool: data.tool_name,
684
+ tool: toolName,
334
685
  file: String(input.file_path || input.notebook_path || input.command || "").slice(0, 300),
335
686
  });
336
687
  return { exit: 2, stderr: denial };
337
688
  }
338
- if (!data || data.tool_name !== "Bash") return { exit: 0, stderr: "" };
339
- const command = data.tool_input && data.tool_input.command;
689
+ // Read tools: speaker first, so owner and no-context turns pass through
690
+ // untouched (no path work, no log line).
691
+ if (READ_TOOLS.has(toolName)) {
692
+ const speaker = guardedSpeakerFromEnv();
693
+ if (!speaker) return { exit: 0, stderr: "" };
694
+ const verdict = evaluateGuardedRead(toolName, data.tool_input, data.cwd);
695
+ if (verdict.malformed) return hookFailure(toolName, "malformed payload");
696
+ if (!verdict.deny) return { exit: 0, stderr: "" };
697
+ logGuardEvent({ kind: "deny", rule: "external-protected-read", tool: toolName, target: String(verdict.target).slice(0, 300) });
698
+ return { exit: 2, stderr: protectedReadDenial(toolName, verdict, speaker) };
699
+ }
700
+ if (toolName !== "Bash") return { exit: 0, stderr: "" };
701
+ const command = shellCommandOf(data.tool_input);
702
+ if (command === null) return hookFailure(toolName, "malformed payload");
340
703
  // Control-plane references are speaker-gated ahead of the tooling-mode
341
704
  // switch — a security control, not a workflow preference.
342
705
  const cpDenial = externalControlPlaneDenial(command);
@@ -344,6 +707,11 @@ function runHook(payload) {
344
707
  logGuardEvent({ kind: "deny", rule: "external-control-plane", command: String(command).slice(0, 300) });
345
708
  return { exit: 2, stderr: cpDenial };
346
709
  }
710
+ const secretDenial = externalSecretReadDenial(command);
711
+ if (secretDenial) {
712
+ logGuardEvent({ kind: "deny", rule: "external-secret-read", pattern: secretDenial.rule, command: String(command).slice(0, 300) });
713
+ return { exit: 2, stderr: secretDenial.message };
714
+ }
347
715
  // Relaxed tooling mode: the gate observes but never blocks. The event is
348
716
  // still logged (as relaxed-pass, not deny) so the dream KPI can report
349
717
  // what WOULD have been denied — visibility survives the mode switch.
@@ -357,7 +725,7 @@ function runHook(payload) {
357
725
  ? { exit: 2, stderr: policy.message }
358
726
  : { exit: 0, stderr: "" };
359
727
  } catch (e) {
360
- return { exit: 0, stderr: "" };
728
+ return hookFailure(toolName, "guard error");
361
729
  }
362
730
  }
363
731
 
@@ -373,8 +741,9 @@ function ensureHookSettings() {
373
741
  }
374
742
 
375
743
  module.exports = {
376
- RULES, RAW_OP_PATTERNS, GUARD_LOG, SETTINGS_FILE, FILE_MUTATION_TOOLS,
744
+ RULES, RAW_OP_PATTERNS, GUARD_LOG, SETTINGS_FILE, FILE_MUTATION_TOOLS, READ_TOOLS, HOME_SECRET_PATHS,
377
745
  isAuthorizedChannelCommand, matchRule, denyMessage, evaluateCommandPolicy, logGuardEvent, runHook, hookSettings, ensureHookSettings,
378
746
  externalFileMutationDenial, externalControlPlaneDenial, guardedSpeakerFromEnv,
747
+ protectedReadRoots, isProtectedReadPath, isProtectedSearchRoot, secretReadRule,
379
748
  matchRawOp, noteRawOp, readGuardEvents,
380
749
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@inetafrica/open-claudia",
3
- "version": "3.4.24",
3
+ "version": "3.5.0",
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-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-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-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-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-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",