@i4ctime/q-ring 0.13.1 → 0.14.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.
@@ -177,7 +177,7 @@ function recordAccess(envelope) {
177
177
 
178
178
  // src/core/collapse.ts
179
179
  import { execSync } from "child_process";
180
- import { existsSync, readFileSync as readFileSync2 } from "fs";
180
+ import { readFileSync as readFileSync2, statSync } from "fs";
181
181
  import { join as join2 } from "path";
182
182
  var BRANCH_ENV_MAP = {
183
183
  main: "prod",
@@ -191,28 +191,49 @@ var BRANCH_ENV_MAP = {
191
191
  test: "test",
192
192
  testing: "test"
193
193
  };
194
+ var BRANCH_CACHE_TTL_MS = 2e3;
195
+ var branchCache = null;
194
196
  function detectGitBranch(cwd) {
197
+ const dir = cwd ?? process.cwd();
198
+ const now = Date.now();
199
+ if (branchCache && branchCache.cwd === dir && now - branchCache.at < BRANCH_CACHE_TTL_MS) {
200
+ return branchCache.branch;
201
+ }
202
+ let branch = null;
195
203
  try {
196
- const branch = execSync("git rev-parse --abbrev-ref HEAD", {
197
- cwd: cwd ?? process.cwd(),
204
+ const out = execSync("git rev-parse --abbrev-ref HEAD", {
205
+ cwd: dir,
198
206
  stdio: ["pipe", "pipe", "pipe"],
199
207
  encoding: "utf8",
200
208
  timeout: 3e3
201
209
  }).trim();
202
- return branch || null;
210
+ branch = out || null;
203
211
  } catch {
204
- return null;
205
212
  }
213
+ branchCache = { cwd: dir, at: now, branch };
214
+ return branch;
206
215
  }
216
+ var configCache = null;
207
217
  function readProjectConfig(projectPath) {
208
218
  const configPath = join2(projectPath ?? process.cwd(), ".q-ring.json");
219
+ let mtimeMs = 0;
209
220
  try {
210
- if (existsSync(configPath)) {
211
- return JSON.parse(readFileSync2(configPath, "utf8"));
212
- }
221
+ mtimeMs = statSync(configPath).mtimeMs;
213
222
  } catch {
214
223
  }
215
- return null;
224
+ if (configCache && configCache.path === configPath && configCache.mtimeMs === mtimeMs) {
225
+ return configCache.config;
226
+ }
227
+ let config = null;
228
+ if (mtimeMs > 0) {
229
+ try {
230
+ config = JSON.parse(readFileSync2(configPath, "utf8"));
231
+ } catch {
232
+ config = null;
233
+ }
234
+ }
235
+ configCache = { path: configPath, mtimeMs, config };
236
+ return config;
216
237
  }
217
238
  function collapseEnvironment(ctx = {}) {
218
239
  if (ctx.explicit) {
@@ -266,38 +287,137 @@ function mapEnvName(raw) {
266
287
 
267
288
  // src/core/observer.ts
268
289
  import {
269
- existsSync as existsSync2,
270
- mkdirSync,
290
+ existsSync,
291
+ mkdirSync as mkdirSync2,
271
292
  appendFileSync,
272
- readFileSync as readFileSync3,
293
+ chmodSync,
294
+ readFileSync as readFileSync4,
273
295
  openSync,
274
296
  fstatSync,
275
297
  readSync,
276
298
  closeSync,
277
- statSync
299
+ statSync as statSync3
300
+ } from "fs";
301
+ import { join as join4 } from "path";
302
+ import { homedir as homedir2 } from "os";
303
+ import { createHash, createHmac, randomBytes } from "crypto";
304
+ import { Entry } from "@napi-rs/keyring";
305
+
306
+ // src/utils/file-lock.ts
307
+ import {
308
+ mkdirSync,
309
+ writeFileSync,
310
+ unlinkSync,
311
+ readFileSync as readFileSync3,
312
+ statSync as statSync2
278
313
  } from "fs";
279
314
  import { join as join3 } from "path";
280
315
  import { homedir } from "os";
281
- import { createHash } from "crypto";
316
+ function sleepSync(ms) {
317
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
318
+ }
319
+ function isProcessAlive(pid) {
320
+ try {
321
+ process.kill(pid, 0);
322
+ return true;
323
+ } catch (err) {
324
+ return err.code === "EPERM";
325
+ }
326
+ }
327
+ function withFileLock(name, fn, opts = {}) {
328
+ const lockDir = join3(homedir(), ".config", "q-ring", opts.dir ?? "locks");
329
+ mkdirSync(lockDir, { recursive: true, mode: 448 });
330
+ const safe = Buffer.from(name, "utf8").toString("base64url");
331
+ const lockPath = join3(lockDir, `${safe}.lock`);
332
+ const deadline = Date.now() + (opts.timeoutMs ?? 8e3);
333
+ const staleMs = opts.staleMs ?? 3e4;
334
+ while (Date.now() < deadline) {
335
+ try {
336
+ writeFileSync(lockPath, `${process.pid}
337
+ `, { flag: "wx", mode: 384 });
338
+ try {
339
+ return fn();
340
+ } finally {
341
+ try {
342
+ unlinkSync(lockPath);
343
+ } catch {
344
+ }
345
+ }
346
+ } catch {
347
+ try {
348
+ const holderPid = parseInt(readFileSync3(lockPath, "utf8").trim(), 10);
349
+ const ageMs = Date.now() - statSync2(lockPath).mtimeMs;
350
+ const stale = Number.isInteger(holderPid) && holderPid > 0 && !isProcessAlive(holderPid) || ageMs > staleMs;
351
+ if (stale) {
352
+ unlinkSync(lockPath);
353
+ continue;
354
+ }
355
+ } catch {
356
+ }
357
+ sleepSync(15);
358
+ }
359
+ }
360
+ throw new Error(`Could not acquire lock "${name}" (timeout)`);
361
+ }
362
+
363
+ // src/core/observer.ts
364
+ var AUDIT_KEYRING_SERVICE = "qring-audit-chain";
365
+ var AUDIT_KEY_ACCOUNT = "hmac-key";
366
+ var AUDIT_ANCHOR_ACCOUNT = "chain-head";
367
+ var warnedNoKeyring = false;
368
+ function getAuditKey() {
369
+ try {
370
+ const entry = new Entry(AUDIT_KEYRING_SERVICE, AUDIT_KEY_ACCOUNT);
371
+ const stored = entry.getPassword();
372
+ if (stored) return Buffer.from(stored, "base64");
373
+ const key = randomBytes(32);
374
+ entry.setPassword(key.toString("base64"));
375
+ return key;
376
+ } catch {
377
+ if (!warnedNoKeyring) {
378
+ console.error(
379
+ "q-ring: WARNING \u2014 OS keyring unavailable; the audit chain has no keyed anchor on this host and is NOT tamper-evident against truncation or full-file rewrite. (In-file SHA-256 chaining still detects edits.)"
380
+ );
381
+ warnedNoKeyring = true;
382
+ }
383
+ return null;
384
+ }
385
+ }
386
+ function headAnchor(line, key) {
387
+ return createHmac("sha256", key).update(line).digest("hex");
388
+ }
389
+ function readStoredAnchor() {
390
+ try {
391
+ return new Entry(AUDIT_KEYRING_SERVICE, AUDIT_ANCHOR_ACCOUNT).getPassword() ?? null;
392
+ } catch {
393
+ return null;
394
+ }
395
+ }
396
+ function writeStoredAnchor(hash) {
397
+ try {
398
+ new Entry(AUDIT_KEYRING_SERVICE, AUDIT_ANCHOR_ACCOUNT).setPassword(hash);
399
+ } catch {
400
+ }
401
+ }
282
402
  function getAuditDir() {
283
403
  if (process.env.QRING_AUDIT_DIR) {
284
- if (!existsSync2(process.env.QRING_AUDIT_DIR)) {
285
- mkdirSync(process.env.QRING_AUDIT_DIR, { recursive: true });
404
+ if (!existsSync(process.env.QRING_AUDIT_DIR)) {
405
+ mkdirSync2(process.env.QRING_AUDIT_DIR, { recursive: true, mode: 448 });
286
406
  }
287
407
  return process.env.QRING_AUDIT_DIR;
288
408
  }
289
- const dir = join3(homedir(), ".config", "q-ring");
290
- if (!existsSync2(dir)) {
291
- mkdirSync(dir, { recursive: true });
409
+ const dir = join4(homedir2(), ".config", "q-ring");
410
+ if (!existsSync(dir)) {
411
+ mkdirSync2(dir, { recursive: true, mode: 448 });
292
412
  }
293
413
  return dir;
294
414
  }
295
415
  function getAuditPath() {
296
- return join3(getAuditDir(), "audit.jsonl");
416
+ return join4(getAuditDir(), "audit.jsonl");
297
417
  }
298
418
  function getLastLineHash() {
299
419
  const path = getAuditPath();
300
- if (!existsSync2(path)) return void 0;
420
+ if (!existsSync(path)) return void 0;
301
421
  try {
302
422
  const fd = openSync(path, "r");
303
423
  const stat = fstatSync(fd);
@@ -319,24 +439,38 @@ function getLastLineHash() {
319
439
  }
320
440
  }
321
441
  function logAudit(event) {
322
- const prevHash = getLastLineHash();
323
- const full = {
324
- ...event,
325
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
326
- pid: process.pid,
327
- prevHash
328
- };
329
442
  try {
330
- appendFileSync(getAuditPath(), JSON.stringify(full) + "\n");
443
+ withFileLock(
444
+ "audit-chain",
445
+ () => {
446
+ const prevHash = getLastLineHash();
447
+ const full = {
448
+ ...event,
449
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
450
+ pid: process.pid,
451
+ prevHash
452
+ };
453
+ const line = JSON.stringify(full);
454
+ const path = getAuditPath();
455
+ appendFileSync(path, line + "\n", { mode: 384 });
456
+ try {
457
+ chmodSync(path, 384);
458
+ } catch {
459
+ }
460
+ const key = getAuditKey();
461
+ if (key) writeStoredAnchor(headAnchor(line, key));
462
+ },
463
+ { timeoutMs: 5e3 }
464
+ );
331
465
  } catch {
332
466
  }
333
467
  }
334
468
  var MAX_AUDIT_BYTES = 12 * 1024 * 1024;
335
469
  function queryAudit(query = {}) {
336
470
  const path = getAuditPath();
337
- if (!existsSync2(path)) return [];
471
+ if (!existsSync(path)) return [];
338
472
  try {
339
- const st = statSync(path);
473
+ const st = statSync3(path);
340
474
  const readStart = st.size > MAX_AUDIT_BYTES ? st.size - MAX_AUDIT_BYTES : 0;
341
475
  const readLen = st.size > MAX_AUDIT_BYTES ? MAX_AUDIT_BYTES : st.size;
342
476
  const buf = Buffer.alloc(readLen);
@@ -375,10 +509,10 @@ function queryAudit(query = {}) {
375
509
  }
376
510
  function verifyAuditChain() {
377
511
  const path = getAuditPath();
378
- if (!existsSync2(path)) {
512
+ if (!existsSync(path)) {
379
513
  return { totalEvents: 0, validEvents: 0, intact: true };
380
514
  }
381
- const lines = readFileSync3(path, "utf8").split("\n").filter((l) => l.trim());
515
+ const lines = readFileSync4(path, "utf8").split("\n").filter((l) => l.trim());
382
516
  if (lines.length === 0) {
383
517
  return { totalEvents: 0, validEvents: 0, intact: true };
384
518
  }
@@ -411,12 +545,26 @@ function verifyAuditChain() {
411
545
  }
412
546
  validEvents++;
413
547
  }
548
+ const key = getAuditKey();
549
+ const anchor = key ? readStoredAnchor() : null;
550
+ if (key && anchor !== null) {
551
+ const head = headAnchor(lines[lines.length - 1], key);
552
+ if (head !== anchor) {
553
+ return {
554
+ totalEvents: lines.length,
555
+ validEvents,
556
+ brokenAt: lines.length - 1,
557
+ intact: false,
558
+ reason: "audit head does not match the keyed anchor in the OS keyring \u2014 the log was truncated or rewritten"
559
+ };
560
+ }
561
+ }
414
562
  return { totalEvents: lines.length, validEvents, intact: true };
415
563
  }
416
564
  function exportAudit(opts = {}) {
417
565
  const path = getAuditPath();
418
- if (!existsSync2(path)) return opts.format === "json" ? "[]" : "";
419
- const lines = readFileSync3(path, "utf8").split("\n").filter((l) => l.trim());
566
+ if (!existsSync(path)) return opts.format === "json" ? "[]" : "";
567
+ const lines = readFileSync4(path, "utf8").split("\n").filter((l) => l.trim());
420
568
  let events = lines.map((l) => {
421
569
  try {
422
570
  return JSON.parse(l);
@@ -481,48 +629,62 @@ function detectAnomalies(key) {
481
629
  }
482
630
 
483
631
  // src/core/entanglement.ts
484
- import { existsSync as existsSync3, readFileSync as readFileSync4, writeFileSync, mkdirSync as mkdirSync2 } from "fs";
485
- import { join as join4 } from "path";
486
- import { homedir as homedir2 } from "os";
632
+ import { existsSync as existsSync3, writeFileSync as writeFileSync2, mkdirSync as mkdirSync3 } from "fs";
633
+ import { join as join5 } from "path";
634
+ import { homedir as homedir3 } from "os";
635
+
636
+ // src/utils/registry.ts
637
+ import { existsSync as existsSync2, readFileSync as readFileSync5, renameSync } from "fs";
638
+ function loadJsonRegistry(path, empty) {
639
+ if (!existsSync2(path)) return empty;
640
+ const raw = readFileSync5(path, "utf8");
641
+ try {
642
+ return JSON.parse(raw);
643
+ } catch (err) {
644
+ const reason = err instanceof Error ? err.message : String(err);
645
+ const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
646
+ const backup = `${path}.corrupt-${stamp}`;
647
+ try {
648
+ renameSync(path, backup);
649
+ } catch {
650
+ throw new Error(
651
+ `q-ring: registry ${path} is corrupt (${reason}) and could not be moved aside \u2014 refusing to continue so a later write cannot overwrite it. Inspect or remove the file manually.`
652
+ );
653
+ }
654
+ console.error(
655
+ `q-ring: WARNING \u2014 registry ${path} was corrupt (${reason}); moved it to ${backup} and reinitialized from empty. Previous entries are preserved in the backup file.`
656
+ );
657
+ return empty;
658
+ }
659
+ }
660
+
661
+ // src/core/entanglement.ts
662
+ var REGISTRY_VERSION = 1;
487
663
  function getRegistryPath() {
488
- const dir = join4(homedir2(), ".config", "q-ring");
664
+ const dir = join5(homedir3(), ".config", "q-ring");
489
665
  if (!existsSync3(dir)) {
490
- mkdirSync2(dir, { recursive: true });
666
+ mkdirSync3(dir, { recursive: true, mode: 448 });
491
667
  }
492
- return join4(dir, "entanglement.json");
668
+ return join5(dir, "entanglement.json");
493
669
  }
494
670
  function loadRegistry() {
495
- const path = getRegistryPath();
496
- if (!existsSync3(path)) {
497
- return { pairs: [] };
498
- }
499
- try {
500
- return JSON.parse(readFileSync4(path, "utf8"));
501
- } catch {
502
- return { pairs: [] };
503
- }
671
+ return loadJsonRegistry(getRegistryPath(), { pairs: [] });
504
672
  }
505
673
  function saveRegistry(registry2) {
506
- writeFileSync(getRegistryPath(), JSON.stringify(registry2, null, 2), {
674
+ registry2.version = REGISTRY_VERSION;
675
+ writeFileSync2(getRegistryPath(), JSON.stringify(registry2, null, 2), {
507
676
  mode: 384
508
677
  });
509
678
  }
510
- function entangle(source, target) {
679
+ function entangle(source, target, createdBy) {
511
680
  const registry2 = loadRegistry();
512
681
  const exists = registry2.pairs.some(
513
682
  (p) => p.source.service === source.service && p.source.key === source.key && p.target.service === target.service && p.target.key === target.key
514
683
  );
515
684
  if (!exists) {
516
- registry2.pairs.push({
517
- source,
518
- target,
519
- createdAt: (/* @__PURE__ */ new Date()).toISOString()
520
- });
521
- registry2.pairs.push({
522
- source: target,
523
- target: source,
524
- createdAt: (/* @__PURE__ */ new Date()).toISOString()
525
- });
685
+ const createdAt = (/* @__PURE__ */ new Date()).toISOString();
686
+ registry2.pairs.push({ source, target, createdAt, createdBy });
687
+ registry2.pairs.push({ source: target, target: source, createdAt, createdBy });
526
688
  saveRegistry(registry2);
527
689
  }
528
690
  }
@@ -544,9 +706,9 @@ function listEntanglements() {
544
706
  }
545
707
 
546
708
  // src/core/hooks.ts
547
- import { existsSync as existsSync4, readFileSync as readFileSync5, writeFileSync as writeFileSync2, mkdirSync as mkdirSync3 } from "fs";
548
- import { join as join5 } from "path";
549
- import { homedir as homedir3 } from "os";
709
+ import { existsSync as existsSync4, writeFileSync as writeFileSync3, mkdirSync as mkdirSync4 } from "fs";
710
+ import { join as join6 } from "path";
711
+ import { homedir as homedir4 } from "os";
550
712
  import { execFile, spawn } from "child_process";
551
713
  import { randomUUID } from "crypto";
552
714
 
@@ -770,25 +932,17 @@ function httpRequest(opts) {
770
932
 
771
933
  // src/core/hooks.ts
772
934
  function getRegistryPath2() {
773
- const dir = join5(homedir3(), ".config", "q-ring");
935
+ const dir = join6(homedir4(), ".config", "q-ring");
774
936
  if (!existsSync4(dir)) {
775
- mkdirSync3(dir, { recursive: true });
937
+ mkdirSync4(dir, { recursive: true, mode: 448 });
776
938
  }
777
- return join5(dir, "hooks.json");
939
+ return join6(dir, "hooks.json");
778
940
  }
779
941
  function loadRegistry2() {
780
- const path = getRegistryPath2();
781
- if (!existsSync4(path)) {
782
- return { hooks: [] };
783
- }
784
- try {
785
- return JSON.parse(readFileSync5(path, "utf8"));
786
- } catch {
787
- return { hooks: [] };
788
- }
942
+ return loadJsonRegistry(getRegistryPath2(), { hooks: [] });
789
943
  }
790
944
  function saveRegistry2(registry2) {
791
- writeFileSync2(getRegistryPath2(), JSON.stringify(registry2, null, 2), {
945
+ writeFileSync3(getRegistryPath2(), JSON.stringify(registry2, null, 2), {
792
946
  mode: 384
793
947
  });
794
948
  }
@@ -1035,19 +1189,19 @@ async function fireHooks(payload, tags) {
1035
1189
  }
1036
1190
 
1037
1191
  // src/core/approval.ts
1038
- import { existsSync as existsSync5, readFileSync as readFileSync6, writeFileSync as writeFileSync3, mkdirSync as mkdirSync4 } from "fs";
1039
- import { join as join6 } from "path";
1040
- import { homedir as homedir4 } from "os";
1041
- import { createHmac, randomBytes, timingSafeEqual } from "crypto";
1192
+ import { existsSync as existsSync5, readFileSync as readFileSync6, writeFileSync as writeFileSync4, mkdirSync as mkdirSync5 } from "fs";
1193
+ import { join as join7 } from "path";
1194
+ import { homedir as homedir5 } from "os";
1195
+ import { createHmac as createHmac2, randomBytes as randomBytes2, timingSafeEqual } from "crypto";
1042
1196
  function getHmacSecret() {
1043
- const dir = join6(homedir4(), ".config", "q-ring");
1044
- const secretPath = join6(dir, ".approval-key");
1045
- if (!existsSync5(dir)) mkdirSync4(dir, { recursive: true, mode: 448 });
1197
+ const dir = join7(homedir5(), ".config", "q-ring");
1198
+ const secretPath = join7(dir, ".approval-key");
1199
+ if (!existsSync5(dir)) mkdirSync5(dir, { recursive: true, mode: 448 });
1046
1200
  if (existsSync5(secretPath)) {
1047
1201
  return readFileSync6(secretPath, "utf8").trim();
1048
1202
  }
1049
- const secret = randomBytes(32).toString("hex");
1050
- writeFileSync3(secretPath, secret, { mode: 384 });
1203
+ const secret = randomBytes2(32).toString("hex");
1204
+ writeFileSync4(secretPath, secret, { mode: 384 });
1051
1205
  return secret;
1052
1206
  }
1053
1207
  function computeHmac(entry) {
@@ -1055,6 +1209,7 @@ function computeHmac(entry) {
1055
1209
  entry.id,
1056
1210
  entry.key,
1057
1211
  entry.scope,
1212
+ entry.service ?? "",
1058
1213
  entry.reason,
1059
1214
  entry.grantedBy,
1060
1215
  entry.grantedAt,
@@ -1062,7 +1217,7 @@ function computeHmac(entry) {
1062
1217
  entry.workspace ?? "",
1063
1218
  entry.sessionId ?? ""
1064
1219
  ].join("|");
1065
- return createHmac("sha256", getHmacSecret()).update(payload).digest("hex");
1220
+ return createHmac2("sha256", getHmacSecret()).update(payload).digest("hex");
1066
1221
  }
1067
1222
  function verifyHmac(entry) {
1068
1223
  const expected = computeHmac(entry);
@@ -1076,27 +1231,19 @@ function verifyHmac(entry) {
1076
1231
  }
1077
1232
  }
1078
1233
  function getRegistryPath3() {
1079
- const dir = join6(homedir4(), ".config", "q-ring");
1234
+ const dir = join7(homedir5(), ".config", "q-ring");
1080
1235
  if (!existsSync5(dir)) {
1081
- mkdirSync4(dir, { recursive: true, mode: 448 });
1236
+ mkdirSync5(dir, { recursive: true, mode: 448 });
1082
1237
  }
1083
- return join6(dir, "approvals.json");
1238
+ return join7(dir, "approvals.json");
1084
1239
  }
1085
1240
  function loadRegistry3() {
1086
- const path = getRegistryPath3();
1087
- if (!existsSync5(path)) {
1088
- return { approvals: [] };
1089
- }
1090
- try {
1091
- return JSON.parse(readFileSync6(path, "utf8"));
1092
- } catch {
1093
- return { approvals: [] };
1094
- }
1241
+ return loadJsonRegistry(getRegistryPath3(), { approvals: [] });
1095
1242
  }
1096
- function hasApproval(key, scope) {
1243
+ function hasApproval(key, scope, service) {
1097
1244
  const registry2 = loadRegistry3();
1098
1245
  const entry = registry2.approvals.find(
1099
- (a) => a.key === key && a.scope === scope
1246
+ (a) => a.key === key && a.scope === scope && a.service === service
1100
1247
  );
1101
1248
  if (!entry) return false;
1102
1249
  if (new Date(entry.expiresAt).getTime() < Date.now()) return false;
@@ -1114,20 +1261,54 @@ function listApprovals() {
1114
1261
  }
1115
1262
 
1116
1263
  // src/core/policy.ts
1117
- import { statSync as statSync2 } from "fs";
1118
- import { join as join7 } from "path";
1264
+ import { statSync as statSync4 } from "fs";
1265
+ import { join as join8 } from "path";
1266
+ import { z as z2 } from "zod";
1267
+ var stringArray = z2.array(z2.string());
1268
+ var mcpPolicySchema = z2.object({
1269
+ allowTools: stringArray.optional(),
1270
+ denyTools: stringArray.optional(),
1271
+ readableKeys: stringArray.optional(),
1272
+ deniedKeys: stringArray.optional(),
1273
+ deniedTags: stringArray.optional()
1274
+ }).strict();
1275
+ var execPolicySchema = z2.object({
1276
+ allowCommands: stringArray.optional(),
1277
+ denyCommands: stringArray.optional(),
1278
+ maxRuntimeSeconds: z2.number().optional(),
1279
+ allowNetwork: z2.boolean().optional()
1280
+ }).strict();
1281
+ var secretsPolicySchema = z2.object({
1282
+ requireApprovalForTags: stringArray.optional(),
1283
+ requireRotationFormatForTags: stringArray.optional(),
1284
+ maxTtlSeconds: z2.number().optional()
1285
+ }).strict();
1286
+ var policySchema = z2.object({
1287
+ mcp: mcpPolicySchema.optional(),
1288
+ exec: execPolicySchema.optional(),
1289
+ secrets: secretsPolicySchema.optional()
1290
+ }).strict();
1291
+ var PolicyConfigError = class extends Error {
1292
+ constructor(message) {
1293
+ super(message);
1294
+ this.name = "PolicyConfigError";
1295
+ }
1296
+ };
1119
1297
  var cachedPolicy = null;
1120
1298
  var policyRoot = null;
1121
1299
  function setPolicyRoot(root) {
1122
1300
  policyRoot = root;
1123
1301
  cachedPolicy = null;
1124
1302
  }
1303
+ function getPolicyRoot() {
1304
+ return policyRoot;
1305
+ }
1125
1306
  function resolvePolicyPath(projectPath) {
1126
1307
  return policyRoot ?? projectPath ?? process.cwd();
1127
1308
  }
1128
1309
  function configMtime(pp) {
1129
1310
  try {
1130
- return statSync2(join7(pp, ".q-ring.json")).mtimeMs;
1311
+ return statSync4(join8(pp, ".q-ring.json")).mtimeMs;
1131
1312
  } catch {
1132
1313
  return 0;
1133
1314
  }
@@ -1136,12 +1317,28 @@ function loadPolicy(projectPath) {
1136
1317
  const pp = resolvePolicyPath(projectPath);
1137
1318
  const mtimeMs = configMtime(pp);
1138
1319
  if (cachedPolicy && cachedPolicy.path === pp && cachedPolicy.mtimeMs === mtimeMs) {
1320
+ if (cachedPolicy.error) throw cachedPolicy.error;
1139
1321
  return cachedPolicy.policy;
1140
1322
  }
1141
1323
  const config = readProjectConfig(pp);
1142
- const policy = config?.policy ?? {};
1143
- cachedPolicy = { path: pp, mtimeMs, policy };
1144
- return policy;
1324
+ const rawPolicy = config?.policy;
1325
+ if (rawPolicy === void 0 || rawPolicy === null) {
1326
+ const policy = {};
1327
+ cachedPolicy = { path: pp, mtimeMs, policy };
1328
+ return policy;
1329
+ }
1330
+ const parsed = policySchema.safeParse(rawPolicy);
1331
+ if (!parsed.success) {
1332
+ const issues = parsed.error.issues.map((i) => `policy${i.path.length ? "." + i.path.join(".") : ""}: ${i.message}`).join("; ");
1333
+ const error = new PolicyConfigError(
1334
+ `Invalid policy in ${join8(pp, ".q-ring.json")} \u2014 refusing to run under an unparseable security policy (fail closed). Fix these and retry: ${issues}`
1335
+ );
1336
+ console.error(`q-ring: ${error.message}`);
1337
+ cachedPolicy = { path: pp, mtimeMs, error };
1338
+ throw error;
1339
+ }
1340
+ cachedPolicy = { path: pp, mtimeMs, policy: parsed.data };
1341
+ return parsed.data;
1145
1342
  }
1146
1343
  function checkToolPolicy(toolName, projectPath) {
1147
1344
  const policy = loadPolicy(projectPath);
@@ -1270,10 +1467,7 @@ function getPolicySummary(projectPath) {
1270
1467
  }
1271
1468
 
1272
1469
  // src/core/keyring.ts
1273
- import { mkdirSync as mkdirSync5, writeFileSync as writeFileSync4, unlinkSync } from "fs";
1274
- import { homedir as homedir5 } from "os";
1275
- import { join as join8 } from "path";
1276
- import { Entry, findCredentials } from "@napi-rs/keyring";
1470
+ import { Entry as Entry2, findCredentials } from "@napi-rs/keyring";
1277
1471
 
1278
1472
  // src/utils/hash.ts
1279
1473
  import { createHash as createHash2 } from "crypto";
@@ -1326,22 +1520,25 @@ function resolveScope(opts) {
1326
1520
  chain.push({ scope: "global", service: globalService() });
1327
1521
  return chain;
1328
1522
  }
1523
+ function serviceForScope(scope, opts = {}) {
1524
+ return resolveScope({ ...opts, scope })[0].service;
1525
+ }
1329
1526
 
1330
1527
  // src/core/provision.ts
1331
1528
  import { execFileSync, spawnSync } from "child_process";
1332
- import { z as z2 } from "zod";
1333
- var AwsStsConfigSchema = z2.object({
1334
- roleArn: z2.string(),
1335
- sessionName: z2.string().optional(),
1336
- durationSeconds: z2.number().optional()
1529
+ import { z as z3 } from "zod";
1530
+ var AwsStsConfigSchema = z3.object({
1531
+ roleArn: z3.string(),
1532
+ sessionName: z3.string().optional(),
1533
+ durationSeconds: z3.number().optional()
1337
1534
  });
1338
- var HttpJitConfigSchema = z2.object({
1339
- url: z2.string(),
1340
- method: z2.string().optional(),
1341
- valuePath: z2.string().optional(),
1342
- expiresInSeconds: z2.number().optional(),
1343
- headers: z2.record(z2.string(), z2.string()).optional(),
1344
- body: z2.unknown().optional()
1535
+ var HttpJitConfigSchema = z3.object({
1536
+ url: z3.string(),
1537
+ method: z3.string().optional(),
1538
+ valuePath: z3.string().optional(),
1539
+ expiresInSeconds: z3.number().optional(),
1540
+ headers: z3.record(z3.string(), z3.string()).optional(),
1541
+ body: z3.unknown().optional()
1345
1542
  });
1346
1543
  var ProvisionRegistry = class {
1347
1544
  providers = /* @__PURE__ */ new Map();
@@ -1476,40 +1673,17 @@ registry.register(httpProvider);
1476
1673
 
1477
1674
  // src/core/keyring.ts
1478
1675
  function withJitEnvelopeLock(service, key, fn) {
1479
- const dir = join8(homedir5(), ".config", "q-ring", "jit-locks");
1480
- mkdirSync5(dir, { recursive: true });
1481
- const safe = Buffer.from(`${service}\0${key}`, "utf8").toString("base64url");
1482
- const lockPath = join8(dir, `${safe}.lock`);
1483
- const deadline = Date.now() + 8e3;
1484
- while (Date.now() < deadline) {
1485
- try {
1486
- writeFileSync4(lockPath, `${process.pid}
1487
- `, { flag: "wx", mode: 384 });
1488
- try {
1489
- return fn();
1490
- } finally {
1491
- try {
1492
- unlinkSync(lockPath);
1493
- } catch {
1494
- }
1495
- }
1496
- } catch {
1497
- const start = Date.now();
1498
- while (Date.now() - start < 15) {
1499
- }
1500
- }
1501
- }
1502
- throw new Error("Could not acquire JIT envelope lock (timeout)");
1676
+ return withFileLock(`${service}\0${key}`, fn, { dir: "jit-locks" });
1503
1677
  }
1504
1678
  function readEnvelope(service, key) {
1505
- const entry = new Entry(service, key);
1679
+ const entry = new Entry2(service, key);
1506
1680
  const raw = entry.getPassword();
1507
1681
  if (raw === null) return null;
1508
1682
  const envelope = parseEnvelope(raw);
1509
1683
  return envelope ?? wrapLegacy(raw);
1510
1684
  }
1511
1685
  function writeEnvelope(service, key, envelope) {
1512
- const entry = new Entry(service, key);
1686
+ const entry = new Entry2(service, key);
1513
1687
  entry.setPassword(serializeEnvelope(envelope));
1514
1688
  }
1515
1689
  function resolveEnv(opts) {
@@ -1577,7 +1751,7 @@ function getSecret(key, opts = {}) {
1577
1751
  continue;
1578
1752
  }
1579
1753
  if (envelope.meta.requiresApproval && source === "mcp") {
1580
- if (!hasApproval(key, scope)) {
1754
+ if (!hasApproval(key, scope, service)) {
1581
1755
  if (!opts.silent) {
1582
1756
  logAudit({
1583
1757
  action: "read",
@@ -1627,8 +1801,8 @@ function getSecret(key, opts = {}) {
1627
1801
  }
1628
1802
  value = resolveTemplates(value, { ...opts, _seen: nextSeen }, nextSeen);
1629
1803
  if (!opts.silent) {
1630
- const updated = recordAccess(envelope);
1631
- writeEnvelope(service, key, updated);
1804
+ const latest = readEnvelope(service, key) ?? envelope;
1805
+ writeEnvelope(service, key, recordAccess(latest));
1632
1806
  logAudit({ action: "read", key, scope, env, source });
1633
1807
  }
1634
1808
  return value;
@@ -1714,6 +1888,19 @@ function setSecret(key, value, opts = {}) {
1714
1888
  const entangled = findEntangled({ service, key });
1715
1889
  for (const target of entangled) {
1716
1890
  try {
1891
+ if (source === "mcp") {
1892
+ const decision = checkKeyReadPolicy(target.key, void 0, opts.projectPath);
1893
+ if (!decision.allowed) {
1894
+ logAudit({
1895
+ action: "entangle",
1896
+ key: target.key,
1897
+ scope: "global",
1898
+ source,
1899
+ detail: `blocked propagation from ${key}: ${decision.reason}`
1900
+ });
1901
+ continue;
1902
+ }
1903
+ }
1717
1904
  const targetEnvelope = readEnvelope(target.service, target.key);
1718
1905
  if (targetEnvelope) {
1719
1906
  if (opts.states) {
@@ -1754,7 +1941,7 @@ function deleteSecret(key, opts = {}) {
1754
1941
  }
1755
1942
  let deleted = false;
1756
1943
  for (const { service, scope } of scopes) {
1757
- const entry = new Entry(service, key);
1944
+ const entry = new Entry2(service, key);
1758
1945
  try {
1759
1946
  if (entry.deleteCredential()) {
1760
1947
  deleted = true;
@@ -1863,7 +2050,7 @@ function exportSecrets(opts = {}) {
1863
2050
  if (entry.envelope) {
1864
2051
  const decay = checkDecay(entry.envelope);
1865
2052
  if (decay.isExpired) continue;
1866
- if (source === "mcp" && entry.envelope.meta.requiresApproval && !hasApproval(entry.key, entry.scope)) {
2053
+ if (source === "mcp" && entry.envelope.meta.requiresApproval && !hasApproval(entry.key, entry.scope, serviceForScope(entry.scope, opts))) {
1867
2054
  logAudit({
1868
2055
  action: "read",
1869
2056
  key: entry.key,
@@ -1908,7 +2095,10 @@ function entangleSecrets(sourceKey, sourceOpts, targetKey, targetOpts) {
1908
2095
  const targetScopes = resolveScope({ ...targetOpts, scope: targetOpts.scope ?? "global" });
1909
2096
  const source = { service: sourceScopes[0].service, key: sourceKey };
1910
2097
  const target = { service: targetScopes[0].service, key: targetKey };
1911
- entangle(source, target);
2098
+ entangle(source, target, {
2099
+ source: sourceOpts.source ?? "cli",
2100
+ policyRoot: getPolicyRoot()
2101
+ });
1912
2102
  logAudit({
1913
2103
  action: "entangle",
1914
2104
  key: sourceKey,
@@ -1931,7 +2121,7 @@ function disentangleSecrets(sourceKey, sourceOpts, targetKey, targetOpts) {
1931
2121
  }
1932
2122
 
1933
2123
  // src/core/tunnel.ts
1934
- import { randomBytes as randomBytes2 } from "crypto";
2124
+ import { randomBytes as randomBytes3 } from "crypto";
1935
2125
  var tunnelStore = /* @__PURE__ */ new Map();
1936
2126
  var cleanupInterval = null;
1937
2127
  function ensureCleanup() {
@@ -1953,7 +2143,7 @@ function ensureCleanup() {
1953
2143
  }
1954
2144
  }
1955
2145
  function tunnelCreate(value, opts = {}) {
1956
- const id = `tun_${Date.now().toString(36)}_${randomBytes2(6).toString("base64url")}`;
2146
+ const id = `tun_${Date.now().toString(36)}_${randomBytes3(6).toString("base64url")}`;
1957
2147
  const now = Date.now();
1958
2148
  tunnelStore.set(id, {
1959
2149
  value,
@@ -2003,43 +2193,72 @@ function tunnelList() {
2003
2193
  }
2004
2194
 
2005
2195
  // src/core/memory.ts
2006
- import { existsSync as existsSync6, readFileSync as readFileSync7, writeFileSync as writeFileSync5, mkdirSync as mkdirSync6 } from "fs";
2196
+ import { existsSync as existsSync6, readFileSync as readFileSync7, writeFileSync as writeFileSync5, mkdirSync as mkdirSync6, chmodSync as chmodSync2 } from "fs";
2007
2197
  import { join as join9 } from "path";
2008
2198
  import { homedir as homedir6, hostname, userInfo } from "os";
2009
- import { createCipheriv, createDecipheriv, createHash as createHash3, randomBytes as randomBytes3 } from "crypto";
2010
- import { Entry as Entry2 } from "@napi-rs/keyring";
2199
+ import {
2200
+ createCipheriv,
2201
+ createDecipheriv,
2202
+ createHash as createHash3,
2203
+ randomBytes as randomBytes4,
2204
+ pbkdf2Sync
2205
+ } from "crypto";
2206
+ import { Entry as Entry3 } from "@napi-rs/keyring";
2011
2207
  var MEMORY_FILE = "agent-memory.enc";
2012
2208
  var KEYRING_SERVICE = "qring-memory-key";
2013
2209
  var KEYRING_ACCOUNT = "encryption-key";
2014
2210
  function getMemoryDir() {
2015
2211
  const dir = join9(homedir6(), ".config", "q-ring");
2016
2212
  if (!existsSync6(dir)) {
2017
- mkdirSync6(dir, { recursive: true });
2213
+ mkdirSync6(dir, { recursive: true, mode: 448 });
2018
2214
  }
2019
2215
  return dir;
2020
2216
  }
2217
+ function writeMemoryFile(path, data) {
2218
+ writeFileSync5(path, data, { mode: 384 });
2219
+ try {
2220
+ chmodSync2(path, 384);
2221
+ } catch {
2222
+ }
2223
+ }
2021
2224
  function getMemoryPath() {
2022
2225
  return join9(getMemoryDir(), MEMORY_FILE);
2023
2226
  }
2227
+ var PBKDF2_ITERATIONS = 21e4;
2228
+ var KEY_LENGTH = 32;
2229
+ var PASSPHRASE_ENV = "QRING_MEMORY_PASSPHRASE";
2230
+ var V2_PREFIX = "qmem2";
2231
+ var MemoryKeyUnavailableError = class extends Error {
2232
+ constructor(message) {
2233
+ super(message);
2234
+ this.name = "MemoryKeyUnavailableError";
2235
+ }
2236
+ };
2024
2237
  function deriveLegacyKey() {
2025
2238
  const fingerprint = `qring-memory:${hostname()}:${userInfo().username}`;
2026
2239
  return createHash3("sha256").update(fingerprint).digest();
2027
2240
  }
2028
- function getOrCreateKey() {
2241
+ function passphrase() {
2242
+ const p = process.env[PASSPHRASE_ENV];
2243
+ return p && p.length > 0 ? p : void 0;
2244
+ }
2245
+ function derivePassphraseKey(salt) {
2246
+ return pbkdf2Sync(passphrase(), salt, PBKDF2_ITERATIONS, KEY_LENGTH, "sha512");
2247
+ }
2248
+ function keyringKey() {
2029
2249
  try {
2030
- const entry = new Entry2(KEYRING_SERVICE, KEYRING_ACCOUNT);
2250
+ const entry = new Entry3(KEYRING_SERVICE, KEYRING_ACCOUNT);
2031
2251
  const stored = entry.getPassword();
2032
2252
  if (stored) return Buffer.from(stored, "base64");
2033
- const key = randomBytes3(32);
2253
+ const key = randomBytes4(KEY_LENGTH);
2034
2254
  entry.setPassword(key.toString("base64"));
2035
2255
  return key;
2036
2256
  } catch {
2037
- console.warn("[q-ring] OS keyring unavailable for memory key \u2014 falling back to machine-derived key");
2038
- return deriveLegacyKey();
2257
+ return null;
2039
2258
  }
2040
2259
  }
2041
2260
  function encryptWith(data, key) {
2042
- const iv = randomBytes3(12);
2261
+ const iv = randomBytes4(12);
2043
2262
  const cipher = createCipheriv("aes-256-gcm", key, iv);
2044
2263
  const encrypted = Buffer.concat([cipher.update(data, "utf8"), cipher.final()]);
2045
2264
  const tag = cipher.getAuthTag();
@@ -2056,18 +2275,44 @@ function decryptWith(blob, key) {
2056
2275
  return decipher.update(encrypted) + decipher.final("utf8");
2057
2276
  }
2058
2277
  function encrypt(data) {
2059
- return encryptWith(data, getOrCreateKey());
2278
+ const kk = keyringKey();
2279
+ if (kk) return encryptWith(data, kk);
2280
+ if (passphrase()) {
2281
+ const salt = randomBytes4(16);
2282
+ const key = derivePassphraseKey(salt);
2283
+ return `${V2_PREFIX}:${salt.toString("base64")}:${encryptWith(data, key)}`;
2284
+ }
2285
+ throw new MemoryKeyUnavailableError(
2286
+ `Cannot persist agent memory: the OS keyring is unavailable and ${PASSPHRASE_ENV} is not set. Refusing to encrypt with a machine-derivable key (any local process could recompute it and read your memory). Set ${PASSPHRASE_ENV} to a strong passphrase, or run on a host with an OS keyring.`
2287
+ );
2060
2288
  }
2061
2289
  function decrypt(blob) {
2062
- const key = getOrCreateKey();
2063
- try {
2064
- return decryptWith(blob, key);
2065
- } catch {
2066
- const legacy = deriveLegacyKey();
2067
- const plain = decryptWith(blob, legacy);
2068
- writeFileSync5(getMemoryPath(), encryptWith(plain, key), "utf8");
2069
- return plain;
2290
+ if (blob.startsWith(`${V2_PREFIX}:`)) {
2291
+ if (!passphrase()) {
2292
+ throw new MemoryKeyUnavailableError(
2293
+ `Agent memory was encrypted with ${PASSPHRASE_ENV} but it is not set \u2014 cannot decrypt.`
2294
+ );
2295
+ }
2296
+ const rest = blob.slice(V2_PREFIX.length + 1);
2297
+ const sep = rest.indexOf(":");
2298
+ const salt = Buffer.from(rest.slice(0, sep), "base64");
2299
+ return decryptWith(rest.slice(sep + 1), derivePassphraseKey(salt));
2070
2300
  }
2301
+ const kk = keyringKey();
2302
+ if (kk) {
2303
+ try {
2304
+ return decryptWith(blob, kk);
2305
+ } catch {
2306
+ }
2307
+ }
2308
+ const plain = decryptWith(blob, deriveLegacyKey());
2309
+ if (kk) {
2310
+ try {
2311
+ writeMemoryFile(getMemoryPath(), encryptWith(plain, kk));
2312
+ } catch {
2313
+ }
2314
+ }
2315
+ return plain;
2071
2316
  }
2072
2317
  function loadStore() {
2073
2318
  const path = getMemoryPath();
@@ -2078,14 +2323,17 @@ function loadStore() {
2078
2323
  const raw = readFileSync7(path, "utf8");
2079
2324
  const decrypted = decrypt(raw);
2080
2325
  return JSON.parse(decrypted);
2081
- } catch {
2326
+ } catch (err) {
2327
+ if (err instanceof MemoryKeyUnavailableError) {
2328
+ console.error(`q-ring: ${err.message}`);
2329
+ }
2082
2330
  return { entries: {} };
2083
2331
  }
2084
2332
  }
2085
2333
  function saveStore(store) {
2086
2334
  const json = JSON.stringify(store);
2087
2335
  const encrypted = encrypt(json);
2088
- writeFileSync5(getMemoryPath(), encrypted, "utf8");
2336
+ writeMemoryFile(getMemoryPath(), encrypted);
2089
2337
  }
2090
2338
  function remember(key, value) {
2091
2339
  const store = loadStore();
@@ -2159,4 +2407,4 @@ export {
2159
2407
  listMemory,
2160
2408
  forget
2161
2409
  };
2162
- //# sourceMappingURL=chunk-Q3UCKJAJ.js.map
2410
+ //# sourceMappingURL=chunk-NNIEXAW5.js.map