@inetafrica/open-claudia 3.4.24 → 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 +55 -0
- package/bin/agent.js +4 -0
- package/bin/cli.js +10 -1
- package/bin/entity.js +17 -15
- package/bin/ideas.js +10 -1
- package/bin/keyring.js +19 -18
- package/bin/lessons.js +10 -1
- package/bin/loopback-client.js +62 -3
- package/bin/owner-gate.js +39 -0
- package/bin/pack.js +11 -1
- package/bin/tool.js +77 -49
- package/bin/transcript-search.js +42 -8
- package/bin/transcript-window.js +31 -2
- package/core/actions.js +15 -4
- package/core/approvals.js +28 -2
- package/core/bot-endpoint.js +194 -0
- package/core/grants.js +156 -27
- package/core/handlers.js +8 -2
- package/core/loopback.js +224 -69
- package/core/providers/claude-hook.js +5 -0
- package/core/runner.js +50 -0
- package/core/scheduler.js +100 -2
- package/core/speaker-attest.js +82 -1
- package/core/tool-guard.js +395 -11
- package/package.json +2 -2
- package/project-transcripts.js +25 -0
- package/test-approval-async.js +22 -0
- package/test-approval-cards.js +40 -0
- package/test-provider-language.js +14 -1
- package/test-provider-parity-e2e.js +8 -2
- package/test-provider-scheduler.js +14 -0
- package/test-provider-tool-hooks.js +11 -0
- package/test-tool-guard.js +12 -3
- package/test-tool-manifest.js +31 -0
package/core/tool-guard.js
CHANGED
|
@@ -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
|
|
19
|
-
// agent's
|
|
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,408 @@ 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
|
+
// 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
|
+
|
|
622
|
+
// Pure: null, or the id of the secret-read pattern the command matches.
|
|
623
|
+
function secretReadRule(command) {
|
|
624
|
+
const cmd = String(command || "");
|
|
625
|
+
if (!cmd.trim() || isAuthorizedChannelCommand(cmd)) return null;
|
|
626
|
+
if (cmd.includes("/proc/") && cmd.includes("environ")) return "proc-environ";
|
|
627
|
+
if (psShowsEnvironment(cmd)) return "ps-environment";
|
|
628
|
+
if (KEYCHAIN_RE.test(cmd)) return "os-keychain";
|
|
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";
|
|
632
|
+
return null;
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
function externalSecretReadDenial(command) {
|
|
636
|
+
const rule = secretReadRule(command);
|
|
637
|
+
if (!rule) return null;
|
|
638
|
+
const speaker = guardedSpeakerFromEnv();
|
|
639
|
+
if (!speaker) return null;
|
|
640
|
+
return {
|
|
641
|
+
rule,
|
|
642
|
+
message:
|
|
643
|
+
`⛔ 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` +
|
|
644
|
+
`Answer without it.`,
|
|
645
|
+
};
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
// A hook call that could not be checked. Owner and no-context turns fail
|
|
649
|
+
// OPEN as they always have; a guarded turn fails CLOSED.
|
|
650
|
+
function hookFailure(toolName, reason) {
|
|
651
|
+
let speaker = null;
|
|
652
|
+
try { speaker = guardedSpeakerFromEnv(); }
|
|
653
|
+
catch (e) { speaker = process.env.OC_CHANNEL_ADAPTER && process.env.OC_CHANNEL_ID ? { name: "" } : null; }
|
|
654
|
+
if (!speaker) return { exit: 0, stderr: "" };
|
|
655
|
+
logGuardEvent({ kind: "deny", rule: "external-guard-failure", tool: String(toolName || "").slice(0, 60), reason });
|
|
656
|
+
return {
|
|
657
|
+
exit: 2,
|
|
658
|
+
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).`,
|
|
659
|
+
};
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
// Claude sends the shell command as a string; accept an argv array too.
|
|
663
|
+
function shellCommandOf(input) {
|
|
664
|
+
if (!input || typeof input !== "object") return null;
|
|
665
|
+
if (typeof input.command === "string") return input.command;
|
|
666
|
+
if (Array.isArray(input.command) && input.command.every((part) => typeof part === "string")) {
|
|
667
|
+
return input.command.join(" ");
|
|
668
|
+
}
|
|
669
|
+
return null;
|
|
670
|
+
}
|
|
671
|
+
|
|
312
672
|
// Hook entry, pure enough to test: takes the PreToolUse stdin payload (object
|
|
313
673
|
// or JSON string), returns { exit, stderr }. exit 2 = deny (stderr is fed back
|
|
314
|
-
// to the model); exit 0 = allow. Anything unexpected → allow
|
|
674
|
+
// to the model); exit 0 = allow. Anything unexpected → allow on owner and
|
|
675
|
+
// no-context turns (fail-open), deny on guarded turns (fail-closed).
|
|
315
676
|
function runHook(payload) {
|
|
316
677
|
let data = payload;
|
|
317
678
|
try {
|
|
318
679
|
if (typeof payload === "string") data = JSON.parse(payload);
|
|
319
680
|
} catch (e) {
|
|
320
|
-
return
|
|
681
|
+
return hookFailure("", "unparseable payload");
|
|
321
682
|
}
|
|
683
|
+
let toolName = "";
|
|
322
684
|
try {
|
|
685
|
+
if (!data || typeof data !== "object" || typeof data.tool_name !== "string" || !data.tool_name) {
|
|
686
|
+
return hookFailure("", "malformed payload");
|
|
687
|
+
}
|
|
688
|
+
toolName = data.tool_name;
|
|
323
689
|
// File-mutation tools are gated by SPEAKER, not by command content — and
|
|
324
690
|
// deliberately outside the relaxed tooling-mode bypass below: tooling mode
|
|
325
691
|
// is an owner workflow preference, this is a security control.
|
|
326
|
-
if (
|
|
327
|
-
const denial = externalFileMutationDenial(
|
|
692
|
+
if (FILE_MUTATION_TOOLS.has(toolName)) {
|
|
693
|
+
const denial = externalFileMutationDenial(toolName);
|
|
328
694
|
if (!denial) return { exit: 0, stderr: "" };
|
|
329
695
|
const input = data.tool_input || {};
|
|
330
696
|
logGuardEvent({
|
|
331
697
|
kind: "deny",
|
|
332
698
|
rule: "external-file-write",
|
|
333
|
-
tool:
|
|
699
|
+
tool: toolName,
|
|
334
700
|
file: String(input.file_path || input.notebook_path || input.command || "").slice(0, 300),
|
|
335
701
|
});
|
|
336
702
|
return { exit: 2, stderr: denial };
|
|
337
703
|
}
|
|
338
|
-
|
|
339
|
-
|
|
704
|
+
// Read tools: speaker first, so owner and no-context turns pass through
|
|
705
|
+
// untouched (no path work, no log line).
|
|
706
|
+
if (READ_TOOLS.has(toolName)) {
|
|
707
|
+
const speaker = guardedSpeakerFromEnv();
|
|
708
|
+
if (!speaker) return { exit: 0, stderr: "" };
|
|
709
|
+
const verdict = evaluateGuardedRead(toolName, data.tool_input, data.cwd);
|
|
710
|
+
if (verdict.malformed) return hookFailure(toolName, "malformed payload");
|
|
711
|
+
if (!verdict.deny) return { exit: 0, stderr: "" };
|
|
712
|
+
logGuardEvent({ kind: "deny", rule: "external-protected-read", tool: toolName, target: String(verdict.target).slice(0, 300) });
|
|
713
|
+
return { exit: 2, stderr: protectedReadDenial(toolName, verdict, speaker) };
|
|
714
|
+
}
|
|
715
|
+
if (toolName !== "Bash") return { exit: 0, stderr: "" };
|
|
716
|
+
const command = shellCommandOf(data.tool_input);
|
|
717
|
+
if (command === null) return hookFailure(toolName, "malformed payload");
|
|
340
718
|
// Control-plane references are speaker-gated ahead of the tooling-mode
|
|
341
719
|
// switch — a security control, not a workflow preference.
|
|
342
720
|
const cpDenial = externalControlPlaneDenial(command);
|
|
@@ -344,6 +722,11 @@ function runHook(payload) {
|
|
|
344
722
|
logGuardEvent({ kind: "deny", rule: "external-control-plane", command: String(command).slice(0, 300) });
|
|
345
723
|
return { exit: 2, stderr: cpDenial };
|
|
346
724
|
}
|
|
725
|
+
const secretDenial = externalSecretReadDenial(command);
|
|
726
|
+
if (secretDenial) {
|
|
727
|
+
logGuardEvent({ kind: "deny", rule: "external-secret-read", pattern: secretDenial.rule, command: String(command).slice(0, 300) });
|
|
728
|
+
return { exit: 2, stderr: secretDenial.message };
|
|
729
|
+
}
|
|
347
730
|
// Relaxed tooling mode: the gate observes but never blocks. The event is
|
|
348
731
|
// still logged (as relaxed-pass, not deny) so the dream KPI can report
|
|
349
732
|
// what WOULD have been denied — visibility survives the mode switch.
|
|
@@ -357,7 +740,7 @@ function runHook(payload) {
|
|
|
357
740
|
? { exit: 2, stderr: policy.message }
|
|
358
741
|
: { exit: 0, stderr: "" };
|
|
359
742
|
} catch (e) {
|
|
360
|
-
return
|
|
743
|
+
return hookFailure(toolName, "guard error");
|
|
361
744
|
}
|
|
362
745
|
}
|
|
363
746
|
|
|
@@ -373,8 +756,9 @@ function ensureHookSettings() {
|
|
|
373
756
|
}
|
|
374
757
|
|
|
375
758
|
module.exports = {
|
|
376
|
-
RULES, RAW_OP_PATTERNS, GUARD_LOG, SETTINGS_FILE, FILE_MUTATION_TOOLS,
|
|
759
|
+
RULES, RAW_OP_PATTERNS, GUARD_LOG, SETTINGS_FILE, FILE_MUTATION_TOOLS, READ_TOOLS, HOME_SECRET_PATHS,
|
|
377
760
|
isAuthorizedChannelCommand, matchRule, denyMessage, evaluateCommandPolicy, logGuardEvent, runHook, hookSettings, ensureHookSettings,
|
|
378
761
|
externalFileMutationDenial, externalControlPlaneDenial, guardedSpeakerFromEnv,
|
|
762
|
+
protectedReadRoots, isProtectedReadPath, isProtectedSearchRoot, secretReadRule,
|
|
379
763
|
matchRawOp, noteRawOp, readGuardEvents,
|
|
380
764
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@inetafrica/open-claudia",
|
|
3
|
-
"version": "3.
|
|
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-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-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",
|
package/project-transcripts.js
CHANGED
|
@@ -136,9 +136,34 @@ class ProjectTranscripts {
|
|
|
136
136
|
}
|
|
137
137
|
}
|
|
138
138
|
|
|
139
|
+
// Who a transcript file belongs to, for the room-scoping guard in the search
|
|
140
|
+
// CLIs. Every project transcript is keyed by (canonicalUserId, projectPath) and
|
|
141
|
+
// records the owning canonicalUserId in its sidecar `.json` (and on each JSONL
|
|
142
|
+
// entry). Reading it back lets a non-owner CLI confirm a transcript it was
|
|
143
|
+
// pointed at is actually the caller's own — an env-forged OC_TRANSCRIPT_PATH
|
|
144
|
+
// aimed at someone else's file resolves to a different canonicalUserId and is
|
|
145
|
+
// refused. Returns null when ownership cannot be established (fail closed).
|
|
146
|
+
function transcriptOwnerOf(transcriptPath) {
|
|
147
|
+
const file = String(transcriptPath || "");
|
|
148
|
+
if (!file) return null;
|
|
149
|
+
const sidecar = file.endsWith(".jsonl") ? `${file.slice(0, -6)}.json` : `${file}.json`;
|
|
150
|
+
try {
|
|
151
|
+
const meta = JSON.parse(fs.readFileSync(sidecar, "utf8"));
|
|
152
|
+
if (meta && meta.canonicalUserId) return String(meta.canonicalUserId);
|
|
153
|
+
} catch (e) { /* fall through to the JSONL header */ }
|
|
154
|
+
try {
|
|
155
|
+
const raw = fs.readFileSync(file, "utf8");
|
|
156
|
+
const newline = raw.indexOf("\n");
|
|
157
|
+
const first = JSON.parse(newline === -1 ? raw : raw.slice(0, newline));
|
|
158
|
+
if (first && first.canonicalUserId) return String(first.canonicalUserId);
|
|
159
|
+
} catch (e) { /* unreadable → unknown owner */ }
|
|
160
|
+
return null;
|
|
161
|
+
}
|
|
162
|
+
|
|
139
163
|
module.exports = {
|
|
140
164
|
ProjectTranscripts,
|
|
141
165
|
normalizeProjectPath,
|
|
142
166
|
projectHash,
|
|
143
167
|
truthy,
|
|
168
|
+
transcriptOwnerOf,
|
|
144
169
|
};
|
package/test-approval-async.js
CHANGED
|
@@ -124,6 +124,28 @@ const approvals = require("./core/approvals");
|
|
|
124
124
|
"legacy approvals with an undisplayed suffix require a fresh decision",
|
|
125
125
|
);
|
|
126
126
|
|
|
127
|
+
// ── an approval carries EXACTLY the tier on the owner's card ──
|
|
128
|
+
// A write approval never redeems for a run that now resolves destructive
|
|
129
|
+
// (and the refusal leaves the token intact); the tier a redemption exports
|
|
130
|
+
// is the record's own, and a record without one falls back to the run's
|
|
131
|
+
// tier — never to "destructive" by default.
|
|
132
|
+
const writeRec = approvals.create({ tool: "fleet", verb: "restart", tier: "write", command: CMD, channelId: "c1", adapter: "tg" });
|
|
133
|
+
approvals.decide(writeRec.id, "approved", "c1");
|
|
134
|
+
const stretched = approvals.consume(writeRec.id, CMD, { tier: "destructive" });
|
|
135
|
+
assert.strictEqual(stretched.ok, false, "a write approval cannot redeem a destructive run");
|
|
136
|
+
assert.ok(/covers a write run, but this run is destructive/.test(stretched.reason));
|
|
137
|
+
assert.strictEqual(approvals.read(writeRec.id).consumedAt, undefined, "the tier refusal does not burn the token");
|
|
138
|
+
const fits = approvals.consume(writeRec.id, CMD, { tier: "write" });
|
|
139
|
+
assert.strictEqual(fits.ok, true, "the approved tier redeems");
|
|
140
|
+
assert.strictEqual(approvals.approvedTier(fits.record, "destructive"), "write", "the record's tier is what the run carries");
|
|
141
|
+
const untiered = approvals.create({ tool: "fleet", command: CMD, channelId: "c1", adapter: "tg" });
|
|
142
|
+
assert.strictEqual(untiered.tier, "", "no tier given → none recorded (not destructive)");
|
|
143
|
+
approvals.decide(untiered.id, "approved", "c1");
|
|
144
|
+
const legacyRun = approvals.consume(untiered.id, CMD, { tier: "destructive" });
|
|
145
|
+
assert.strictEqual(legacyRun.ok, true, "a tierless record binds nothing extra");
|
|
146
|
+
assert.strictEqual(approvals.approvedTier(legacyRun.record, "write"), "write", "a tierless record exports the run's own tier");
|
|
147
|
+
assert.strictEqual(approvals.approvedTier({ tier: "reply" }, "read-only"), "read-only", "non-tool tiers never widen a run");
|
|
148
|
+
|
|
127
149
|
const quotedCommand = approvals.formatApprovedCommand(["open-claudia", "tool", "run", "fleet", "purge", "node one", "it's-safe"]);
|
|
128
150
|
assert.strictEqual(quotedCommand, "open-claudia tool run fleet purge 'node one' 'it'\"'\"'s-safe'");
|
|
129
151
|
|