@i4ctime/q-ring 0.16.1 → 0.16.2
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 +22 -2
- package/dist/{chunk-NCM5GHNW.js → chunk-KFILBHOY.js} +58 -7
- package/dist/chunk-KFILBHOY.js.map +1 -0
- package/dist/{chunk-5LFKBZ3Q.js → chunk-SQKTDYSO.js} +80 -7
- package/dist/chunk-SQKTDYSO.js.map +1 -0
- package/dist/dashboard-2MLSJZQR.js +658 -0
- package/dist/dashboard-2MLSJZQR.js.map +1 -0
- package/dist/dashboard-WVPR5BQO.js +658 -0
- package/dist/dashboard-WVPR5BQO.js.map +1 -0
- package/dist/index.js +96 -27
- package/dist/index.js.map +1 -1
- package/dist/mcp.js +16 -6
- package/dist/mcp.js.map +1 -1
- package/package.json +12 -12
- package/dist/chunk-5LFKBZ3Q.js.map +0 -1
- package/dist/chunk-NCM5GHNW.js.map +0 -1
- package/dist/dashboard-A3GJQCJX.js +0 -658
- package/dist/dashboard-A3GJQCJX.js.map +0 -1
- package/dist/dashboard-R3FWTFFW.js +0 -658
- package/dist/dashboard-R3FWTFFW.js.map +0 -1
package/README.md
CHANGED
|
@@ -518,6 +518,8 @@ qring audit --action canary
|
|
|
518
518
|
|
|
519
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
520
|
|
|
521
|
+
Canaries are built to stay covert: they carry no identifying description (add an innocuous cover story with `--description` if you like), their flag never appears in MCP tool responses, and trip records are visible only from the operator's terminal — never to agents via MCP audit tools. Bulk `export` and `delete` trip them just like reads, so sweeping the ring or removing the tripwire both ring the bell. Done with one? `qring canary disarm <key>` turns it back into an ordinary secret (`qring set` over a canary warns you first — the flag deliberately survives overwrites so an agent can't launder it away).
|
|
522
|
+
|
|
521
523
|
### MCP Airlock
|
|
522
524
|
|
|
523
525
|
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.
|
|
@@ -533,7 +535,9 @@ Run a third-party MCP server behind q-ring. The airlock sits between your agent
|
|
|
533
535
|
}
|
|
534
536
|
```
|
|
535
537
|
|
|
536
|
-
Tools-only proxy today: `tools/list` and `tools/call` pass through verbatim,
|
|
538
|
+
Tools-only proxy today: `tools/list` and `tools/call` pass through verbatim (pagination, progress notifications, cancellation, and `tools/list_changed` included; long-running tools are governed by the host's own timeout, with a generous airlock ceiling configurable via `QRING_WRAP_TIMEOUT_MS`). A wrapped server's *resources and prompts* are not proxied yet — a resources-heavy server will look tools-only behind the airlock.
|
|
539
|
+
|
|
540
|
+
Be clear about what the airlock is: env stripping plus a tamper-evident record of every tool call. It is **not a sandbox** — the wrapped process still runs as your user with normal filesystem, network, and OS-keychain access, and tool descriptions/results pass through uninspected. See `docs/threat-model.md` for the honest boundary picture.
|
|
537
541
|
|
|
538
542
|
### Just-In-Time (JIT) Provisioning
|
|
539
543
|
|
|
@@ -683,7 +687,7 @@ qring exec -- echo "hello"
|
|
|
683
687
|
|
|
684
688
|
### Tamper-Evident Audit
|
|
685
689
|
|
|
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.
|
|
690
|
+
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) — shown in `qring audit` output and filterable with `qring audit --agent <label>`. It's an audit label for "which agent did this", never an authorization boundary, since clients choose what to report.
|
|
687
691
|
|
|
688
692
|
```bash
|
|
689
693
|
# Verify the entire audit chain
|
|
@@ -951,6 +955,22 @@ Add to `~/.claude/claude_desktop_config.json`:
|
|
|
951
955
|
}
|
|
952
956
|
```
|
|
953
957
|
|
|
958
|
+
### VS Code Configuration
|
|
959
|
+
|
|
960
|
+
VS Code speaks MCP natively — add to `.vscode/mcp.json` (note the `servers` key, not `mcpServers`):
|
|
961
|
+
|
|
962
|
+
```json
|
|
963
|
+
{
|
|
964
|
+
"servers": {
|
|
965
|
+
"q-ring": {
|
|
966
|
+
"command": "qring-mcp"
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
```
|
|
971
|
+
|
|
972
|
+
`qring setup` does not write this file yet — VS Code is config-only (no first-party plugin bundle).
|
|
973
|
+
|
|
954
974
|
## Editor Plugins
|
|
955
975
|
|
|
956
976
|
The q-ring repo ships three first-party editor packs — each one adds rules/steering, agents, commands, skills, hooks, and the MCP connector to its host editor.
|
|
@@ -335,7 +335,8 @@ function isProcessAlive(pid) {
|
|
|
335
335
|
}
|
|
336
336
|
}
|
|
337
337
|
function withFileLock(name, fn, opts = {}) {
|
|
338
|
-
const
|
|
338
|
+
const baseDir = process.env.QRING_LOCK_DIR ?? (process.env.QRING_AUDIT_DIR ? join3(process.env.QRING_AUDIT_DIR, ".locks") : join3(homedir(), ".config", "q-ring"));
|
|
339
|
+
const lockDir = join3(baseDir, opts.dir ?? "locks");
|
|
339
340
|
mkdirSync(lockDir, { recursive: true, mode: 448 });
|
|
340
341
|
const safe = Buffer.from(name, "utf8").toString("base64url");
|
|
341
342
|
const lockPath = join3(lockDir, `${safe}.lock`);
|
|
@@ -577,6 +578,13 @@ function setAuditAgentLabel(label) {
|
|
|
577
578
|
function getAuditAgentLabel() {
|
|
578
579
|
return auditAgentLabel;
|
|
579
580
|
}
|
|
581
|
+
function sanitizeAuditText(value, max) {
|
|
582
|
+
const cleaned = Array.from(value).filter((ch) => {
|
|
583
|
+
const cp = ch.codePointAt(0) ?? 0;
|
|
584
|
+
return cp >= 32 && cp !== 127;
|
|
585
|
+
}).join("");
|
|
586
|
+
return cleaned.length > max ? `${cleaned.slice(0, max)}\u2026` : cleaned;
|
|
587
|
+
}
|
|
580
588
|
function getAuditDir() {
|
|
581
589
|
if (process.env.QRING_AUDIT_DIR) {
|
|
582
590
|
if (!existsSync2(process.env.QRING_AUDIT_DIR)) {
|
|
@@ -631,6 +639,10 @@ function logAudit(event) {
|
|
|
631
639
|
if (full.agent === void 0 && auditAgentLabel) {
|
|
632
640
|
full.agent = auditAgentLabel;
|
|
633
641
|
}
|
|
642
|
+
if (full.key !== void 0) full.key = sanitizeAuditText(full.key, 256);
|
|
643
|
+
if (full.detail !== void 0) full.detail = sanitizeAuditText(full.detail, 600);
|
|
644
|
+
if (full.scope !== void 0) full.scope = sanitizeAuditText(full.scope, 128);
|
|
645
|
+
if (full.env !== void 0) full.env = sanitizeAuditText(full.env, 128);
|
|
634
646
|
const line = JSON.stringify(full);
|
|
635
647
|
const path = getAuditPath();
|
|
636
648
|
appendFileSync(path, line + "\n", { mode: 384 });
|
|
@@ -643,7 +655,12 @@ function logAudit(event) {
|
|
|
643
655
|
},
|
|
644
656
|
{ timeoutMs: 5e3 }
|
|
645
657
|
);
|
|
646
|
-
} catch {
|
|
658
|
+
} catch (err) {
|
|
659
|
+
if (process.env.QRING_DEBUG) {
|
|
660
|
+
console.error(
|
|
661
|
+
`q-ring: audit event dropped (${event.action}${event.key ? ` ${event.key}` : ""}): ${err instanceof Error ? err.message : String(err)}`
|
|
662
|
+
);
|
|
663
|
+
}
|
|
647
664
|
}
|
|
648
665
|
}
|
|
649
666
|
var MAX_AUDIT_BYTES = 12 * 1024 * 1024;
|
|
@@ -762,13 +779,33 @@ function exportAudit(opts = {}) {
|
|
|
762
779
|
const until = new Date(opts.until).getTime();
|
|
763
780
|
events = events.filter((e) => new Date(e.timestamp).getTime() <= until);
|
|
764
781
|
}
|
|
782
|
+
if (opts.excludeActions?.length) {
|
|
783
|
+
const excluded = new Set(opts.excludeActions);
|
|
784
|
+
events = events.filter((e) => !excluded.has(e.action));
|
|
785
|
+
}
|
|
765
786
|
if (opts.format === "json") {
|
|
766
787
|
return JSON.stringify(events, null, 2);
|
|
767
788
|
}
|
|
768
789
|
if (opts.format === "csv") {
|
|
790
|
+
const csvField = (v) => {
|
|
791
|
+
let s = v === void 0 ? "" : String(v);
|
|
792
|
+
if (/^[=+\-@\t\r]/.test(s)) s = `'${s}`;
|
|
793
|
+
return `"${s.replace(/"/g, '""')}"`;
|
|
794
|
+
};
|
|
769
795
|
const header = "timestamp,action,key,scope,env,source,agent,pid,correlationId,detail";
|
|
770
796
|
const rows = events.map(
|
|
771
|
-
(e) =>
|
|
797
|
+
(e) => [
|
|
798
|
+
csvField(e.timestamp),
|
|
799
|
+
csvField(e.action),
|
|
800
|
+
csvField(e.key),
|
|
801
|
+
csvField(e.scope),
|
|
802
|
+
csvField(e.env),
|
|
803
|
+
csvField(e.source),
|
|
804
|
+
csvField(e.agent),
|
|
805
|
+
csvField(e.pid),
|
|
806
|
+
csvField(e.correlationId),
|
|
807
|
+
csvField(e.detail)
|
|
808
|
+
].join(",")
|
|
772
809
|
);
|
|
773
810
|
return [header, ...rows].join("\n");
|
|
774
811
|
}
|
|
@@ -1718,7 +1755,8 @@ function notifyUser(title, body) {
|
|
|
1718
1755
|
let args;
|
|
1719
1756
|
if (process.platform === "linux") {
|
|
1720
1757
|
command = "notify-send";
|
|
1721
|
-
|
|
1758
|
+
const pango = (s) => s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
1759
|
+
args = ["--app-name=q-ring", "--urgency=critical", pango(title), pango(body)];
|
|
1722
1760
|
} else if (process.platform === "darwin") {
|
|
1723
1761
|
command = "osascript";
|
|
1724
1762
|
const clean = (s) => s.replace(/["\\]/g, "");
|
|
@@ -1758,7 +1796,7 @@ function recordCanaryTrip(trip) {
|
|
|
1758
1796
|
scope: trip.scope,
|
|
1759
1797
|
env: trip.env,
|
|
1760
1798
|
source: trip.source,
|
|
1761
|
-
detail: `CANARY TRIPPED: honeytoken read via ${trip.source}`
|
|
1799
|
+
detail: `CANARY TRIPPED: ${trip.detail ?? `honeytoken read via ${trip.source}`}`
|
|
1762
1800
|
});
|
|
1763
1801
|
if (!notificationsEnabled()) return;
|
|
1764
1802
|
const now = Date.now();
|
|
@@ -2201,8 +2239,12 @@ function deleteSecret(key, opts = {}) {
|
|
|
2201
2239
|
for (const { service, scope } of scopes) {
|
|
2202
2240
|
const entry = new Entry(service, key);
|
|
2203
2241
|
try {
|
|
2242
|
+
const existing = readEnvelope(service, key);
|
|
2204
2243
|
if (entry.deleteCredential()) {
|
|
2205
2244
|
deleted = true;
|
|
2245
|
+
if (existing?.meta.canary) {
|
|
2246
|
+
recordCanaryTrip({ key, scope, source, detail: "canary deleted" });
|
|
2247
|
+
}
|
|
2206
2248
|
logAudit({ action: "delete", key, scope, source });
|
|
2207
2249
|
fireHooks({
|
|
2208
2250
|
action: "delete",
|
|
@@ -2321,6 +2363,9 @@ function exportSecrets(opts = {}) {
|
|
|
2321
2363
|
const value = collapseValue(entry.envelope, env);
|
|
2322
2364
|
if (value !== null) {
|
|
2323
2365
|
rawValues.set(entry.key, value);
|
|
2366
|
+
if (entry.envelope.meta.canary) {
|
|
2367
|
+
recordCanaryTrip({ key: entry.key, scope: entry.scope, env, source });
|
|
2368
|
+
}
|
|
2324
2369
|
}
|
|
2325
2370
|
}
|
|
2326
2371
|
}
|
|
@@ -2333,7 +2378,13 @@ function exportSecrets(opts = {}) {
|
|
|
2333
2378
|
console.warn(`Warning: skipped exporting ${key} due to template error: ${err instanceof Error ? err.message : String(err)}`);
|
|
2334
2379
|
}
|
|
2335
2380
|
}
|
|
2336
|
-
|
|
2381
|
+
const exportedKeys = [...merged.keys()];
|
|
2382
|
+
const keyList = exportedKeys.length > 20 ? `${exportedKeys.slice(0, 20).join(",")} +${exportedKeys.length - 20} more` : exportedKeys.join(",");
|
|
2383
|
+
logAudit({
|
|
2384
|
+
action: "export",
|
|
2385
|
+
source,
|
|
2386
|
+
detail: `format=${format} keys=${keyList}`
|
|
2387
|
+
});
|
|
2337
2388
|
if (format === "json") {
|
|
2338
2389
|
const obj = {};
|
|
2339
2390
|
for (const [key, value] of merged) {
|
|
@@ -2665,4 +2716,4 @@ export {
|
|
|
2665
2716
|
listMemory,
|
|
2666
2717
|
forget
|
|
2667
2718
|
};
|
|
2668
|
-
//# sourceMappingURL=chunk-
|
|
2719
|
+
//# sourceMappingURL=chunk-KFILBHOY.js.map
|