@i4ctime/q-ring 0.11.7 → 0.13.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 +45 -10
- package/dist/{chunk-MQM4DLPI.js → chunk-CWV3WTPF.js} +168 -95
- package/dist/chunk-CWV3WTPF.js.map +1 -0
- package/dist/{chunk-A4RZVP3P.js → chunk-SRESNRML.js} +163 -95
- package/dist/chunk-SRESNRML.js.map +1 -0
- package/dist/dashboard-II2ILNXA.js +658 -0
- package/dist/dashboard-II2ILNXA.js.map +1 -0
- package/dist/dashboard-RLMROWIU.js +658 -0
- package/dist/dashboard-RLMROWIU.js.map +1 -0
- package/dist/index.js +712 -145
- package/dist/index.js.map +1 -1
- package/dist/mcp.js +49 -31
- package/dist/mcp.js.map +1 -1
- package/package.json +29 -14
- package/dist/chunk-A4RZVP3P.js.map +0 -1
- package/dist/chunk-MQM4DLPI.js.map +0 -1
- package/dist/dashboard-FSCJDLJX.js +0 -1313
- package/dist/dashboard-FSCJDLJX.js.map +0 -1
- package/dist/dashboard-OXUD62RV.js +0 -1313
- package/dist/dashboard-OXUD62RV.js.map +0 -1
package/README.md
CHANGED
|
@@ -44,6 +44,19 @@ yarn global add @i4ctime/q-ring
|
|
|
44
44
|
brew install i4ctime/tap/qring
|
|
45
45
|
```
|
|
46
46
|
|
|
47
|
+
### Docker (MCP server)
|
|
48
|
+
|
|
49
|
+
The repo ships a `Dockerfile` that builds the MCP server and exposes it through [`mcp-proxy`](https://github.com/punkpeye/mcp-proxy) — useful for hosted MCP deployments (e.g. Glama) or keeping the server off the host entirely:
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
git clone https://github.com/I4cTime/quantum_ring.git
|
|
53
|
+
cd quantum_ring
|
|
54
|
+
docker build -t qring-mcp .
|
|
55
|
+
docker run --rm -p 8080:8080 qring-mcp
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
> Note: inside a container there is no OS keychain (GNOME Keyring / macOS Keychain), so this path is for the MCP protocol surface, ephemeral use, and CI experiments — not for durable local secret storage. For day-to-day use install the CLI natively via one of the package managers above.
|
|
59
|
+
|
|
47
60
|
## ⚡ Quick Start
|
|
48
61
|
|
|
49
62
|
```bash
|
|
@@ -61,6 +74,12 @@ qring generate --format api-key --prefix "sk-" --save MY_KEY
|
|
|
61
74
|
|
|
62
75
|
# 5️⃣ Run a full health scan
|
|
63
76
|
qring health
|
|
77
|
+
|
|
78
|
+
# Something not working? Diagnose the install (keyring, audit, MCP wiring)
|
|
79
|
+
qring doctor
|
|
80
|
+
|
|
81
|
+
# Tab completion for your shell
|
|
82
|
+
qring completion zsh > ~/.zsh/completions/_qring # also: bash, fish
|
|
64
83
|
```
|
|
65
84
|
|
|
66
85
|
## Quantum Features
|
|
@@ -182,7 +201,7 @@ qring tunnel list
|
|
|
182
201
|
|
|
183
202
|
### Teleportation — Encrypted Sharing
|
|
184
203
|
|
|
185
|
-
Pack secrets into AES-256-GCM encrypted bundles for secure transfer between machines.
|
|
204
|
+
Pack secrets into AES-256-GCM encrypted bundles for secure transfer between machines. Keys are derived with PBKDF2-HMAC-SHA512 (210 000 iterations) from your passphrase; each bundle records its iteration count, so bundles produced by older versions still unpack.
|
|
186
205
|
|
|
187
206
|
```bash
|
|
188
207
|
# Pack secrets (prompts for passphrase)
|
|
@@ -197,7 +216,7 @@ qring teleport unpack <bundle> --dry-run
|
|
|
197
216
|
|
|
198
217
|
### Import — Bulk Secret Ingestion
|
|
199
218
|
|
|
200
|
-
Import secrets from `.env` files directly into q-ring. Supports standard dotenv syntax including comments, quoted values, and escape sequences.
|
|
219
|
+
Import secrets from `.env` files directly into q-ring. Supports standard dotenv syntax including comments, quoted values, and escape sequences. The CLI accepts either a file path or raw content; the `import_dotenv` MCP tool only accepts raw content (it never reads files from disk) so an agent can't coerce it into reading arbitrary local files.
|
|
201
220
|
|
|
202
221
|
```bash
|
|
203
222
|
# Import all secrets from a .env file
|
|
@@ -241,6 +260,9 @@ qring list --stale
|
|
|
241
260
|
|
|
242
261
|
# Glob pattern on key name
|
|
243
262
|
qring list --filter "API_*"
|
|
263
|
+
|
|
264
|
+
# Script-friendly existence check (exit 0 if present, 1 if not; decay-aware)
|
|
265
|
+
qring has OPENAI_API_KEY --quiet && echo "configured"
|
|
244
266
|
```
|
|
245
267
|
|
|
246
268
|
### Project Secret Manifest
|
|
@@ -335,7 +357,7 @@ qring hook test <id>
|
|
|
335
357
|
|
|
336
358
|
Hooks are fire-and-forget: a failing hook never blocks secret operations. The hook registry is stored at `~/.config/q-ring/hooks.json`.
|
|
337
359
|
|
|
338
|
-
**SSRF protection:** HTTP hook URLs targeting private/loopback IP ranges (`127.0.0.0/8`, `10.0.0.0/8`, `172.16.0.0/12`, `192.168.0.0/16`, `169.254.0.0/16`, `::1`, `fc00::/7`) are blocked by default. DNS
|
|
360
|
+
**SSRF protection:** HTTP hook URLs targeting private/loopback IP ranges (`127.0.0.0/8`, `10.0.0.0/8`, `172.16.0.0/12`, `192.168.0.0/16`, `169.254.0.0/16`, `::1`, `fc00::/7`) are blocked by default. DNS is checked up front *and* re-validated at connect time, so a hostname can't pass the check then rebind to a private address before the socket opens. To allow hooks targeting local services (e.g. during development), set the environment variable `Q_RING_ALLOW_PRIVATE_HOOKS=1`.
|
|
339
361
|
|
|
340
362
|
### Configurable Rotation
|
|
341
363
|
|
|
@@ -397,7 +419,7 @@ qring get DB_URL
|
|
|
397
419
|
|
|
398
420
|
### User Approvals (Zero-Trust Agent)
|
|
399
421
|
|
|
400
|
-
Protect sensitive production secrets from being read autonomously by the MCP server without explicit user approval. Each approval token is HMAC-verified, scoped, reasoned, and time-limited.
|
|
422
|
+
Protect sensitive production secrets from being read autonomously by the MCP server without explicit user approval. Each approval token is HMAC-verified, scoped, reasoned, and time-limited. The gate applies to bulk reads too — `export_secrets` and `teleport_pack` over MCP skip approval-protected keys that lack a valid grant.
|
|
401
423
|
|
|
402
424
|
```bash
|
|
403
425
|
# Mark a secret as requiring approval
|
|
@@ -508,6 +530,8 @@ qring wizard myservice --hook-exec "pm2 restart app"
|
|
|
508
530
|
|
|
509
531
|
Define project-level governance rules in `.q-ring.json` to control which MCP tools can be used, which keys are accessible, and which commands can be executed. Policy is enforced at both the MCP server and keyring level.
|
|
510
532
|
|
|
533
|
+
Over MCP, policy is resolved from the directory the server was **launched** in — not from the `projectPath` a caller passes — so an agent can't sidestep restrictions by pointing at a directory with no policy. Launch the MCP server from your project root (where `.q-ring.json` lives). Edits to `.q-ring.json` are picked up automatically (the policy cache invalidates on file change), so you don't need to restart the server.
|
|
534
|
+
|
|
511
535
|
```bash
|
|
512
536
|
# View the active policy
|
|
513
537
|
qring policy
|
|
@@ -626,7 +650,7 @@ qring agent --once
|
|
|
626
650
|
|
|
627
651
|
### Quantum Status Dashboard — Live Monitoring
|
|
628
652
|
|
|
629
|
-
Launch a real-time dashboard in your browser that turns the entire quantum subsystem into one glanceable page.
|
|
653
|
+
Launch a real-time dashboard in your browser that turns the entire quantum subsystem into one glanceable page. It's a single self-contained HTML page served locally — no cloud, no config, fully offline — built as a Preact + htm app (runtime bundled and inlined). It streams updates every 5 seconds via Server-Sent Events and diffs the DOM in place, so data refreshes without re-running entrance animations and your search input, caret, and scroll position are preserved across ticks.
|
|
630
654
|
|
|
631
655
|
What you get:
|
|
632
656
|
|
|
@@ -644,14 +668,16 @@ What you get:
|
|
|
644
668
|
|
|
645
669
|
Top-bar controls let you **pause** SSE updates (handy while reading the audit feed), **refresh** on demand, or jump to the raw JSON snapshot at `/api/status`. Keyboard shortcuts: `/` focus secrets search · `P` pause · `R` refresh.
|
|
646
670
|
|
|
671
|
+
The dashboard binds to `127.0.0.1` only and **never** exposes secret values, but it does surface key names, the audit log, and approval grants — so every route is gated by a random, per-launch token. `qring status` prints (and opens) the full URL including `?token=…`; requests without the token get a `403`. Stop the server to invalidate the token.
|
|
672
|
+
|
|
647
673
|
```bash
|
|
648
|
-
# Open the dashboard (auto-launches your browser)
|
|
674
|
+
# Open the dashboard (auto-launches your browser at http://127.0.0.1:9876/?token=…)
|
|
649
675
|
qring status
|
|
650
676
|
|
|
651
677
|
# Specify a custom port
|
|
652
678
|
qring status --port 4200
|
|
653
679
|
|
|
654
|
-
# Don't auto-open the browser
|
|
680
|
+
# Don't auto-open the browser (copy the printed tokenized URL yourself)
|
|
655
681
|
qring status --no-open
|
|
656
682
|
```
|
|
657
683
|
|
|
@@ -668,8 +694,8 @@ q-ring includes a full MCP server with 44 tools for AI agent integration.
|
|
|
668
694
|
| `set_secret` | Create or overwrite a single secret with optional TTL, per-env state, tags, rotation format |
|
|
669
695
|
| `delete_secret` | Permanently remove a secret value (not undoable from q-ring) |
|
|
670
696
|
| `has_secret` | Boolean existence check that respects decay (no audit read) |
|
|
671
|
-
| `export_secrets` | Render multiple secrets as `.env` or JSON for one-off export |
|
|
672
|
-
| `import_dotenv` | Parse `.env` text and bulk-store every key/value pair |
|
|
697
|
+
| `export_secrets` | Render multiple secrets as `.env` or JSON for one-off export (skips approval-protected keys without a grant) |
|
|
698
|
+
| `import_dotenv` | Parse `.env` text and bulk-store every key/value pair (accepts raw content only — never reads files) |
|
|
673
699
|
| `check_project` | Compare `.q-ring.json` manifest against the keyring for missing/expired/stale keys |
|
|
674
700
|
| `env_generate` | Render a complete `.env` body from the project manifest, with warnings for gaps |
|
|
675
701
|
|
|
@@ -741,7 +767,7 @@ q-ring includes a full MCP server with 44 tools for AI agent integration.
|
|
|
741
767
|
| `verify_audit_chain` | Recompute the audit hash chain and report the first break point if tampered |
|
|
742
768
|
| `export_audit` | Export audit events as jsonl, json, or csv for archival/SIEM |
|
|
743
769
|
| `health_check` | Read-only scope sweep: decay/stale/expired counts plus current anomalies |
|
|
744
|
-
| `status_dashboard` | Start a local SSE dashboard with live KPIs, secrets, hooks, and audit feed |
|
|
770
|
+
| `status_dashboard` | Start a local SSE dashboard with live KPIs, secrets, hooks, and audit feed (returns a token-gated `127.0.0.1` URL) |
|
|
745
771
|
| `agent_scan` | Multi-project health pass with optional `autoRotate` for expired secrets |
|
|
746
772
|
|
|
747
773
|
### Governance & Policy Tools
|
|
@@ -940,8 +966,17 @@ Optional per-project configuration:
|
|
|
940
966
|
- **`validationUrl`** configures the generic HTTP provider's endpoint for custom validation
|
|
941
967
|
- **`policy`** defines governance rules for MCP tool gating, key access restrictions, exec allowlists, and secret lifecycle requirements
|
|
942
968
|
|
|
969
|
+
## 📚 Docs
|
|
970
|
+
|
|
971
|
+
- [Quickstart: Claude Code](docs/quickstart-claude-code.md) · [Cursor](docs/quickstart-cursor.md) · [Kiro](docs/quickstart-kiro.md)
|
|
972
|
+
- [Troubleshooting](docs/troubleshooting.md) — keyring backends, MCP connection, approval gate, policy pinning
|
|
973
|
+
- [CLI ↔ MCP parity](docs/cli-mcp-parity.md) — every command mapped to its MCP tool
|
|
974
|
+
- [Releasing](docs/releasing.md) — tag-driven release flow
|
|
975
|
+
|
|
943
976
|
## Contributing
|
|
944
977
|
|
|
978
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md) for the full guide (dev environment, conventions, files to keep in sync). The short version:
|
|
979
|
+
|
|
945
980
|
- Run **`pnpm run lint`**, **`pnpm run typecheck`**, and **`pnpm run test:ci`** before opening a PR.
|
|
946
981
|
- Tests or sandboxes can point the audit log elsewhere with **`QRING_AUDIT_DIR`** (directory is created if missing); default is `~/.config/q-ring/audit.jsonl`.
|
|
947
982
|
- Optional local pre-commit: **`qring hook:install`** (uses this package’s `precommit` hook when `qring` is on your `PATH`).
|
|
@@ -147,7 +147,7 @@ function checkDecay(envelope) {
|
|
|
147
147
|
const remaining = expires - now;
|
|
148
148
|
const lifetimePercent = totalLifetime > 0 ? Math.round(elapsed / totalLifetime * 100) : 100;
|
|
149
149
|
const secondsRemaining = Math.floor(remaining / 1e3);
|
|
150
|
-
let timeRemaining =
|
|
150
|
+
let timeRemaining = "expired";
|
|
151
151
|
if (remaining > 0) {
|
|
152
152
|
const days = Math.floor(remaining / 864e5);
|
|
153
153
|
const hours = Math.floor(remaining % 864e5 / 36e5);
|
|
@@ -155,8 +155,6 @@ function checkDecay(envelope) {
|
|
|
155
155
|
if (days > 0) timeRemaining = `${days}d ${hours}h`;
|
|
156
156
|
else if (hours > 0) timeRemaining = `${hours}h ${minutes}m`;
|
|
157
157
|
else timeRemaining = `${minutes}m`;
|
|
158
|
-
} else {
|
|
159
|
-
timeRemaining = "expired";
|
|
160
158
|
}
|
|
161
159
|
return {
|
|
162
160
|
isExpired: remaining <= 0,
|
|
@@ -555,87 +553,11 @@ import { randomUUID } from "crypto";
|
|
|
555
553
|
// src/utils/http-request.ts
|
|
556
554
|
import { request as httpsRequest } from "https";
|
|
557
555
|
import { request as httpRequestPlain } from "http";
|
|
558
|
-
var DEFAULT_TIMEOUT_MS = 1e4;
|
|
559
|
-
var DEFAULT_MAX_RESPONSE_BYTES = 65536;
|
|
560
|
-
function httpRequest(opts) {
|
|
561
|
-
const {
|
|
562
|
-
url,
|
|
563
|
-
method = "GET",
|
|
564
|
-
headers = {},
|
|
565
|
-
body,
|
|
566
|
-
timeoutMs = DEFAULT_TIMEOUT_MS,
|
|
567
|
-
maxResponseBytes = DEFAULT_MAX_RESPONSE_BYTES
|
|
568
|
-
} = opts;
|
|
569
|
-
return new Promise((resolve, reject) => {
|
|
570
|
-
const parsed = new URL(url);
|
|
571
|
-
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
|
572
|
-
reject(new Error(`Unsupported URL protocol: ${parsed.protocol}`));
|
|
573
|
-
return;
|
|
574
|
-
}
|
|
575
|
-
const reqFn = parsed.protocol === "https:" ? httpsRequest : httpRequestPlain;
|
|
576
|
-
const reqHeaders = { ...headers };
|
|
577
|
-
if (body && !reqHeaders["Content-Length"]) {
|
|
578
|
-
reqHeaders["Content-Length"] = Buffer.byteLength(body);
|
|
579
|
-
}
|
|
580
|
-
const req = reqFn(
|
|
581
|
-
url,
|
|
582
|
-
{ method, headers: reqHeaders, timeout: timeoutMs },
|
|
583
|
-
(res) => {
|
|
584
|
-
const chunks = [];
|
|
585
|
-
let totalBytes = 0;
|
|
586
|
-
let truncated = false;
|
|
587
|
-
res.on("data", (chunk) => {
|
|
588
|
-
totalBytes += chunk.length;
|
|
589
|
-
if (totalBytes > maxResponseBytes) {
|
|
590
|
-
truncated = true;
|
|
591
|
-
res.destroy();
|
|
592
|
-
return;
|
|
593
|
-
}
|
|
594
|
-
chunks.push(chunk);
|
|
595
|
-
});
|
|
596
|
-
let settled = false;
|
|
597
|
-
const settle = (result) => {
|
|
598
|
-
if (!settled) {
|
|
599
|
-
settled = true;
|
|
600
|
-
resolve(result);
|
|
601
|
-
}
|
|
602
|
-
};
|
|
603
|
-
const fail = (err) => {
|
|
604
|
-
if (!settled) {
|
|
605
|
-
settled = true;
|
|
606
|
-
reject(err);
|
|
607
|
-
}
|
|
608
|
-
};
|
|
609
|
-
res.on("error", (err) => fail(new Error(`Response error: ${err.message}`)));
|
|
610
|
-
res.on("end", () => {
|
|
611
|
-
settle({
|
|
612
|
-
statusCode: res.statusCode ?? 0,
|
|
613
|
-
body: Buffer.concat(chunks).toString("utf8"),
|
|
614
|
-
truncated
|
|
615
|
-
});
|
|
616
|
-
});
|
|
617
|
-
res.on("close", () => {
|
|
618
|
-
settle({
|
|
619
|
-
statusCode: res.statusCode ?? 0,
|
|
620
|
-
body: Buffer.concat(chunks).toString("utf8"),
|
|
621
|
-
truncated
|
|
622
|
-
});
|
|
623
|
-
});
|
|
624
|
-
}
|
|
625
|
-
);
|
|
626
|
-
req.on("error", (err) => reject(new Error(`Network error: ${err.message}`)));
|
|
627
|
-
req.on("timeout", () => {
|
|
628
|
-
req.destroy();
|
|
629
|
-
reject(new Error("Request timed out"));
|
|
630
|
-
});
|
|
631
|
-
if (body) req.write(body);
|
|
632
|
-
req.end();
|
|
633
|
-
});
|
|
634
|
-
}
|
|
635
556
|
|
|
636
557
|
// src/core/ssrf.ts
|
|
637
558
|
import { lookup } from "dns/promises";
|
|
638
559
|
import * as dns from "dns";
|
|
560
|
+
import { lookup as dnsLookup } from "dns";
|
|
639
561
|
import { isIPv4, isIPv6 } from "net";
|
|
640
562
|
function lookupAddressesSync(hostname2) {
|
|
641
563
|
const lookupSync2 = dns.lookupSync;
|
|
@@ -682,6 +604,36 @@ async function checkSSRF(url) {
|
|
|
682
604
|
}
|
|
683
605
|
return null;
|
|
684
606
|
}
|
|
607
|
+
function guardedLookup(hostname2, options, callback) {
|
|
608
|
+
if (process.env.Q_RING_ALLOW_PRIVATE_HOOKS === "1") {
|
|
609
|
+
dnsLookup(
|
|
610
|
+
hostname2,
|
|
611
|
+
options,
|
|
612
|
+
callback
|
|
613
|
+
);
|
|
614
|
+
return;
|
|
615
|
+
}
|
|
616
|
+
dnsLookup(
|
|
617
|
+
hostname2,
|
|
618
|
+
options,
|
|
619
|
+
(err, address, family) => {
|
|
620
|
+
if (err) return callback(err, address, family);
|
|
621
|
+
const list = Array.isArray(address) ? address : [{ address, family: family ?? 0 }];
|
|
622
|
+
for (const a of list) {
|
|
623
|
+
if (isPrivateIP(a.address)) {
|
|
624
|
+
const blocked = Object.assign(
|
|
625
|
+
new Error(
|
|
626
|
+
`Blocked: "${hostname2}" resolved to private address ${a.address} at connect time.`
|
|
627
|
+
),
|
|
628
|
+
{ code: "EQRINGSSRF" }
|
|
629
|
+
);
|
|
630
|
+
return callback(blocked, address, family);
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
callback(null, address, family);
|
|
634
|
+
}
|
|
635
|
+
);
|
|
636
|
+
}
|
|
685
637
|
function checkJitHttpProvisionUrl(url) {
|
|
686
638
|
if (process.env.Q_RING_ALLOW_PRIVATE_HOOKS === "1") return null;
|
|
687
639
|
try {
|
|
@@ -719,6 +671,91 @@ function checkJitHttpProvisionUrl(url) {
|
|
|
719
671
|
return null;
|
|
720
672
|
}
|
|
721
673
|
|
|
674
|
+
// src/utils/http-request.ts
|
|
675
|
+
var DEFAULT_TIMEOUT_MS = 1e4;
|
|
676
|
+
var DEFAULT_MAX_RESPONSE_BYTES = 65536;
|
|
677
|
+
function httpRequest(opts) {
|
|
678
|
+
const {
|
|
679
|
+
url,
|
|
680
|
+
method = "GET",
|
|
681
|
+
headers = {},
|
|
682
|
+
body,
|
|
683
|
+
timeoutMs = DEFAULT_TIMEOUT_MS,
|
|
684
|
+
maxResponseBytes = DEFAULT_MAX_RESPONSE_BYTES
|
|
685
|
+
} = opts;
|
|
686
|
+
return new Promise((resolve, reject) => {
|
|
687
|
+
const parsed = new URL(url);
|
|
688
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
|
689
|
+
reject(new Error(`Unsupported URL protocol: ${parsed.protocol}`));
|
|
690
|
+
return;
|
|
691
|
+
}
|
|
692
|
+
const reqFn = parsed.protocol === "https:" ? httpsRequest : httpRequestPlain;
|
|
693
|
+
const reqHeaders = { ...headers };
|
|
694
|
+
if (body && !reqHeaders["Content-Length"]) {
|
|
695
|
+
reqHeaders["Content-Length"] = Buffer.byteLength(body);
|
|
696
|
+
}
|
|
697
|
+
const req = reqFn(
|
|
698
|
+
url,
|
|
699
|
+
{
|
|
700
|
+
method,
|
|
701
|
+
headers: reqHeaders,
|
|
702
|
+
timeout: timeoutMs,
|
|
703
|
+
// Re-validate the resolved IP at connect time (DNS-rebinding guard).
|
|
704
|
+
lookup: guardedLookup
|
|
705
|
+
},
|
|
706
|
+
(res) => {
|
|
707
|
+
const chunks = [];
|
|
708
|
+
let totalBytes = 0;
|
|
709
|
+
let truncated = false;
|
|
710
|
+
res.on("data", (chunk) => {
|
|
711
|
+
totalBytes += chunk.length;
|
|
712
|
+
if (totalBytes > maxResponseBytes) {
|
|
713
|
+
truncated = true;
|
|
714
|
+
res.destroy();
|
|
715
|
+
return;
|
|
716
|
+
}
|
|
717
|
+
chunks.push(chunk);
|
|
718
|
+
});
|
|
719
|
+
let settled = false;
|
|
720
|
+
const settle = (result) => {
|
|
721
|
+
if (!settled) {
|
|
722
|
+
settled = true;
|
|
723
|
+
resolve(result);
|
|
724
|
+
}
|
|
725
|
+
};
|
|
726
|
+
const fail = (err) => {
|
|
727
|
+
if (!settled) {
|
|
728
|
+
settled = true;
|
|
729
|
+
reject(err);
|
|
730
|
+
}
|
|
731
|
+
};
|
|
732
|
+
res.on("error", (err) => fail(new Error(`Response error: ${err.message}`)));
|
|
733
|
+
res.on("end", () => {
|
|
734
|
+
settle({
|
|
735
|
+
statusCode: res.statusCode ?? 0,
|
|
736
|
+
body: Buffer.concat(chunks).toString("utf8"),
|
|
737
|
+
truncated
|
|
738
|
+
});
|
|
739
|
+
});
|
|
740
|
+
res.on("close", () => {
|
|
741
|
+
settle({
|
|
742
|
+
statusCode: res.statusCode ?? 0,
|
|
743
|
+
body: Buffer.concat(chunks).toString("utf8"),
|
|
744
|
+
truncated
|
|
745
|
+
});
|
|
746
|
+
});
|
|
747
|
+
}
|
|
748
|
+
);
|
|
749
|
+
req.on("error", (err) => reject(new Error(`Network error: ${err.message}`)));
|
|
750
|
+
req.on("timeout", () => {
|
|
751
|
+
req.destroy();
|
|
752
|
+
reject(new Error("Request timed out"));
|
|
753
|
+
});
|
|
754
|
+
if (body) req.write(body);
|
|
755
|
+
req.end();
|
|
756
|
+
});
|
|
757
|
+
}
|
|
758
|
+
|
|
722
759
|
// src/core/hooks.ts
|
|
723
760
|
function getRegistryPath2() {
|
|
724
761
|
const dir = join5(homedir3(), ".config", "q-ring");
|
|
@@ -1065,15 +1102,33 @@ function listApprovals() {
|
|
|
1065
1102
|
}
|
|
1066
1103
|
|
|
1067
1104
|
// src/core/policy.ts
|
|
1105
|
+
import { statSync as statSync2 } from "fs";
|
|
1106
|
+
import { join as join7 } from "path";
|
|
1068
1107
|
var cachedPolicy = null;
|
|
1108
|
+
var policyRoot = null;
|
|
1109
|
+
function setPolicyRoot(root) {
|
|
1110
|
+
policyRoot = root;
|
|
1111
|
+
cachedPolicy = null;
|
|
1112
|
+
}
|
|
1113
|
+
function resolvePolicyPath(projectPath) {
|
|
1114
|
+
return policyRoot ?? projectPath ?? process.cwd();
|
|
1115
|
+
}
|
|
1116
|
+
function configMtime(pp) {
|
|
1117
|
+
try {
|
|
1118
|
+
return statSync2(join7(pp, ".q-ring.json")).mtimeMs;
|
|
1119
|
+
} catch {
|
|
1120
|
+
return 0;
|
|
1121
|
+
}
|
|
1122
|
+
}
|
|
1069
1123
|
function loadPolicy(projectPath) {
|
|
1070
|
-
const pp = projectPath
|
|
1071
|
-
|
|
1124
|
+
const pp = resolvePolicyPath(projectPath);
|
|
1125
|
+
const mtimeMs = configMtime(pp);
|
|
1126
|
+
if (cachedPolicy && cachedPolicy.path === pp && cachedPolicy.mtimeMs === mtimeMs) {
|
|
1072
1127
|
return cachedPolicy.policy;
|
|
1073
1128
|
}
|
|
1074
1129
|
const config = readProjectConfig(pp);
|
|
1075
1130
|
const policy = config?.policy ?? {};
|
|
1076
|
-
cachedPolicy = { path: pp, policy };
|
|
1131
|
+
cachedPolicy = { path: pp, mtimeMs, policy };
|
|
1077
1132
|
return policy;
|
|
1078
1133
|
}
|
|
1079
1134
|
function checkToolPolicy(toolName, projectPath) {
|
|
@@ -1161,7 +1216,13 @@ function checkExecPolicy(command, projectPath) {
|
|
|
1161
1216
|
const policy = loadPolicy(projectPath);
|
|
1162
1217
|
if (!policy.exec) return { allowed: true, policySource: "no-policy" };
|
|
1163
1218
|
if (policy.exec.denyCommands) {
|
|
1164
|
-
const denied = policy.exec.denyCommands.find((d) =>
|
|
1219
|
+
const denied = policy.exec.denyCommands.find((d) => {
|
|
1220
|
+
const pattern = new RegExp(
|
|
1221
|
+
`(^|[\\s/])${d.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}(\\s|$)`,
|
|
1222
|
+
"i"
|
|
1223
|
+
);
|
|
1224
|
+
return pattern.test(command);
|
|
1225
|
+
});
|
|
1165
1226
|
if (denied) {
|
|
1166
1227
|
return {
|
|
1167
1228
|
allowed: false,
|
|
@@ -1171,7 +1232,8 @@ function checkExecPolicy(command, projectPath) {
|
|
|
1171
1232
|
}
|
|
1172
1233
|
}
|
|
1173
1234
|
if (policy.exec.allowCommands) {
|
|
1174
|
-
const
|
|
1235
|
+
const normalized = command.trimStart();
|
|
1236
|
+
const allowed = policy.exec.allowCommands.some((a) => normalized.startsWith(a));
|
|
1175
1237
|
if (!allowed) {
|
|
1176
1238
|
return {
|
|
1177
1239
|
allowed: false,
|
|
@@ -1198,7 +1260,7 @@ function getPolicySummary(projectPath) {
|
|
|
1198
1260
|
// src/core/keyring.ts
|
|
1199
1261
|
import { mkdirSync as mkdirSync5, writeFileSync as writeFileSync4, unlinkSync } from "fs";
|
|
1200
1262
|
import { homedir as homedir5 } from "os";
|
|
1201
|
-
import { join as
|
|
1263
|
+
import { join as join8 } from "path";
|
|
1202
1264
|
import { Entry, findCredentials } from "@napi-rs/keyring";
|
|
1203
1265
|
|
|
1204
1266
|
// src/utils/hash.ts
|
|
@@ -1324,7 +1386,7 @@ var awsStsProvider = {
|
|
|
1324
1386
|
expiresAt: creds.Expiration
|
|
1325
1387
|
};
|
|
1326
1388
|
} catch (err) {
|
|
1327
|
-
throw new Error(`AWS STS provision failed: ${err instanceof Error ? err.message : String(err)}
|
|
1389
|
+
throw new Error(`AWS STS provision failed: ${err instanceof Error ? err.message : String(err)}`, { cause: err });
|
|
1328
1390
|
}
|
|
1329
1391
|
}
|
|
1330
1392
|
};
|
|
@@ -1392,7 +1454,7 @@ req.end();
|
|
|
1392
1454
|
expiresAt: new Date(Date.now() + expiresInSeconds * 1e3).toISOString()
|
|
1393
1455
|
};
|
|
1394
1456
|
} catch (err) {
|
|
1395
|
-
throw new Error(`HTTP provision failed: ${err instanceof Error ? err.message : String(err)}
|
|
1457
|
+
throw new Error(`HTTP provision failed: ${err instanceof Error ? err.message : String(err)}`, { cause: err });
|
|
1396
1458
|
}
|
|
1397
1459
|
}
|
|
1398
1460
|
};
|
|
@@ -1402,10 +1464,10 @@ registry.register(httpProvider);
|
|
|
1402
1464
|
|
|
1403
1465
|
// src/core/keyring.ts
|
|
1404
1466
|
function withJitEnvelopeLock(service, key, fn) {
|
|
1405
|
-
const dir =
|
|
1467
|
+
const dir = join8(homedir5(), ".config", "q-ring", "jit-locks");
|
|
1406
1468
|
mkdirSync5(dir, { recursive: true });
|
|
1407
1469
|
const safe = Buffer.from(`${service}\0${key}`, "utf8").toString("base64url");
|
|
1408
|
-
const lockPath =
|
|
1470
|
+
const lockPath = join8(dir, `${safe}.lock`);
|
|
1409
1471
|
const deadline = Date.now() + 8e3;
|
|
1410
1472
|
while (Date.now() < deadline) {
|
|
1411
1473
|
try {
|
|
@@ -1789,6 +1851,16 @@ function exportSecrets(opts = {}) {
|
|
|
1789
1851
|
if (entry.envelope) {
|
|
1790
1852
|
const decay = checkDecay(entry.envelope);
|
|
1791
1853
|
if (decay.isExpired) continue;
|
|
1854
|
+
if (source === "mcp" && entry.envelope.meta.requiresApproval && !hasApproval(entry.key, entry.scope)) {
|
|
1855
|
+
logAudit({
|
|
1856
|
+
action: "read",
|
|
1857
|
+
key: entry.key,
|
|
1858
|
+
scope: entry.scope,
|
|
1859
|
+
source,
|
|
1860
|
+
detail: "blocked: requires user approval (export)"
|
|
1861
|
+
});
|
|
1862
|
+
continue;
|
|
1863
|
+
}
|
|
1792
1864
|
const value = collapseValue(entry.envelope, env);
|
|
1793
1865
|
if (value !== null) {
|
|
1794
1866
|
rawValues.set(entry.key, value);
|
|
@@ -1920,7 +1992,7 @@ function tunnelList() {
|
|
|
1920
1992
|
|
|
1921
1993
|
// src/core/memory.ts
|
|
1922
1994
|
import { existsSync as existsSync6, readFileSync as readFileSync7, writeFileSync as writeFileSync5, mkdirSync as mkdirSync6 } from "fs";
|
|
1923
|
-
import { join as
|
|
1995
|
+
import { join as join9 } from "path";
|
|
1924
1996
|
import { homedir as homedir6, hostname, userInfo } from "os";
|
|
1925
1997
|
import { createCipheriv, createDecipheriv, createHash as createHash3, randomBytes as randomBytes3 } from "crypto";
|
|
1926
1998
|
import { Entry as Entry2 } from "@napi-rs/keyring";
|
|
@@ -1928,14 +2000,14 @@ var MEMORY_FILE = "agent-memory.enc";
|
|
|
1928
2000
|
var KEYRING_SERVICE = "qring-memory-key";
|
|
1929
2001
|
var KEYRING_ACCOUNT = "encryption-key";
|
|
1930
2002
|
function getMemoryDir() {
|
|
1931
|
-
const dir =
|
|
2003
|
+
const dir = join9(homedir6(), ".config", "q-ring");
|
|
1932
2004
|
if (!existsSync6(dir)) {
|
|
1933
2005
|
mkdirSync6(dir, { recursive: true });
|
|
1934
2006
|
}
|
|
1935
2007
|
return dir;
|
|
1936
2008
|
}
|
|
1937
2009
|
function getMemoryPath() {
|
|
1938
|
-
return
|
|
2010
|
+
return join9(getMemoryDir(), MEMORY_FILE);
|
|
1939
2011
|
}
|
|
1940
2012
|
function deriveLegacyKey() {
|
|
1941
2013
|
const fingerprint = `qring-memory:${hostname()}:${userInfo().username}`;
|
|
@@ -2043,14 +2115,15 @@ export {
|
|
|
2043
2115
|
exportAudit,
|
|
2044
2116
|
detectAnomalies,
|
|
2045
2117
|
listEntanglements,
|
|
2046
|
-
httpRequest,
|
|
2047
2118
|
checkSSRF,
|
|
2119
|
+
httpRequest,
|
|
2048
2120
|
registerHook,
|
|
2049
2121
|
removeHook,
|
|
2050
2122
|
listHooks,
|
|
2051
2123
|
fireHooks,
|
|
2052
2124
|
listApprovals,
|
|
2053
2125
|
registry,
|
|
2126
|
+
setPolicyRoot,
|
|
2054
2127
|
checkToolPolicy,
|
|
2055
2128
|
checkKeyReadPolicy,
|
|
2056
2129
|
checkExecPolicy,
|
|
@@ -2074,4 +2147,4 @@ export {
|
|
|
2074
2147
|
listMemory,
|
|
2075
2148
|
forget
|
|
2076
2149
|
};
|
|
2077
|
-
//# sourceMappingURL=chunk-
|
|
2150
|
+
//# sourceMappingURL=chunk-CWV3WTPF.js.map
|