@i4ctime/q-ring 0.10.1 → 0.11.5

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.
@@ -1,5 +1,22 @@
1
1
  #!/usr/bin/env node
2
2
 
3
+ // src/version.ts
4
+ import { readFileSync } from "fs";
5
+ import { dirname, join } from "path";
6
+ import { fileURLToPath } from "url";
7
+ function readPackageVersion() {
8
+ try {
9
+ const here = dirname(fileURLToPath(import.meta.url));
10
+ const pkgPath = join(here, "..", "package.json");
11
+ const raw = readFileSync(pkgPath, "utf8");
12
+ const pkg = JSON.parse(raw);
13
+ return typeof pkg.version === "string" ? pkg.version : "0.0.0";
14
+ } catch {
15
+ return "0.0.0";
16
+ }
17
+ }
18
+ var PACKAGE_VERSION = readPackageVersion();
19
+
3
20
  // src/core/envelope.ts
4
21
  import { z } from "zod";
5
22
  var EntanglementLinkSchema = z.object({
@@ -162,8 +179,8 @@ function recordAccess(envelope) {
162
179
 
163
180
  // src/core/collapse.ts
164
181
  import { execSync } from "child_process";
165
- import { existsSync, readFileSync } from "fs";
166
- import { join } from "path";
182
+ import { existsSync, readFileSync as readFileSync2 } from "fs";
183
+ import { join as join2 } from "path";
167
184
  var BRANCH_ENV_MAP = {
168
185
  main: "prod",
169
186
  master: "prod",
@@ -190,10 +207,10 @@ function detectGitBranch(cwd) {
190
207
  }
191
208
  }
192
209
  function readProjectConfig(projectPath) {
193
- const configPath = join(projectPath ?? process.cwd(), ".q-ring.json");
210
+ const configPath = join2(projectPath ?? process.cwd(), ".q-ring.json");
194
211
  try {
195
212
  if (existsSync(configPath)) {
196
- return JSON.parse(readFileSync(configPath, "utf8"));
213
+ return JSON.parse(readFileSync2(configPath, "utf8"));
197
214
  }
198
215
  } catch {
199
216
  }
@@ -254,14 +271,14 @@ import {
254
271
  existsSync as existsSync2,
255
272
  mkdirSync,
256
273
  appendFileSync,
257
- readFileSync as readFileSync2,
274
+ readFileSync as readFileSync3,
258
275
  openSync,
259
276
  fstatSync,
260
277
  readSync,
261
278
  closeSync,
262
279
  statSync
263
280
  } from "fs";
264
- import { join as join2 } from "path";
281
+ import { join as join3 } from "path";
265
282
  import { homedir } from "os";
266
283
  import { createHash } from "crypto";
267
284
  function getAuditDir() {
@@ -271,14 +288,14 @@ function getAuditDir() {
271
288
  }
272
289
  return process.env.QRING_AUDIT_DIR;
273
290
  }
274
- const dir = join2(homedir(), ".config", "q-ring");
291
+ const dir = join3(homedir(), ".config", "q-ring");
275
292
  if (!existsSync2(dir)) {
276
293
  mkdirSync(dir, { recursive: true });
277
294
  }
278
295
  return dir;
279
296
  }
280
297
  function getAuditPath() {
281
- return join2(getAuditDir(), "audit.jsonl");
298
+ return join3(getAuditDir(), "audit.jsonl");
282
299
  }
283
300
  function getLastLineHash() {
284
301
  const path = getAuditPath();
@@ -363,7 +380,7 @@ function verifyAuditChain() {
363
380
  if (!existsSync2(path)) {
364
381
  return { totalEvents: 0, validEvents: 0, intact: true };
365
382
  }
366
- const lines = readFileSync2(path, "utf8").split("\n").filter((l) => l.trim());
383
+ const lines = readFileSync3(path, "utf8").split("\n").filter((l) => l.trim());
367
384
  if (lines.length === 0) {
368
385
  return { totalEvents: 0, validEvents: 0, intact: true };
369
386
  }
@@ -401,7 +418,7 @@ function verifyAuditChain() {
401
418
  function exportAudit(opts = {}) {
402
419
  const path = getAuditPath();
403
420
  if (!existsSync2(path)) return opts.format === "json" ? "[]" : "";
404
- const lines = readFileSync2(path, "utf8").split("\n").filter((l) => l.trim());
421
+ const lines = readFileSync3(path, "utf8").split("\n").filter((l) => l.trim());
405
422
  let events = lines.map((l) => {
406
423
  try {
407
424
  return JSON.parse(l);
@@ -466,15 +483,15 @@ function detectAnomalies(key) {
466
483
  }
467
484
 
468
485
  // src/core/entanglement.ts
469
- import { existsSync as existsSync3, readFileSync as readFileSync3, writeFileSync, mkdirSync as mkdirSync2 } from "fs";
470
- import { join as join3 } from "path";
486
+ import { existsSync as existsSync3, readFileSync as readFileSync4, writeFileSync, mkdirSync as mkdirSync2 } from "fs";
487
+ import { join as join4 } from "path";
471
488
  import { homedir as homedir2 } from "os";
472
489
  function getRegistryPath() {
473
- const dir = join3(homedir2(), ".config", "q-ring");
490
+ const dir = join4(homedir2(), ".config", "q-ring");
474
491
  if (!existsSync3(dir)) {
475
492
  mkdirSync2(dir, { recursive: true });
476
493
  }
477
- return join3(dir, "entanglement.json");
494
+ return join4(dir, "entanglement.json");
478
495
  }
479
496
  function loadRegistry() {
480
497
  const path = getRegistryPath();
@@ -482,7 +499,7 @@ function loadRegistry() {
482
499
  return { pairs: [] };
483
500
  }
484
501
  try {
485
- return JSON.parse(readFileSync3(path, "utf8"));
502
+ return JSON.parse(readFileSync4(path, "utf8"));
486
503
  } catch {
487
504
  return { pairs: [] };
488
505
  }
@@ -528,67 +545,9 @@ function listEntanglements() {
528
545
  return loadRegistry().pairs;
529
546
  }
530
547
 
531
- // src/core/keyring.ts
532
- import { mkdirSync as mkdirSync5, writeFileSync as writeFileSync4, unlinkSync } from "fs";
533
- import { homedir as homedir5 } from "os";
534
- import { join as join6 } from "path";
535
- import { Entry, findCredentials } from "@napi-rs/keyring";
536
-
537
- // src/utils/hash.ts
538
- import { createHash as createHash2 } from "crypto";
539
- function hashProjectPath(projectPath) {
540
- return createHash2("sha256").update(projectPath).digest("hex").slice(0, 12);
541
- }
542
-
543
- // src/core/scope.ts
544
- var SERVICE_PREFIX = "q-ring";
545
- function globalService() {
546
- return `${SERVICE_PREFIX}:global`;
547
- }
548
- function projectService(projectPath) {
549
- const hash = hashProjectPath(projectPath);
550
- return `${SERVICE_PREFIX}:project:${hash}`;
551
- }
552
- function teamService(teamId) {
553
- return `${SERVICE_PREFIX}:team:${teamId}`;
554
- }
555
- function orgService(orgId) {
556
- return `${SERVICE_PREFIX}:org:${orgId}`;
557
- }
558
- function resolveScope(opts) {
559
- const { scope, projectPath, teamId, orgId } = opts;
560
- if (scope === "global") {
561
- return [{ scope: "global", service: globalService() }];
562
- }
563
- if (scope === "project") {
564
- if (!projectPath) throw new Error("Project path is required for project scope");
565
- return [{ scope: "project", service: projectService(projectPath), projectPath }];
566
- }
567
- if (scope === "team") {
568
- if (!teamId) throw new Error("Team ID is required for team scope");
569
- return [{ scope: "team", service: teamService(teamId), teamId }];
570
- }
571
- if (scope === "org") {
572
- if (!orgId) throw new Error("Org ID is required for org scope");
573
- return [{ scope: "org", service: orgService(orgId), orgId }];
574
- }
575
- const chain = [];
576
- if (projectPath) {
577
- chain.push({ scope: "project", service: projectService(projectPath), projectPath });
578
- }
579
- if (teamId) {
580
- chain.push({ scope: "team", service: teamService(teamId), teamId });
581
- }
582
- if (orgId) {
583
- chain.push({ scope: "org", service: orgService(orgId), orgId });
584
- }
585
- chain.push({ scope: "global", service: globalService() });
586
- return chain;
587
- }
588
-
589
548
  // src/core/hooks.ts
590
- import { existsSync as existsSync4, readFileSync as readFileSync4, writeFileSync as writeFileSync2, mkdirSync as mkdirSync3 } from "fs";
591
- import { join as join4 } from "path";
549
+ import { existsSync as existsSync4, readFileSync as readFileSync5, writeFileSync as writeFileSync2, mkdirSync as mkdirSync3 } from "fs";
550
+ import { join as join5 } from "path";
592
551
  import { homedir as homedir3 } from "os";
593
552
  import { execFile, spawn } from "child_process";
594
553
  import { randomUUID } from "crypto";
@@ -678,16 +637,16 @@ function httpRequest(opts) {
678
637
  import { lookup } from "dns/promises";
679
638
  import * as dns from "dns";
680
639
  import { isIPv4, isIPv6 } from "net";
681
- function lookupAddressesSync(hostname) {
640
+ function lookupAddressesSync(hostname2) {
682
641
  const lookupSync2 = dns.lookupSync;
683
- return lookupSync2(hostname, { all: true });
642
+ return lookupSync2(hostname2, { all: true });
684
643
  }
685
- function isHostnameIpLiteral(hostname) {
686
- if (isIPv4(hostname)) return true;
687
- if (hostname.startsWith("[") && hostname.endsWith("]")) {
688
- return isIPv6(hostname.slice(1, -1));
644
+ function isHostnameIpLiteral(hostname2) {
645
+ if (isIPv4(hostname2)) return true;
646
+ if (hostname2.startsWith("[") && hostname2.endsWith("]")) {
647
+ return isIPv6(hostname2.slice(1, -1));
689
648
  }
690
- return isIPv6(hostname);
649
+ return isIPv6(hostname2);
691
650
  }
692
651
  function isPrivateIP(ip) {
693
652
  const octet = "(?:25[0-5]|2[0-4]\\d|1?\\d{1,2})";
@@ -709,14 +668,14 @@ async function checkSSRF(url) {
709
668
  if (process.env.Q_RING_ALLOW_PRIVATE_HOOKS === "1") return null;
710
669
  try {
711
670
  const parsed = new URL(url);
712
- const hostname = parsed.hostname.replace(/^\[|\]$/g, "");
713
- if (isPrivateIP(hostname)) {
714
- return `Blocked: URL resolves to private address (${hostname}). Set Q_RING_ALLOW_PRIVATE_HOOKS=1 to override.`;
671
+ const hostname2 = parsed.hostname.replace(/^\[|\]$/g, "");
672
+ if (isPrivateIP(hostname2)) {
673
+ return `Blocked: URL resolves to private address (${hostname2}). Set Q_RING_ALLOW_PRIVATE_HOOKS=1 to override.`;
715
674
  }
716
- const results = await lookup(hostname, { all: true });
675
+ const results = await lookup(hostname2, { all: true });
717
676
  for (const { address } of results) {
718
677
  if (isPrivateIP(address)) {
719
- return `Blocked: URL "${hostname}" resolves to private address ${address}. Set Q_RING_ALLOW_PRIVATE_HOOKS=1 to override.`;
678
+ return `Blocked: URL "${hostname2}" resolves to private address ${address}. Set Q_RING_ALLOW_PRIVATE_HOOKS=1 to override.`;
720
679
  }
721
680
  }
722
681
  } catch {
@@ -730,28 +689,28 @@ function checkJitHttpProvisionUrl(url) {
730
689
  if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
731
690
  return "Blocked: JIT HTTP provider only allows http: or https: URLs.";
732
691
  }
733
- const hostname = parsed.hostname.replace(/^\[|\]$/g, "");
734
- if (!hostname) {
692
+ const hostname2 = parsed.hostname.replace(/^\[|\]$/g, "");
693
+ if (!hostname2) {
735
694
  return "Blocked: empty hostname in JIT URL.";
736
695
  }
737
- if (isPrivateIP(hostname)) {
738
- return `Blocked: URL resolves to private address (${hostname}). Set Q_RING_ALLOW_PRIVATE_HOOKS=1 to override.`;
696
+ if (isPrivateIP(hostname2)) {
697
+ return `Blocked: URL resolves to private address (${hostname2}). Set Q_RING_ALLOW_PRIVATE_HOOKS=1 to override.`;
739
698
  }
740
- if (isHostnameIpLiteral(hostname)) {
699
+ if (isHostnameIpLiteral(hostname2)) {
741
700
  return null;
742
701
  }
743
702
  let results;
744
703
  try {
745
- results = lookupAddressesSync(hostname);
704
+ results = lookupAddressesSync(hostname2);
746
705
  } catch {
747
- return `Blocked: DNS resolution failed for "${hostname}" (JIT HTTP provisioning fails closed).`;
706
+ return `Blocked: DNS resolution failed for "${hostname2}" (JIT HTTP provisioning fails closed).`;
748
707
  }
749
708
  if (!results.length) {
750
- return `Blocked: DNS returned no addresses for "${hostname}".`;
709
+ return `Blocked: DNS returned no addresses for "${hostname2}".`;
751
710
  }
752
711
  for (const { address } of results) {
753
712
  if (isPrivateIP(address)) {
754
- return `Blocked: URL "${hostname}" resolves to private address ${address}. Set Q_RING_ALLOW_PRIVATE_HOOKS=1 to override.`;
713
+ return `Blocked: URL "${hostname2}" resolves to private address ${address}. Set Q_RING_ALLOW_PRIVATE_HOOKS=1 to override.`;
755
714
  }
756
715
  }
757
716
  } catch {
@@ -762,11 +721,11 @@ function checkJitHttpProvisionUrl(url) {
762
721
 
763
722
  // src/core/hooks.ts
764
723
  function getRegistryPath2() {
765
- const dir = join4(homedir3(), ".config", "q-ring");
724
+ const dir = join5(homedir3(), ".config", "q-ring");
766
725
  if (!existsSync4(dir)) {
767
726
  mkdirSync3(dir, { recursive: true });
768
727
  }
769
- return join4(dir, "hooks.json");
728
+ return join5(dir, "hooks.json");
770
729
  }
771
730
  function loadRegistry2() {
772
731
  const path = getRegistryPath2();
@@ -774,7 +733,7 @@ function loadRegistry2() {
774
733
  return { hooks: [] };
775
734
  }
776
735
  try {
777
- return JSON.parse(readFileSync4(path, "utf8"));
736
+ return JSON.parse(readFileSync5(path, "utf8"));
778
737
  } catch {
779
738
  return { hooks: [] };
780
739
  }
@@ -1043,16 +1002,16 @@ async function fireHooks(payload, tags) {
1043
1002
  }
1044
1003
 
1045
1004
  // src/core/approval.ts
1046
- import { existsSync as existsSync5, readFileSync as readFileSync5, writeFileSync as writeFileSync3, mkdirSync as mkdirSync4 } from "fs";
1047
- import { join as join5 } from "path";
1005
+ import { existsSync as existsSync5, readFileSync as readFileSync6, writeFileSync as writeFileSync3, mkdirSync as mkdirSync4 } from "fs";
1006
+ import { join as join6 } from "path";
1048
1007
  import { homedir as homedir4 } from "os";
1049
1008
  import { createHmac, randomBytes, timingSafeEqual } from "crypto";
1050
1009
  function getHmacSecret() {
1051
- const dir = join5(homedir4(), ".config", "q-ring");
1052
- const secretPath = join5(dir, ".approval-key");
1010
+ const dir = join6(homedir4(), ".config", "q-ring");
1011
+ const secretPath = join6(dir, ".approval-key");
1053
1012
  if (!existsSync5(dir)) mkdirSync4(dir, { recursive: true, mode: 448 });
1054
1013
  if (existsSync5(secretPath)) {
1055
- return readFileSync5(secretPath, "utf8").trim();
1014
+ return readFileSync6(secretPath, "utf8").trim();
1056
1015
  }
1057
1016
  const secret = randomBytes(32).toString("hex");
1058
1017
  writeFileSync3(secretPath, secret, { mode: 384 });
@@ -1084,11 +1043,11 @@ function verifyHmac(entry) {
1084
1043
  }
1085
1044
  }
1086
1045
  function getRegistryPath3() {
1087
- const dir = join5(homedir4(), ".config", "q-ring");
1046
+ const dir = join6(homedir4(), ".config", "q-ring");
1088
1047
  if (!existsSync5(dir)) {
1089
1048
  mkdirSync4(dir, { recursive: true, mode: 448 });
1090
1049
  }
1091
- return join5(dir, "approvals.json");
1050
+ return join6(dir, "approvals.json");
1092
1051
  }
1093
1052
  function loadRegistry3() {
1094
1053
  const path = getRegistryPath3();
@@ -1096,7 +1055,7 @@ function loadRegistry3() {
1096
1055
  return { approvals: [] };
1097
1056
  }
1098
1057
  try {
1099
- return JSON.parse(readFileSync5(path, "utf8"));
1058
+ return JSON.parse(readFileSync6(path, "utf8"));
1100
1059
  } catch {
1101
1060
  return { approvals: [] };
1102
1061
  }
@@ -1168,6 +1127,176 @@ function listApprovals() {
1168
1127
  }));
1169
1128
  }
1170
1129
 
1130
+ // src/core/policy.ts
1131
+ var cachedPolicy = null;
1132
+ function loadPolicy(projectPath) {
1133
+ const pp = projectPath ?? process.cwd();
1134
+ if (cachedPolicy && cachedPolicy.path === pp) {
1135
+ return cachedPolicy.policy;
1136
+ }
1137
+ const config = readProjectConfig(pp);
1138
+ const policy = config?.policy ?? {};
1139
+ cachedPolicy = { path: pp, policy };
1140
+ return policy;
1141
+ }
1142
+ function checkSecretLifecyclePolicy(input, projectPath) {
1143
+ const policy = loadPolicy(projectPath);
1144
+ if (!policy.secrets) return { allowed: true, policySource: "no-policy" };
1145
+ const s = policy.secrets;
1146
+ if (s.maxTtlSeconds != null && input.ttlSeconds != null && input.ttlSeconds > s.maxTtlSeconds) {
1147
+ return {
1148
+ allowed: false,
1149
+ reason: `TTL ${input.ttlSeconds}s exceeds policy maximum ${s.maxTtlSeconds}s`,
1150
+ policySource: ".q-ring.json policy.secrets.maxTtlSeconds"
1151
+ };
1152
+ }
1153
+ if (s.requireApprovalForTags?.length && input.tags?.length) {
1154
+ const hit = input.tags.find((t) => s.requireApprovalForTags.includes(t));
1155
+ if (hit && !input.requiresApproval) {
1156
+ return {
1157
+ allowed: false,
1158
+ reason: `Tag "${hit}" requires explicit approval metadata (set requiresApproval / --requires-approval)`,
1159
+ policySource: ".q-ring.json policy.secrets.requireApprovalForTags"
1160
+ };
1161
+ }
1162
+ }
1163
+ if (s.requireRotationFormatForTags?.length && input.tags?.length) {
1164
+ const hit = input.tags.find((t) => s.requireRotationFormatForTags.includes(t));
1165
+ if (hit && !input.rotationFormat) {
1166
+ return {
1167
+ allowed: false,
1168
+ reason: `Tag "${hit}" requires a rotationFormat to be set`,
1169
+ policySource: ".q-ring.json policy.secrets.requireRotationFormatForTags"
1170
+ };
1171
+ }
1172
+ }
1173
+ return { allowed: true, policySource: ".q-ring.json" };
1174
+ }
1175
+ function checkKeyReadPolicy(key, tags, projectPath) {
1176
+ const policy = loadPolicy(projectPath);
1177
+ if (!policy.mcp) return { allowed: true, policySource: "no-policy" };
1178
+ if (policy.mcp.deniedKeys?.includes(key)) {
1179
+ return {
1180
+ allowed: false,
1181
+ reason: `Key "${key}" is denied by project policy`,
1182
+ policySource: ".q-ring.json policy.mcp.deniedKeys"
1183
+ };
1184
+ }
1185
+ if (policy.mcp.readableKeys && !policy.mcp.readableKeys.includes(key)) {
1186
+ return {
1187
+ allowed: false,
1188
+ reason: `Key "${key}" is not in the readable keys allowlist`,
1189
+ policySource: ".q-ring.json policy.mcp.readableKeys"
1190
+ };
1191
+ }
1192
+ if (tags && policy.mcp.deniedTags) {
1193
+ const blocked = tags.find((t) => policy.mcp.deniedTags.includes(t));
1194
+ if (blocked) {
1195
+ return {
1196
+ allowed: false,
1197
+ reason: `Tag "${blocked}" is denied by project policy`,
1198
+ policySource: ".q-ring.json policy.mcp.deniedTags"
1199
+ };
1200
+ }
1201
+ }
1202
+ return { allowed: true, policySource: ".q-ring.json" };
1203
+ }
1204
+ function checkExecPolicy(command, projectPath) {
1205
+ const policy = loadPolicy(projectPath);
1206
+ if (!policy.exec) return { allowed: true, policySource: "no-policy" };
1207
+ if (policy.exec.denyCommands) {
1208
+ const denied = policy.exec.denyCommands.find((d) => command.includes(d));
1209
+ if (denied) {
1210
+ return {
1211
+ allowed: false,
1212
+ reason: `Command containing "${denied}" is denied by project policy`,
1213
+ policySource: ".q-ring.json policy.exec.denyCommands"
1214
+ };
1215
+ }
1216
+ }
1217
+ if (policy.exec.allowCommands) {
1218
+ const allowed = policy.exec.allowCommands.some((a) => command.startsWith(a));
1219
+ if (!allowed) {
1220
+ return {
1221
+ allowed: false,
1222
+ reason: `Command "${command}" is not in the exec allowlist`,
1223
+ policySource: ".q-ring.json policy.exec.allowCommands"
1224
+ };
1225
+ }
1226
+ }
1227
+ return { allowed: true, policySource: ".q-ring.json" };
1228
+ }
1229
+ function getExecMaxRuntime(projectPath) {
1230
+ return loadPolicy(projectPath).exec?.maxRuntimeSeconds;
1231
+ }
1232
+ function getPolicySummary(projectPath) {
1233
+ const policy = loadPolicy(projectPath);
1234
+ return {
1235
+ hasMcpPolicy: !!policy.mcp,
1236
+ hasExecPolicy: !!policy.exec,
1237
+ hasSecretPolicy: !!policy.secrets,
1238
+ details: policy
1239
+ };
1240
+ }
1241
+
1242
+ // src/core/keyring.ts
1243
+ import { mkdirSync as mkdirSync5, writeFileSync as writeFileSync4, unlinkSync } from "fs";
1244
+ import { homedir as homedir5 } from "os";
1245
+ import { join as join7 } from "path";
1246
+ import { Entry, findCredentials } from "@napi-rs/keyring";
1247
+
1248
+ // src/utils/hash.ts
1249
+ import { createHash as createHash2 } from "crypto";
1250
+ function hashProjectPath(projectPath) {
1251
+ return createHash2("sha256").update(projectPath).digest("hex").slice(0, 12);
1252
+ }
1253
+
1254
+ // src/core/scope.ts
1255
+ var SERVICE_PREFIX = "q-ring";
1256
+ function globalService() {
1257
+ return `${SERVICE_PREFIX}:global`;
1258
+ }
1259
+ function projectService(projectPath) {
1260
+ const hash = hashProjectPath(projectPath);
1261
+ return `${SERVICE_PREFIX}:project:${hash}`;
1262
+ }
1263
+ function teamService(teamId) {
1264
+ return `${SERVICE_PREFIX}:team:${teamId}`;
1265
+ }
1266
+ function orgService(orgId) {
1267
+ return `${SERVICE_PREFIX}:org:${orgId}`;
1268
+ }
1269
+ function resolveScope(opts) {
1270
+ const { scope, projectPath, teamId, orgId } = opts;
1271
+ if (scope === "global") {
1272
+ return [{ scope: "global", service: globalService() }];
1273
+ }
1274
+ if (scope === "project") {
1275
+ if (!projectPath) throw new Error("Project path is required for project scope");
1276
+ return [{ scope: "project", service: projectService(projectPath), projectPath }];
1277
+ }
1278
+ if (scope === "team") {
1279
+ if (!teamId) throw new Error("Team ID is required for team scope");
1280
+ return [{ scope: "team", service: teamService(teamId), teamId }];
1281
+ }
1282
+ if (scope === "org") {
1283
+ if (!orgId) throw new Error("Org ID is required for org scope");
1284
+ return [{ scope: "org", service: orgService(orgId), orgId }];
1285
+ }
1286
+ const chain = [];
1287
+ if (projectPath) {
1288
+ chain.push({ scope: "project", service: projectService(projectPath), projectPath });
1289
+ }
1290
+ if (teamId) {
1291
+ chain.push({ scope: "team", service: teamService(teamId), teamId });
1292
+ }
1293
+ if (orgId) {
1294
+ chain.push({ scope: "org", service: orgService(orgId), orgId });
1295
+ }
1296
+ chain.push({ scope: "global", service: globalService() });
1297
+ return chain;
1298
+ }
1299
+
1171
1300
  // src/core/provision.ts
1172
1301
  import { execFileSync, spawnSync } from "child_process";
1173
1302
  import { z as z2 } from "zod";
@@ -1315,124 +1444,12 @@ var registry = new ProvisionRegistry();
1315
1444
  registry.register(awsStsProvider);
1316
1445
  registry.register(httpProvider);
1317
1446
 
1318
- // src/core/policy.ts
1319
- var cachedPolicy = null;
1320
- function loadPolicy(projectPath) {
1321
- const pp = projectPath ?? process.cwd();
1322
- if (cachedPolicy && cachedPolicy.path === pp) {
1323
- return cachedPolicy.policy;
1324
- }
1325
- const config = readProjectConfig(pp);
1326
- const policy = config?.policy ?? {};
1327
- cachedPolicy = { path: pp, policy };
1328
- return policy;
1329
- }
1330
- function checkSecretLifecyclePolicy(input, projectPath) {
1331
- const policy = loadPolicy(projectPath);
1332
- if (!policy.secrets) return { allowed: true, policySource: "no-policy" };
1333
- const s = policy.secrets;
1334
- if (s.maxTtlSeconds != null && input.ttlSeconds != null && input.ttlSeconds > s.maxTtlSeconds) {
1335
- return {
1336
- allowed: false,
1337
- reason: `TTL ${input.ttlSeconds}s exceeds policy maximum ${s.maxTtlSeconds}s`,
1338
- policySource: ".q-ring.json policy.secrets.maxTtlSeconds"
1339
- };
1340
- }
1341
- if (s.requireApprovalForTags?.length && input.tags?.length) {
1342
- const hit = input.tags.find((t) => s.requireApprovalForTags.includes(t));
1343
- if (hit && !input.requiresApproval) {
1344
- return {
1345
- allowed: false,
1346
- reason: `Tag "${hit}" requires explicit approval metadata (set requiresApproval / --requires-approval)`,
1347
- policySource: ".q-ring.json policy.secrets.requireApprovalForTags"
1348
- };
1349
- }
1350
- }
1351
- if (s.requireRotationFormatForTags?.length && input.tags?.length) {
1352
- const hit = input.tags.find((t) => s.requireRotationFormatForTags.includes(t));
1353
- if (hit && !input.rotationFormat) {
1354
- return {
1355
- allowed: false,
1356
- reason: `Tag "${hit}" requires a rotationFormat to be set`,
1357
- policySource: ".q-ring.json policy.secrets.requireRotationFormatForTags"
1358
- };
1359
- }
1360
- }
1361
- return { allowed: true, policySource: ".q-ring.json" };
1362
- }
1363
- function checkKeyReadPolicy(key, tags, projectPath) {
1364
- const policy = loadPolicy(projectPath);
1365
- if (!policy.mcp) return { allowed: true, policySource: "no-policy" };
1366
- if (policy.mcp.deniedKeys?.includes(key)) {
1367
- return {
1368
- allowed: false,
1369
- reason: `Key "${key}" is denied by project policy`,
1370
- policySource: ".q-ring.json policy.mcp.deniedKeys"
1371
- };
1372
- }
1373
- if (policy.mcp.readableKeys && !policy.mcp.readableKeys.includes(key)) {
1374
- return {
1375
- allowed: false,
1376
- reason: `Key "${key}" is not in the readable keys allowlist`,
1377
- policySource: ".q-ring.json policy.mcp.readableKeys"
1378
- };
1379
- }
1380
- if (tags && policy.mcp.deniedTags) {
1381
- const blocked = tags.find((t) => policy.mcp.deniedTags.includes(t));
1382
- if (blocked) {
1383
- return {
1384
- allowed: false,
1385
- reason: `Tag "${blocked}" is denied by project policy`,
1386
- policySource: ".q-ring.json policy.mcp.deniedTags"
1387
- };
1388
- }
1389
- }
1390
- return { allowed: true, policySource: ".q-ring.json" };
1391
- }
1392
- function checkExecPolicy(command, projectPath) {
1393
- const policy = loadPolicy(projectPath);
1394
- if (!policy.exec) return { allowed: true, policySource: "no-policy" };
1395
- if (policy.exec.denyCommands) {
1396
- const denied = policy.exec.denyCommands.find((d) => command.includes(d));
1397
- if (denied) {
1398
- return {
1399
- allowed: false,
1400
- reason: `Command containing "${denied}" is denied by project policy`,
1401
- policySource: ".q-ring.json policy.exec.denyCommands"
1402
- };
1403
- }
1404
- }
1405
- if (policy.exec.allowCommands) {
1406
- const allowed = policy.exec.allowCommands.some((a) => command.startsWith(a));
1407
- if (!allowed) {
1408
- return {
1409
- allowed: false,
1410
- reason: `Command "${command}" is not in the exec allowlist`,
1411
- policySource: ".q-ring.json policy.exec.allowCommands"
1412
- };
1413
- }
1414
- }
1415
- return { allowed: true, policySource: ".q-ring.json" };
1416
- }
1417
- function getExecMaxRuntime(projectPath) {
1418
- return loadPolicy(projectPath).exec?.maxRuntimeSeconds;
1419
- }
1420
- function getPolicySummary(projectPath) {
1421
- const policy = loadPolicy(projectPath);
1422
- return {
1423
- hasMcpPolicy: !!policy.mcp,
1424
- hasExecPolicy: !!policy.exec,
1425
- hasSecretPolicy: !!policy.secrets,
1426
- details: policy
1427
- };
1428
- }
1429
-
1430
1447
  // src/core/keyring.ts
1431
1448
  function withJitEnvelopeLock(service, key, fn) {
1432
- const dir = join6(homedir5(), ".config", "q-ring", "jit-locks");
1449
+ const dir = join7(homedir5(), ".config", "q-ring", "jit-locks");
1433
1450
  mkdirSync5(dir, { recursive: true });
1434
1451
  const safe = Buffer.from(`${service}\0${key}`, "utf8").toString("base64url");
1435
- const lockPath = join6(dir, `${safe}.lock`);
1452
+ const lockPath = join7(dir, `${safe}.lock`);
1436
1453
  const deadline = Date.now() + 8e3;
1437
1454
  while (Date.now() < deadline) {
1438
1455
  try {
@@ -1945,7 +1962,125 @@ function tunnelList() {
1945
1962
  return result;
1946
1963
  }
1947
1964
 
1965
+ // src/core/memory.ts
1966
+ import { existsSync as existsSync6, readFileSync as readFileSync7, writeFileSync as writeFileSync5, mkdirSync as mkdirSync6 } from "fs";
1967
+ import { join as join8 } from "path";
1968
+ import { homedir as homedir6, hostname, userInfo } from "os";
1969
+ import { createCipheriv, createDecipheriv, createHash as createHash3, randomBytes as randomBytes3 } from "crypto";
1970
+ import { Entry as Entry2 } from "@napi-rs/keyring";
1971
+ var MEMORY_FILE = "agent-memory.enc";
1972
+ var KEYRING_SERVICE = "qring-memory-key";
1973
+ var KEYRING_ACCOUNT = "encryption-key";
1974
+ function getMemoryDir() {
1975
+ const dir = join8(homedir6(), ".config", "q-ring");
1976
+ if (!existsSync6(dir)) {
1977
+ mkdirSync6(dir, { recursive: true });
1978
+ }
1979
+ return dir;
1980
+ }
1981
+ function getMemoryPath() {
1982
+ return join8(getMemoryDir(), MEMORY_FILE);
1983
+ }
1984
+ function deriveLegacyKey() {
1985
+ const fingerprint = `qring-memory:${hostname()}:${userInfo().username}`;
1986
+ return createHash3("sha256").update(fingerprint).digest();
1987
+ }
1988
+ function getOrCreateKey() {
1989
+ try {
1990
+ const entry = new Entry2(KEYRING_SERVICE, KEYRING_ACCOUNT);
1991
+ const stored = entry.getPassword();
1992
+ if (stored) return Buffer.from(stored, "base64");
1993
+ const key = randomBytes3(32);
1994
+ entry.setPassword(key.toString("base64"));
1995
+ return key;
1996
+ } catch {
1997
+ console.warn("[q-ring] OS keyring unavailable for memory key \u2014 falling back to machine-derived key");
1998
+ return deriveLegacyKey();
1999
+ }
2000
+ }
2001
+ function encryptWith(data, key) {
2002
+ const iv = randomBytes3(12);
2003
+ const cipher = createCipheriv("aes-256-gcm", key, iv);
2004
+ const encrypted = Buffer.concat([cipher.update(data, "utf8"), cipher.final()]);
2005
+ const tag = cipher.getAuthTag();
2006
+ return `${iv.toString("base64")}:${tag.toString("base64")}:${encrypted.toString("base64")}`;
2007
+ }
2008
+ function decryptWith(blob, key) {
2009
+ const parts = blob.split(":");
2010
+ if (parts.length !== 3) throw new Error("Invalid encrypted format");
2011
+ const iv = Buffer.from(parts[0], "base64");
2012
+ const tag = Buffer.from(parts[1], "base64");
2013
+ const encrypted = Buffer.from(parts[2], "base64");
2014
+ const decipher = createDecipheriv("aes-256-gcm", key, iv);
2015
+ decipher.setAuthTag(tag);
2016
+ return decipher.update(encrypted) + decipher.final("utf8");
2017
+ }
2018
+ function encrypt(data) {
2019
+ return encryptWith(data, getOrCreateKey());
2020
+ }
2021
+ function decrypt(blob) {
2022
+ const key = getOrCreateKey();
2023
+ try {
2024
+ return decryptWith(blob, key);
2025
+ } catch {
2026
+ const legacy = deriveLegacyKey();
2027
+ const plain = decryptWith(blob, legacy);
2028
+ writeFileSync5(getMemoryPath(), encryptWith(plain, key), "utf8");
2029
+ return plain;
2030
+ }
2031
+ }
2032
+ function loadStore() {
2033
+ const path = getMemoryPath();
2034
+ if (!existsSync6(path)) {
2035
+ return { entries: {} };
2036
+ }
2037
+ try {
2038
+ const raw = readFileSync7(path, "utf8");
2039
+ const decrypted = decrypt(raw);
2040
+ return JSON.parse(decrypted);
2041
+ } catch {
2042
+ return { entries: {} };
2043
+ }
2044
+ }
2045
+ function saveStore(store) {
2046
+ const json = JSON.stringify(store);
2047
+ const encrypted = encrypt(json);
2048
+ writeFileSync5(getMemoryPath(), encrypted, "utf8");
2049
+ }
2050
+ function remember(key, value) {
2051
+ const store = loadStore();
2052
+ store.entries[key] = {
2053
+ value,
2054
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
2055
+ };
2056
+ saveStore(store);
2057
+ }
2058
+ function recall(key) {
2059
+ const store = loadStore();
2060
+ return store.entries[key]?.value ?? null;
2061
+ }
2062
+ function listMemory() {
2063
+ const store = loadStore();
2064
+ return Object.entries(store.entries).map(([key, entry]) => ({
2065
+ key,
2066
+ updatedAt: entry.updatedAt
2067
+ }));
2068
+ }
2069
+ function forget(key) {
2070
+ const store = loadStore();
2071
+ if (key in store.entries) {
2072
+ delete store.entries[key];
2073
+ saveStore(store);
2074
+ return true;
2075
+ }
2076
+ return false;
2077
+ }
2078
+ function clearMemory() {
2079
+ saveStore({ entries: {} });
2080
+ }
2081
+
1948
2082
  export {
2083
+ PACKAGE_VERSION,
1949
2084
  checkDecay,
1950
2085
  readProjectConfig,
1951
2086
  collapseEnvironment,
@@ -1982,6 +2117,11 @@ export {
1982
2117
  tunnelCreate,
1983
2118
  tunnelRead,
1984
2119
  tunnelDestroy,
1985
- tunnelList
2120
+ tunnelList,
2121
+ remember,
2122
+ recall,
2123
+ listMemory,
2124
+ forget,
2125
+ clearMemory
1986
2126
  };
1987
- //# sourceMappingURL=chunk-LLMO64FE.js.map
2127
+ //# sourceMappingURL=chunk-A4RZVP3P.js.map