@i4ctime/q-ring 0.15.0 → 0.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -500,6 +500,41 @@ qring approve PROD_DB_URL --revoke
500
500
 
501
501
  When an agent is blocked on an approval-protected key, q-ring raises a desktop notification (Linux `notify-send`, macOS `osascript`) naming the key and the exact `qring approve` command — throttled per key, disabled with `QRING_NOTIFY=off`.
502
502
 
503
+ ### Canary Honeytokens
504
+
505
+ Plant fake credentials that look and read exactly like real ones. Anything that touches one — a compromised MCP server, an over-curious agent, exfiltrated tooling sweeping the ring — gets the fake value back with no tell, while q-ring fires a desktop alert and writes a `canary` event into the tamper-evident audit chain.
506
+
507
+ ```bash
508
+ # Plant a canary shaped like a real AWS access key
509
+ qring canary plant AWS_SECRET_ACCESS_KEY --format aws
510
+
511
+ # Other shapes: github, openai, anthropic, stripe, generic
512
+ qring canary plant GHP_BACKUP_TOKEN --format github
513
+
514
+ # See what's been tripped
515
+ qring canary list
516
+ qring audit --action canary
517
+ ```
518
+
519
+ Values are CSPRNG noise in the provider's real token shape (an `aws` canary matches `AKIA[A-Z0-9]{16}`) — plausible enough to be taken, never valid. Alerts are throttled to one per key per 30 seconds; the audit trail records every read.
520
+
521
+ ### MCP Airlock
522
+
523
+ Run a third-party MCP server behind q-ring. The airlock sits between your agent host and the wrapped server, spawns it with a **stripped environment** (no inherited API keys — opt back in with `--inherit-env`), and records every tool call that crosses it as a `wrap` event in the audit chain, grouped per session and labeled with the calling client's identity. Tool arguments are never logged — they may contain secrets.
524
+
525
+ ```json
526
+ {
527
+ "mcpServers": {
528
+ "some-server": {
529
+ "command": "qring",
530
+ "args": ["mcp", "wrap", "--", "npx", "-y", "some-mcp-server"]
531
+ }
532
+ }
533
+ }
534
+ ```
535
+
536
+ Tools-only proxy today: `tools/list` and `tools/call` pass through verbatim, so the wrapped server behaves identically — it just can't read your environment, and everything it's asked to do is on the record.
537
+
503
538
  ### Just-In-Time (JIT) Provisioning
504
539
 
505
540
  Instead of storing static credentials, configure `q-ring` to dynamically generate short-lived tokens on the fly when requested (e.g. AWS STS, generic HTTP endpoints).
@@ -648,7 +683,7 @@ qring exec -- echo "hello"
648
683
 
649
684
  ### Tamper-Evident Audit
650
685
 
651
- Every audit event includes a SHA-256 hash of the previous event, creating a tamper-evident chain. Since v0.14 the chain is also anchored with a keyed HMAC stored in the OS keyring, so `qring audit:verify` detects truncation and whole-file rewrites — not just in-place edits. Verify integrity and export logs in multiple formats.
686
+ Every audit event includes a SHA-256 hash of the previous event, creating a tamper-evident chain. Since v0.14 the chain is also anchored with a keyed HMAC stored in the OS keyring, so `qring audit:verify` detects truncation and whole-file rewrites — not just in-place edits. Verify integrity and export logs in multiple formats. Events from MCP sessions are additionally stamped with the connecting client's self-reported identity (`clientInfo` name@version) — an audit label for "which agent did this", never an authorization boundary, since clients choose what to report.
652
687
 
653
688
  ```bash
654
689
  # Verify the entire audit chain
@@ -40,7 +40,9 @@ var SecretMetadataSchema = z.object({
40
40
  validationUrl: z.string().optional(),
41
41
  requiresApproval: z.boolean().optional(),
42
42
  jitProvider: z.string().optional(),
43
- jitExpiresAt: z.string().optional()
43
+ jitExpiresAt: z.string().optional(),
44
+ canary: z.boolean().optional(),
45
+ canaryFormat: z.string().optional()
44
46
  });
45
47
  var QuantumEnvelopeSchema = z.object({
46
48
  v: z.literal(1),
@@ -73,7 +75,9 @@ function createEnvelope(value, opts) {
73
75
  rotationPrefix: opts?.rotationPrefix,
74
76
  provider: opts?.provider,
75
77
  requiresApproval: opts?.requiresApproval,
76
- jitProvider: opts?.jitProvider
78
+ jitProvider: opts?.jitProvider,
79
+ canary: opts?.canary,
80
+ canaryFormat: opts?.canaryFormat
77
81
  }
78
82
  };
79
83
  }
@@ -561,6 +565,18 @@ function writeStoredAnchor(hash) {
561
565
  } catch {
562
566
  }
563
567
  }
568
+ var auditAgentLabel = null;
569
+ function setAuditAgentLabel(label) {
570
+ if (label === null) {
571
+ auditAgentLabel = null;
572
+ return;
573
+ }
574
+ const cleaned = label.replace(/[^\x20-\x7e]/g, "").trim().slice(0, 128);
575
+ auditAgentLabel = cleaned.length > 0 ? cleaned : null;
576
+ }
577
+ function getAuditAgentLabel() {
578
+ return auditAgentLabel;
579
+ }
564
580
  function getAuditDir() {
565
581
  if (process.env.QRING_AUDIT_DIR) {
566
582
  if (!existsSync2(process.env.QRING_AUDIT_DIR)) {
@@ -612,6 +628,9 @@ function logAudit(event) {
612
628
  pid: process.pid,
613
629
  prevHash
614
630
  };
631
+ if (full.agent === void 0 && auditAgentLabel) {
632
+ full.agent = auditAgentLabel;
633
+ }
615
634
  const line = JSON.stringify(full);
616
635
  const path = getAuditPath();
617
636
  appendFileSync(path, line + "\n", { mode: 384 });
@@ -656,6 +675,7 @@ function queryAudit(query = {}) {
656
675
  if (query.action) events = events.filter((e) => e.action === query.action);
657
676
  if (query.source) events = events.filter((e) => e.source === query.source);
658
677
  if (query.correlationId) events = events.filter((e) => e.correlationId === query.correlationId);
678
+ if (query.agent) events = events.filter((e) => e.agent === query.agent);
659
679
  if (query.since) {
660
680
  const since = new Date(query.since).getTime();
661
681
  events = events.filter((e) => new Date(e.timestamp).getTime() >= since);
@@ -746,9 +766,9 @@ function exportAudit(opts = {}) {
746
766
  return JSON.stringify(events, null, 2);
747
767
  }
748
768
  if (opts.format === "csv") {
749
- const header = "timestamp,action,key,scope,env,source,pid,correlationId,detail";
769
+ const header = "timestamp,action,key,scope,env,source,agent,pid,correlationId,detail";
750
770
  const rows = events.map(
751
- (e) => `${e.timestamp},${e.action},${e.key ?? ""},${e.scope ?? ""},${e.env ?? ""},${e.source},${e.pid},${e.correlationId ?? ""},${(e.detail ?? "").replace(/,/g, ";")}`
771
+ (e) => `${e.timestamp},${e.action},${e.key ?? ""},${e.scope ?? ""},${e.env ?? ""},${e.source},${(e.agent ?? "").replace(/,/g, ";")},${e.pid},${e.correlationId ?? ""},${(e.detail ?? "").replace(/,/g, ";")}`
752
772
  );
753
773
  return [header, ...rows].join("\n");
754
774
  }
@@ -1771,6 +1791,31 @@ function notifyApprovalRequested(key, source) {
1771
1791
  );
1772
1792
  }
1773
1793
 
1794
+ // src/core/canary-alert.ts
1795
+ var TRIP_THROTTLE_MS = 30 * 1e3;
1796
+ var lastAlerted = /* @__PURE__ */ new Map();
1797
+ function recordCanaryTrip(trip) {
1798
+ const agent = getAuditAgentLabel();
1799
+ logAudit({
1800
+ action: "canary",
1801
+ key: trip.key,
1802
+ scope: trip.scope,
1803
+ env: trip.env,
1804
+ source: trip.source,
1805
+ detail: `CANARY TRIPPED: honeytoken read via ${trip.source}`
1806
+ });
1807
+ if (!notificationsEnabled()) return;
1808
+ const now = Date.now();
1809
+ const last = lastAlerted.get(trip.key);
1810
+ if (last !== void 0 && now - last < TRIP_THROTTLE_MS) return;
1811
+ lastAlerted.set(trip.key, now);
1812
+ const who = agent ? `${trip.source} (${agent})` : trip.source;
1813
+ notifyUser(
1814
+ "q-ring: CANARY TRIPPED",
1815
+ `Honeytoken "${trip.key}" was read by ${who}. This credential is fake \u2014 but something reached for it. Investigate: qring audit --action canary`
1816
+ );
1817
+ }
1818
+
1774
1819
  // src/core/provision.ts
1775
1820
  import { execFileSync, spawnSync } from "child_process";
1776
1821
  import { z as z3 } from "zod";
@@ -2052,6 +2097,9 @@ function getSecret(key, opts = {}) {
2052
2097
  const latest = readEnvelope(service, key) ?? envelope;
2053
2098
  writeEnvelope(service, key, recordAccess(latest));
2054
2099
  logAudit({ action: "read", key, scope, env, source });
2100
+ if (envelope.meta.canary) {
2101
+ recordCanaryTrip({ key, scope, env, source });
2102
+ }
2055
2103
  }
2056
2104
  return value;
2057
2105
  }
@@ -2084,6 +2132,8 @@ function setSecret(key, value, opts = {}) {
2084
2132
  const prov = opts.provider ?? existing?.meta.provider;
2085
2133
  const reqApp = opts.requiresApproval ?? existing?.meta.requiresApproval;
2086
2134
  const jitProv = opts.jitProvider ?? existing?.meta.jitProvider;
2135
+ const canaryFlag = opts.canary ?? existing?.meta.canary;
2136
+ const canaryFmt = opts.canaryFormat ?? existing?.meta.canaryFormat;
2087
2137
  const mergedTags = opts.tags ?? existing?.meta.tags;
2088
2138
  const ttlForPolicy = opts.ttlSeconds ?? existing?.meta.ttlSeconds;
2089
2139
  const life = checkSecretLifecyclePolicy(
@@ -2111,7 +2161,9 @@ function setSecret(key, value, opts = {}) {
2111
2161
  rotationPrefix: rotPfx,
2112
2162
  provider: prov,
2113
2163
  requiresApproval: reqApp,
2114
- jitProvider: jitProv
2164
+ jitProvider: jitProv,
2165
+ canary: canaryFlag,
2166
+ canaryFormat: canaryFmt
2115
2167
  });
2116
2168
  } else {
2117
2169
  envelope = createEnvelope(value, {
@@ -2124,7 +2176,9 @@ function setSecret(key, value, opts = {}) {
2124
2176
  rotationPrefix: rotPfx,
2125
2177
  provider: prov,
2126
2178
  requiresApproval: reqApp,
2127
- jitProvider: jitProv
2179
+ jitProvider: jitProv,
2180
+ canary: canaryFlag,
2181
+ canaryFormat: canaryFmt
2128
2182
  });
2129
2183
  }
2130
2184
  if (existing) {
@@ -2620,6 +2674,7 @@ export {
2620
2674
  checkDecay,
2621
2675
  readProjectConfig,
2622
2676
  collapseEnvironment,
2677
+ setAuditAgentLabel,
2623
2678
  logAudit,
2624
2679
  queryAudit,
2625
2680
  verifyAuditChain,
@@ -2661,4 +2716,4 @@ export {
2661
2716
  forget,
2662
2717
  clearMemory
2663
2718
  };
2664
- //# sourceMappingURL=chunk-MLBJCPX2.js.map
2719
+ //# sourceMappingURL=chunk-5LFKBZ3Q.js.map