@inetafrica/open-claudia 3.0.44 → 3.1.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.
@@ -106,5 +106,37 @@ const approvals = require("./core/approvals");
106
106
  approvals.decide(rec3.id, "denied", "c1");
107
107
  assert.strictEqual(await approvals.waitForDecision(rec3.id, { timeoutMs: 5000, pollMs: 50 }), "denied");
108
108
 
109
+ // ── B10: missing keyring creds are preflighted BEFORE the one-shot approval
110
+ // token is consumed. The old order burned the human's decision on a run that
111
+ // could never start; the same token must survive and redeem after the key
112
+ // is set, with the byte-identical command.
113
+ const toolCli = require("./bin/tool");
114
+ const b10src = path.join(tmp, "vaulted.sh");
115
+ fs.writeFileSync(b10src, "#!/usr/bin/env bash\necho ran-vaulted\n");
116
+ tools.addTool(b10src, { name: "vaulted", description: "needs a cred", risk: "destructive", requires: ["vaulted_api_key"] });
117
+ const b10cmd = approvals.formatApprovedCommand(["open-claudia", "tool", "run", "vaulted", "go"]);
118
+ const b10rec = approvals.create({ tool: "vaulted", tier: "destructive", command: b10cmd, channelId: "c1", adapter: "tg" });
119
+ approvals.decide(b10rec.id, "approved", "c1");
120
+ const errs = [];
121
+ const realErr = console.error;
122
+ console.error = (...a) => errs.push(a.join(" "));
123
+ try {
124
+ process.exitCode = 0;
125
+ await toolCli.run(["run", "vaulted", "go", "--approval", b10rec.id]);
126
+ assert.strictEqual(process.exitCode, 1, "missing creds refuse the run with exit 1, not an approval error");
127
+ assert.ok(errs.some((l) => /missing keyring keys vaulted_api_key/.test(l)), "failure names the missing key");
128
+ assert.ok(errs.some((l) => /NOT consumed/.test(l)), "agent is told the token survived");
129
+ assert.strictEqual(approvals.read(b10rec.id).consumedAt, undefined, "the approval token is NOT burned by the creds failure");
130
+
131
+ fs.writeFileSync(process.env.KEYRING_FILE, JSON.stringify({ vaulted_api_key: "k" }));
132
+ process.exitCode = 0;
133
+ await toolCli.run(["run", "vaulted", "go", "--approval", b10rec.id]);
134
+ assert.strictEqual(process.exitCode, 0, "the same token redeems once the key exists");
135
+ assert.ok(approvals.read(b10rec.id).consumedAt, "token consumed exactly by the real run");
136
+ } finally {
137
+ console.error = realErr;
138
+ process.exitCode = 0;
139
+ }
140
+
109
141
  console.log("tool manifest OK");
110
142
  })().catch((e) => { console.error(e); process.exit(1); });
package/test-tools.js CHANGED
@@ -490,4 +490,35 @@ assert.ok(tools.removeTool("hello"), "removeTool returns the removed tool");
490
490
  assert.strictEqual(tools.findTool("hello"), null, "removed tool is gone");
491
491
  assert.ok(!fs.existsSync(path.join(process.env.TOOLS_DIR, "hello")), "removed tool's directory is gone");
492
492
 
493
+ // ── keyring durability (B6/B13): atomic save + .bak fallback on corruption ──
494
+ // A half-written keyring must not silently strip every tool of credentials:
495
+ // save() snapshots the last-good file, load() falls back to it and says so
496
+ // loudly on stderr (once per process) instead of returning {} in silence.
497
+ const keyring = require("./core/keyring");
498
+ const KR = process.env.KEYRING_FILE;
499
+ fs.rmSync(KR, { force: true });
500
+ fs.rmSync(KR + ".bak", { force: true });
501
+ keyring.set("kr_a", "1");
502
+ assert.ok(!fs.existsSync(KR + ".bak"), "first save has no previous version to snapshot");
503
+ keyring.set("kr_b", "2");
504
+ assert.deepStrictEqual(JSON.parse(fs.readFileSync(KR + ".bak", "utf-8")), { kr_a: "1" },
505
+ "second save snapshots the previous good version to .bak");
506
+ assert.strictEqual(fs.statSync(KR).mode & 0o777, 0o600, "keyring file is 0600 from birth");
507
+ const krErrs = [];
508
+ const realErr = console.error;
509
+ console.error = (...a) => krErrs.push(a.join(" "));
510
+ try {
511
+ fs.writeFileSync(KR, "{truncated-by-crash"); // simulate a torn write
512
+ assert.deepStrictEqual(keyring.all(), { kr_a: "1" }, "corrupt primary falls back to .bak snapshot");
513
+ assert.strictEqual(keyring.get("kr_a"), "1", "get() reads through the fallback");
514
+ keyring.all();
515
+ assert.strictEqual(krErrs.filter((l) => /corrupt/.test(l)).length, 1, "corruption is warned exactly once per process");
516
+ } finally {
517
+ console.error = realErr;
518
+ }
519
+ keyring.set("kr_c", "3"); // next save repairs the primary from the fallback view
520
+ assert.deepStrictEqual(JSON.parse(fs.readFileSync(KR, "utf-8")), { kr_a: "1", kr_c: "3" },
521
+ "save after corruption rewrites a valid primary from the recovered data");
522
+ assert.ok(!fs.readdirSync(tmp).some((f) => f.endsWith(".tmp")), "no stray temp files left behind by atomic writes");
523
+
493
524
  console.log("tools OK");