@suveren/gateway 0.24.0 → 0.25.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.
- package/dist/mcp-server/{chunk-APIQIEUA.mjs → chunk-PU4VQEEA.mjs} +1 -0
- package/dist/mcp-server/http.mjs +415 -151
- package/dist/mcp-server/report-verify-cli.mjs +1 -1
- package/dist/ui/assets/index-Ccst5MQ7.css +1 -0
- package/dist/ui/assets/index-CxVyYf7C.js +120 -0
- package/dist/ui/index.html +2 -2
- package/package.json +1 -1
- package/dist/ui/assets/index-BydbZb1A.js +0 -120
- package/dist/ui/assets/index-C604QQYJ.css +0 -1
package/dist/mcp-server/http.mjs
CHANGED
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
computeMetric,
|
|
16
16
|
fingerprintOf,
|
|
17
17
|
formatDateTime,
|
|
18
|
+
getIdentityDirectory,
|
|
18
19
|
glossaryUsage,
|
|
19
20
|
issuerFromPublicKeyHex,
|
|
20
21
|
mandateFields,
|
|
@@ -31,7 +32,7 @@ import {
|
|
|
31
32
|
sanitizeReport,
|
|
32
33
|
suggestedFilename,
|
|
33
34
|
ticketFields
|
|
34
|
-
} from "./chunk-
|
|
35
|
+
} from "./chunk-PU4VQEEA.mjs";
|
|
35
36
|
|
|
36
37
|
// bin/http.ts
|
|
37
38
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
@@ -987,6 +988,19 @@ var SPClient = class {
|
|
|
987
988
|
const data = await res.json();
|
|
988
989
|
return data.proposals;
|
|
989
990
|
}
|
|
991
|
+
/**
|
|
992
|
+
* RJ4 — the same proposals a human just rejected, sent back for a change,
|
|
993
|
+
* or replaced with their own mandate (all three outcomes live under
|
|
994
|
+
* `status: 'rejected'`, see `ProposalOutcome`). Polled on the SAME tick as
|
|
995
|
+
* {@link getCommittedProposals} (bin/http.ts) so the outcome outbox learns
|
|
996
|
+
* about them without a second AS polling loop.
|
|
997
|
+
*/
|
|
998
|
+
async getRejectedProposals() {
|
|
999
|
+
const res = await this.fetch("/api/proposals?status=rejected");
|
|
1000
|
+
if (!res.ok) throw new Error(`SP rejected proposals request failed: ${res.status}`);
|
|
1001
|
+
const data = await res.json();
|
|
1002
|
+
return data.proposals;
|
|
1003
|
+
}
|
|
990
1004
|
/**
|
|
991
1005
|
* Look up ONE proposal by id, whatever its status. `null` when the AS has no
|
|
992
1006
|
* such proposal (or the caller may not see it).
|
|
@@ -2132,6 +2146,163 @@ var PreviewLog = class {
|
|
|
2132
2146
|
}
|
|
2133
2147
|
};
|
|
2134
2148
|
|
|
2149
|
+
// src/lib/outcome-outbox.ts
|
|
2150
|
+
import { mkdirSync as mkdirSync9, readFileSync as readFileSync12, writeFileSync as writeFileSync9, existsSync as existsSync11, unlinkSync as unlinkSync5 } from "fs";
|
|
2151
|
+
import { homedir as homedir8 } from "os";
|
|
2152
|
+
import { dirname as dirname9, join as join11 } from "path";
|
|
2153
|
+
import { createCipheriv as createCipheriv5, createDecipheriv as createDecipheriv5, randomBytes as randomBytes5 } from "crypto";
|
|
2154
|
+
var DEFAULT_DIR8 = process.env.SUVEREN_DATA_DIR ?? join11(homedir8(), ".suveren");
|
|
2155
|
+
var MAX_AGE_MS3 = 7 * 24 * 60 * 60 * 1e3;
|
|
2156
|
+
function outcomeEntryFor(proposal, submittedHere, who, nowMs) {
|
|
2157
|
+
if (!submittedHere) return null;
|
|
2158
|
+
const by = proposal.approverRejectedBy ?? proposal.rejectedBy;
|
|
2159
|
+
if (!by || typeof by.at !== "number") return null;
|
|
2160
|
+
const decidedAt = by.at * 1e3;
|
|
2161
|
+
if (nowMs - decidedAt > MAX_AGE_MS3) return null;
|
|
2162
|
+
return { proposalId: proposal.id, outcome: by.outcome ?? "rejected", note: by.note, who, decidedAt };
|
|
2163
|
+
}
|
|
2164
|
+
var OutcomeOutbox = class {
|
|
2165
|
+
entries = [];
|
|
2166
|
+
baseDir;
|
|
2167
|
+
vaultKey = null;
|
|
2168
|
+
constructor(filePath) {
|
|
2169
|
+
this.baseDir = filePath && filePath.endsWith(".json") ? dirname9(filePath) : filePath ?? DEFAULT_DIR8;
|
|
2170
|
+
this.loadPlaintext();
|
|
2171
|
+
}
|
|
2172
|
+
/** Mirrors DenialLog.setVaultKey — migrates plaintext -> encrypted once, in place. */
|
|
2173
|
+
setVaultKey(key) {
|
|
2174
|
+
this.vaultKey = key;
|
|
2175
|
+
if (existsSync11(this.encryptedFilePath)) {
|
|
2176
|
+
this.loadEncrypted();
|
|
2177
|
+
} else if (this.entries.length > 0) {
|
|
2178
|
+
this.persistEncrypted();
|
|
2179
|
+
if (existsSync11(this.plaintextFilePath)) {
|
|
2180
|
+
try {
|
|
2181
|
+
unlinkSync5(this.plaintextFilePath);
|
|
2182
|
+
} catch {
|
|
2183
|
+
}
|
|
2184
|
+
}
|
|
2185
|
+
}
|
|
2186
|
+
}
|
|
2187
|
+
/**
|
|
2188
|
+
* Record a resolved outcome, UNLESS this proposal is already known here
|
|
2189
|
+
* (delivered or not) — the poller rechecks the AS's rejected list on
|
|
2190
|
+
* every tick, and a proposal stays in that list well past the moment this
|
|
2191
|
+
* gateway first learned about it, so without this guard the same outcome
|
|
2192
|
+
* would be queued again right after it was delivered.
|
|
2193
|
+
*/
|
|
2194
|
+
recordIfNew(entry) {
|
|
2195
|
+
if (this.entries.some((e) => e.proposalId === entry.proposalId)) return;
|
|
2196
|
+
this.entries.push({ ...entry });
|
|
2197
|
+
this.prune(Date.now());
|
|
2198
|
+
this.persist();
|
|
2199
|
+
}
|
|
2200
|
+
/** Undelivered entries, oldest first — what the next tool reply should carry. */
|
|
2201
|
+
getUndelivered() {
|
|
2202
|
+
return this.entries.filter((e) => e.deliveredAt === void 0).sort((a, b) => a.decidedAt - b.decidedAt);
|
|
2203
|
+
}
|
|
2204
|
+
/** Mark these proposals' entries delivered (idempotent). Always persists from disk state. */
|
|
2205
|
+
markDelivered(proposalIds) {
|
|
2206
|
+
if (proposalIds.length === 0) return;
|
|
2207
|
+
const ids = new Set(proposalIds);
|
|
2208
|
+
const now = Date.now();
|
|
2209
|
+
let changed = false;
|
|
2210
|
+
for (const e of this.entries) {
|
|
2211
|
+
if (ids.has(e.proposalId) && e.deliveredAt === void 0) {
|
|
2212
|
+
e.deliveredAt = now;
|
|
2213
|
+
changed = true;
|
|
2214
|
+
}
|
|
2215
|
+
}
|
|
2216
|
+
if (changed) this.persist();
|
|
2217
|
+
}
|
|
2218
|
+
/** Diagnostic / tests. */
|
|
2219
|
+
get size() {
|
|
2220
|
+
return this.entries.length;
|
|
2221
|
+
}
|
|
2222
|
+
// ─── Retention ────────────────────────────────────────────────────────────
|
|
2223
|
+
prune(now) {
|
|
2224
|
+
const cutoff = now - MAX_AGE_MS3;
|
|
2225
|
+
this.entries = this.entries.filter((e) => e.decidedAt >= cutoff);
|
|
2226
|
+
}
|
|
2227
|
+
// ─── Encryption (mirrors DenialLog / ExecutionLog) ─────────────────────────
|
|
2228
|
+
encrypt(plaintext) {
|
|
2229
|
+
if (!this.vaultKey) throw new Error("No vault key");
|
|
2230
|
+
const iv = randomBytes5(12);
|
|
2231
|
+
const cipher = createCipheriv5("aes-256-gcm", this.vaultKey, iv);
|
|
2232
|
+
const enc = Buffer.concat([cipher.update(plaintext, "utf8"), cipher.final()]);
|
|
2233
|
+
return { iv: iv.toString("hex"), ciphertext: enc.toString("hex"), tag: cipher.getAuthTag().toString("hex") };
|
|
2234
|
+
}
|
|
2235
|
+
decrypt(blob) {
|
|
2236
|
+
if (!this.vaultKey) throw new Error("No vault key");
|
|
2237
|
+
const decipher = createDecipheriv5("aes-256-gcm", this.vaultKey, Buffer.from(blob.iv, "hex"));
|
|
2238
|
+
decipher.setAuthTag(Buffer.from(blob.tag, "hex"));
|
|
2239
|
+
return Buffer.concat([decipher.update(Buffer.from(blob.ciphertext, "hex")), decipher.final()]).toString("utf8");
|
|
2240
|
+
}
|
|
2241
|
+
get plaintextFilePath() {
|
|
2242
|
+
return join11(this.baseDir, "outcome-outbox.json");
|
|
2243
|
+
}
|
|
2244
|
+
get encryptedFilePath() {
|
|
2245
|
+
return join11(this.baseDir, "outcome-outbox.enc.json");
|
|
2246
|
+
}
|
|
2247
|
+
loadPlaintext() {
|
|
2248
|
+
if (!existsSync11(this.plaintextFilePath)) {
|
|
2249
|
+
mkdirSync9(this.baseDir, { recursive: true });
|
|
2250
|
+
return;
|
|
2251
|
+
}
|
|
2252
|
+
try {
|
|
2253
|
+
const data = JSON.parse(readFileSync12(this.plaintextFilePath, "utf-8"));
|
|
2254
|
+
this.entries = data.entries ?? [];
|
|
2255
|
+
this.prune(Date.now());
|
|
2256
|
+
} catch {
|
|
2257
|
+
console.error(`[OutcomeOutbox] Could not parse ${this.plaintextFilePath}, starting fresh`);
|
|
2258
|
+
this.entries = [];
|
|
2259
|
+
}
|
|
2260
|
+
}
|
|
2261
|
+
loadEncrypted() {
|
|
2262
|
+
if (!existsSync11(this.encryptedFilePath)) return;
|
|
2263
|
+
try {
|
|
2264
|
+
const data = JSON.parse(readFileSync12(this.encryptedFilePath, "utf-8"));
|
|
2265
|
+
const storeFile = JSON.parse(this.decrypt(data.blob));
|
|
2266
|
+
this.entries = storeFile.entries ?? [];
|
|
2267
|
+
this.prune(Date.now());
|
|
2268
|
+
} catch (err) {
|
|
2269
|
+
console.error(`[OutcomeOutbox] Could not decrypt ${this.encryptedFilePath}:`, err);
|
|
2270
|
+
this.entries = [];
|
|
2271
|
+
}
|
|
2272
|
+
}
|
|
2273
|
+
persist() {
|
|
2274
|
+
if (this.vaultKey) this.persistEncrypted();
|
|
2275
|
+
else this.persistPlaintext();
|
|
2276
|
+
}
|
|
2277
|
+
persistPlaintext() {
|
|
2278
|
+
const data = { version: 1, entries: this.entries };
|
|
2279
|
+
mkdirSync9(this.baseDir, { recursive: true, mode: 448 });
|
|
2280
|
+
writeFileSync9(this.plaintextFilePath, JSON.stringify(data, null, 2), { encoding: "utf-8", mode: 384 });
|
|
2281
|
+
}
|
|
2282
|
+
persistEncrypted() {
|
|
2283
|
+
const storeFile = { version: 1, entries: this.entries };
|
|
2284
|
+
const data = { version: 1, blob: this.encrypt(JSON.stringify(storeFile)) };
|
|
2285
|
+
mkdirSync9(this.baseDir, { recursive: true, mode: 448 });
|
|
2286
|
+
writeFileSync9(this.encryptedFilePath, JSON.stringify(data, null, 2), { encoding: "utf-8", mode: 384 });
|
|
2287
|
+
}
|
|
2288
|
+
};
|
|
2289
|
+
function formatOutcomeLine(e) {
|
|
2290
|
+
const what = e.outcome === "change_needed" ? `SENT BACK FOR A CHANGE by ${e.who}: "${e.note ?? ""}"` : e.outcome === "replaced" ? `REPLACED by the person's own mandate` : `REJECTED by ${e.who}`;
|
|
2291
|
+
return `[Suveren gateway] Since your last call: proposal ${e.proposalId} was ${what}.`;
|
|
2292
|
+
}
|
|
2293
|
+
function wrapWithOutcomeOutbox(outbox, handler) {
|
|
2294
|
+
return async (...args) => {
|
|
2295
|
+
const result = await handler(...args);
|
|
2296
|
+
if (!outbox) return result;
|
|
2297
|
+
const pending = outbox.getUndelivered();
|
|
2298
|
+
if (pending.length === 0) return result;
|
|
2299
|
+
if (!result || !Array.isArray(result.content)) return result;
|
|
2300
|
+
outbox.markDelivered(pending.map((e) => e.proposalId));
|
|
2301
|
+
const note = pending.map(formatOutcomeLine).join("\n");
|
|
2302
|
+
return { ...result, content: [...result.content, { type: "text", text: note }] };
|
|
2303
|
+
};
|
|
2304
|
+
}
|
|
2305
|
+
|
|
2135
2306
|
// src/lib/gatekeeper.ts
|
|
2136
2307
|
import { verify } from "@hap/core";
|
|
2137
2308
|
var MCPGatekeeper = class {
|
|
@@ -2224,10 +2395,10 @@ var MCPGatekeeper = class {
|
|
|
2224
2395
|
};
|
|
2225
2396
|
|
|
2226
2397
|
// src/lib/report/report-store.ts
|
|
2227
|
-
import { mkdirSync as
|
|
2228
|
-
import { homedir as
|
|
2229
|
-
import { dirname as
|
|
2230
|
-
import { createCipheriv as
|
|
2398
|
+
import { mkdirSync as mkdirSync10, readFileSync as readFileSync13, writeFileSync as writeFileSync10, existsSync as existsSync12, unlinkSync as unlinkSync6 } from "fs";
|
|
2399
|
+
import { homedir as homedir9 } from "os";
|
|
2400
|
+
import { dirname as dirname10, join as join12 } from "path";
|
|
2401
|
+
import { createCipheriv as createCipheriv6, createDecipheriv as createDecipheriv6, randomBytes as randomBytes6 } from "crypto";
|
|
2231
2402
|
|
|
2232
2403
|
// src/lib/report/record-resolvers.ts
|
|
2233
2404
|
function findById(rows, id) {
|
|
@@ -2535,29 +2706,29 @@ async function buildCoverage(caseElements, getExport, archive, referenced, windo
|
|
|
2535
2706
|
}
|
|
2536
2707
|
|
|
2537
2708
|
// src/lib/report/report-store.ts
|
|
2538
|
-
var
|
|
2709
|
+
var DEFAULT_DIR9 = process.env.SUVEREN_DATA_DIR ?? join12(homedir9(), ".suveren");
|
|
2539
2710
|
var ReportStore = class {
|
|
2540
2711
|
report = null;
|
|
2541
2712
|
baseDir;
|
|
2542
2713
|
vaultKey = null;
|
|
2543
2714
|
constructor(filePath) {
|
|
2544
2715
|
if (filePath && filePath.endsWith(".json")) {
|
|
2545
|
-
this.baseDir =
|
|
2716
|
+
this.baseDir = dirname10(filePath);
|
|
2546
2717
|
} else {
|
|
2547
|
-
this.baseDir = filePath ??
|
|
2718
|
+
this.baseDir = filePath ?? DEFAULT_DIR9;
|
|
2548
2719
|
}
|
|
2549
2720
|
this.loadPlaintext();
|
|
2550
2721
|
}
|
|
2551
2722
|
/** Set the vault key for encryption. Triggers migration from plaintext if needed. */
|
|
2552
2723
|
setVaultKey(key) {
|
|
2553
2724
|
this.vaultKey = key;
|
|
2554
|
-
if (
|
|
2725
|
+
if (existsSync12(this.encryptedFilePath)) {
|
|
2555
2726
|
this.loadEncrypted();
|
|
2556
2727
|
} else if (this.report !== null) {
|
|
2557
2728
|
this.persistEncrypted();
|
|
2558
|
-
if (
|
|
2729
|
+
if (existsSync12(this.plaintextFilePath)) {
|
|
2559
2730
|
try {
|
|
2560
|
-
|
|
2731
|
+
unlinkSync6(this.plaintextFilePath);
|
|
2561
2732
|
} catch {
|
|
2562
2733
|
}
|
|
2563
2734
|
}
|
|
@@ -2570,7 +2741,7 @@ var ReportStore = class {
|
|
|
2570
2741
|
* "the report exists but is locked".
|
|
2571
2742
|
*/
|
|
2572
2743
|
isLocked() {
|
|
2573
|
-
return this.vaultKey === null &&
|
|
2744
|
+
return this.vaultKey === null && existsSync12(this.encryptedFilePath);
|
|
2574
2745
|
}
|
|
2575
2746
|
/** The stored, already-verified report — or null if none has been saved
|
|
2576
2747
|
* (and the store is not locked; check `isLocked()` first). */
|
|
@@ -2604,49 +2775,49 @@ var ReportStore = class {
|
|
|
2604
2775
|
// ─── Encryption helpers ─────────────────────────────────────────────────
|
|
2605
2776
|
encrypt(plaintext) {
|
|
2606
2777
|
if (!this.vaultKey) throw new Error("No vault key");
|
|
2607
|
-
const iv =
|
|
2608
|
-
const cipher =
|
|
2778
|
+
const iv = randomBytes6(12);
|
|
2779
|
+
const cipher = createCipheriv6("aes-256-gcm", this.vaultKey, iv);
|
|
2609
2780
|
const encrypted = Buffer.concat([cipher.update(plaintext, "utf8"), cipher.final()]);
|
|
2610
2781
|
const tag = cipher.getAuthTag();
|
|
2611
2782
|
return { iv: iv.toString("hex"), ciphertext: encrypted.toString("hex"), tag: tag.toString("hex") };
|
|
2612
2783
|
}
|
|
2613
2784
|
decrypt(blob) {
|
|
2614
2785
|
if (!this.vaultKey) throw new Error("No vault key");
|
|
2615
|
-
const decipher =
|
|
2786
|
+
const decipher = createDecipheriv6("aes-256-gcm", this.vaultKey, Buffer.from(blob.iv, "hex"));
|
|
2616
2787
|
decipher.setAuthTag(Buffer.from(blob.tag, "hex"));
|
|
2617
2788
|
const decrypted = Buffer.concat([decipher.update(Buffer.from(blob.ciphertext, "hex")), decipher.final()]);
|
|
2618
2789
|
return decrypted.toString("utf8");
|
|
2619
2790
|
}
|
|
2620
2791
|
// ─── File paths ─────────────────────────────────────────────────────────
|
|
2621
2792
|
get plaintextFilePath() {
|
|
2622
|
-
return
|
|
2793
|
+
return join12(this.baseDir, "report.json");
|
|
2623
2794
|
}
|
|
2624
2795
|
get encryptedFilePath() {
|
|
2625
|
-
return
|
|
2796
|
+
return join12(this.baseDir, "report.enc.json");
|
|
2626
2797
|
}
|
|
2627
2798
|
// ─── Load / Persist ─────────────────────────────────────────────────────
|
|
2628
2799
|
loadPlaintext() {
|
|
2629
|
-
if (!
|
|
2630
|
-
|
|
2800
|
+
if (!existsSync12(this.plaintextFilePath)) {
|
|
2801
|
+
mkdirSync10(this.baseDir, { recursive: true });
|
|
2631
2802
|
return;
|
|
2632
2803
|
}
|
|
2633
2804
|
try {
|
|
2634
|
-
const raw =
|
|
2805
|
+
const raw = readFileSync13(this.plaintextFilePath, "utf-8");
|
|
2635
2806
|
const parsed = JSON.parse(raw);
|
|
2636
2807
|
this.report = parsed.report ?? null;
|
|
2637
2808
|
} catch {
|
|
2638
2809
|
console.error(`[ReportStore] Could not parse ${this.plaintextFilePath} \u2014 preserving as .corrupt`);
|
|
2639
2810
|
try {
|
|
2640
|
-
|
|
2811
|
+
writeFileSync10(`${this.plaintextFilePath}.corrupt-${Date.now()}`, readFileSync13(this.plaintextFilePath));
|
|
2641
2812
|
} catch {
|
|
2642
2813
|
}
|
|
2643
2814
|
this.report = null;
|
|
2644
2815
|
}
|
|
2645
2816
|
}
|
|
2646
2817
|
loadEncrypted() {
|
|
2647
|
-
if (!
|
|
2818
|
+
if (!existsSync12(this.encryptedFilePath)) return;
|
|
2648
2819
|
try {
|
|
2649
|
-
const raw =
|
|
2820
|
+
const raw = readFileSync13(this.encryptedFilePath, "utf-8");
|
|
2650
2821
|
const data = JSON.parse(raw);
|
|
2651
2822
|
const decrypted = this.decrypt(data.blob);
|
|
2652
2823
|
const parsed = JSON.parse(decrypted);
|
|
@@ -2654,7 +2825,7 @@ var ReportStore = class {
|
|
|
2654
2825
|
} catch (err) {
|
|
2655
2826
|
console.error(`[ReportStore] Could not decrypt ${this.encryptedFilePath} \u2014 preserving as .corrupt:`, err);
|
|
2656
2827
|
try {
|
|
2657
|
-
|
|
2828
|
+
writeFileSync10(`${this.encryptedFilePath}.corrupt-${Date.now()}`, readFileSync13(this.encryptedFilePath));
|
|
2658
2829
|
} catch {
|
|
2659
2830
|
}
|
|
2660
2831
|
this.report = null;
|
|
@@ -2669,16 +2840,16 @@ var ReportStore = class {
|
|
|
2669
2840
|
}
|
|
2670
2841
|
persistPlaintext() {
|
|
2671
2842
|
const data = { version: 1, report: this.report };
|
|
2672
|
-
|
|
2673
|
-
|
|
2843
|
+
mkdirSync10(this.baseDir, { recursive: true, mode: 448 });
|
|
2844
|
+
writeFileSync10(this.plaintextFilePath, JSON.stringify(data, null, 2), { encoding: "utf-8", mode: 384 });
|
|
2674
2845
|
}
|
|
2675
2846
|
persistEncrypted() {
|
|
2676
2847
|
const data = {
|
|
2677
2848
|
version: 1,
|
|
2678
2849
|
blob: this.encrypt(JSON.stringify({ version: 1, report: this.report }))
|
|
2679
2850
|
};
|
|
2680
|
-
|
|
2681
|
-
|
|
2851
|
+
mkdirSync10(this.baseDir, { recursive: true, mode: 448 });
|
|
2852
|
+
writeFileSync10(this.encryptedFilePath, JSON.stringify(data, null, 2), { encoding: "utf-8", mode: 384 });
|
|
2682
2853
|
}
|
|
2683
2854
|
};
|
|
2684
2855
|
|
|
@@ -2701,6 +2872,9 @@ var SharedState = class {
|
|
|
2701
2872
|
proposalSubmissions;
|
|
2702
2873
|
/** AU3 — local, content-free record of preview reads. See preview-log.ts. */
|
|
2703
2874
|
previewLog;
|
|
2875
|
+
/** RJ4 — rejection/change-needed/replaced outcomes awaiting one-time
|
|
2876
|
+
* delivery to the AI on its next tool call. See outcome-outbox.ts. */
|
|
2877
|
+
outcomeOutbox;
|
|
2704
2878
|
gatekeeper;
|
|
2705
2879
|
/**
|
|
2706
2880
|
* V7 — set by {@link checkAsCompat} (called once at process start, and
|
|
@@ -2735,6 +2909,7 @@ var SharedState = class {
|
|
|
2735
2909
|
this.executionJournal = new ExecutionJournal(gateStorePath);
|
|
2736
2910
|
this.proposalSubmissions = new ProposalSubmissionStore(gateStorePath);
|
|
2737
2911
|
this.previewLog = new PreviewLog(gateStorePath);
|
|
2912
|
+
this.outcomeOutbox = new OutcomeOutbox(gateStorePath);
|
|
2738
2913
|
this.gatekeeper = new MCPGatekeeper(this.cache);
|
|
2739
2914
|
}
|
|
2740
2915
|
/**
|
|
@@ -2923,29 +3098,29 @@ function formatConsumptionFull(entries) {
|
|
|
2923
3098
|
}
|
|
2924
3099
|
|
|
2925
3100
|
// src/lib/context-loader.ts
|
|
2926
|
-
import { readFileSync as
|
|
2927
|
-
import { homedir as
|
|
2928
|
-
import { join as
|
|
2929
|
-
var
|
|
3101
|
+
import { readFileSync as readFileSync14, existsSync as existsSync13, mkdirSync as mkdirSync11, writeFileSync as writeFileSync11, renameSync as renameSync4 } from "fs";
|
|
3102
|
+
import { homedir as homedir10 } from "os";
|
|
3103
|
+
import { join as join13, dirname as dirname11 } from "path";
|
|
3104
|
+
var DEFAULT_DIR10 = process.env.SUVEREN_DATA_DIR ?? join13(homedir10(), ".suveren");
|
|
2930
3105
|
var CONTEXT_MAX_BYTES = 16 * 1024;
|
|
2931
3106
|
function contextFilePath(dataDir2) {
|
|
2932
|
-
return
|
|
3107
|
+
return join13(dataDir2 ?? DEFAULT_DIR10, "context.md");
|
|
2933
3108
|
}
|
|
2934
3109
|
function writeContextFile(content, dataDir2) {
|
|
2935
3110
|
const bytes = Buffer.byteLength(content, "utf-8");
|
|
2936
3111
|
if (bytes > CONTEXT_MAX_BYTES) throw new Error(`the brief is ${bytes} bytes; the limit is ${CONTEXT_MAX_BYTES}.`);
|
|
2937
3112
|
const filePath = contextFilePath(dataDir2);
|
|
2938
|
-
|
|
3113
|
+
mkdirSync11(dirname11(filePath), { recursive: true });
|
|
2939
3114
|
const tmpPath = `${filePath}.tmp`;
|
|
2940
|
-
|
|
3115
|
+
writeFileSync11(tmpPath, content, "utf-8");
|
|
2941
3116
|
renameSync4(tmpPath, filePath);
|
|
2942
3117
|
}
|
|
2943
3118
|
var BRIEF_MAX_CHARS = 16 * 1024;
|
|
2944
3119
|
function readContextFile(dataDir2) {
|
|
2945
3120
|
const filePath = contextFilePath(dataDir2);
|
|
2946
|
-
if (!
|
|
3121
|
+
if (!existsSync13(filePath)) return null;
|
|
2947
3122
|
try {
|
|
2948
|
-
const content =
|
|
3123
|
+
const content = readFileSync14(filePath, "utf-8").trim();
|
|
2949
3124
|
return content || null;
|
|
2950
3125
|
} catch {
|
|
2951
3126
|
return null;
|
|
@@ -3135,8 +3310,8 @@ function attachTicketId(tool, args, ticketId) {
|
|
|
3135
3310
|
import { computeContentHash as computeContentHash2 } from "@hap/core";
|
|
3136
3311
|
|
|
3137
3312
|
// src/lib/manifest-loader.ts
|
|
3138
|
-
import { readFileSync as
|
|
3139
|
-
import { join as
|
|
3313
|
+
import { readFileSync as readFileSync15, existsSync as existsSync14 } from "fs";
|
|
3314
|
+
import { join as join14, resolve } from "path";
|
|
3140
3315
|
var EXACT_SEMVER_RE = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/;
|
|
3141
3316
|
function isExactSemver(value) {
|
|
3142
3317
|
return typeof value === "string" && EXACT_SEMVER_RE.test(value);
|
|
@@ -3205,25 +3380,25 @@ function invalidManifestPreviewReason(manifest) {
|
|
|
3205
3380
|
var manifests = /* @__PURE__ */ new Map();
|
|
3206
3381
|
function loadManifests(integrationsDir) {
|
|
3207
3382
|
const dir = resolve(
|
|
3208
|
-
integrationsDir ?? process.env.SUVEREN_MANIFESTS_DIR ??
|
|
3383
|
+
integrationsDir ?? process.env.SUVEREN_MANIFESTS_DIR ?? join14(import.meta.dirname ?? __dirname, "..", "..", "..", "..", "content", "integrations")
|
|
3209
3384
|
);
|
|
3210
|
-
const indexPath =
|
|
3211
|
-
if (!
|
|
3385
|
+
const indexPath = join14(dir, "index.json");
|
|
3386
|
+
if (!existsSync14(indexPath)) {
|
|
3212
3387
|
console.error(`[ManifestLoader] No index.json found at ${indexPath}, skipping manifest loading`);
|
|
3213
3388
|
return 0;
|
|
3214
3389
|
}
|
|
3215
3390
|
let index;
|
|
3216
3391
|
try {
|
|
3217
|
-
index = JSON.parse(
|
|
3392
|
+
index = JSON.parse(readFileSync15(indexPath, "utf-8"));
|
|
3218
3393
|
} catch (err) {
|
|
3219
3394
|
console.error(`[ManifestLoader] Failed to parse ${indexPath}:`, err);
|
|
3220
3395
|
return 0;
|
|
3221
3396
|
}
|
|
3222
3397
|
let loaded = 0;
|
|
3223
3398
|
for (const [id, relativePath] of Object.entries(index.integrations)) {
|
|
3224
|
-
const manifestPath =
|
|
3399
|
+
const manifestPath = join14(dir, relativePath);
|
|
3225
3400
|
try {
|
|
3226
|
-
const manifest = JSON.parse(
|
|
3401
|
+
const manifest = JSON.parse(readFileSync15(manifestPath, "utf-8"));
|
|
3227
3402
|
const pinReason = invalidNpmPinReason(manifest);
|
|
3228
3403
|
if (pinReason) {
|
|
3229
3404
|
console.error(`[ManifestLoader] Refusing manifest ${id} (${manifestPath}): ${pinReason}`);
|
|
@@ -3858,6 +4033,11 @@ function zeroCappedBound(profileId, bounds, actionType) {
|
|
|
3858
4033
|
}
|
|
3859
4034
|
return null;
|
|
3860
4035
|
}
|
|
4036
|
+
function toolAlwaysListed(tool, isBuiltin) {
|
|
4037
|
+
if (tool.internal || !tool.gating?.profile) return false;
|
|
4038
|
+
if (tool.gating.hideUnlessAuthorized || tool.gating.category === "disabled") return false;
|
|
4039
|
+
return !isBuiltin(tool.integrationId);
|
|
4040
|
+
}
|
|
3861
4041
|
function toolIsAuthorizedForDisplay(tool, matchingAuths) {
|
|
3862
4042
|
if (!tool.gating?.hideUnlessAuthorized) return true;
|
|
3863
4043
|
if (matchingAuths.length === 0) return false;
|
|
@@ -4033,7 +4213,7 @@ function createGatedToolHandlerInner(tool, integrationManager2, state2) {
|
|
|
4033
4213
|
return {
|
|
4034
4214
|
content: [{
|
|
4035
4215
|
type: "text",
|
|
4036
|
-
text: refusals.length > 0 ? `Refused: ${reasons.charAt(0).toUpperCase() + reasons.slice(1)} No ticket was requested.` :
|
|
4216
|
+
text: refusals.length > 0 ? `Refused: ${reasons.charAt(0).toUpperCase() + reasons.slice(1)} No ticket was requested.` : noMandateRefusal(tool.namespacedName, profile)
|
|
4037
4217
|
}],
|
|
4038
4218
|
isError: true
|
|
4039
4219
|
};
|
|
@@ -4645,20 +4825,21 @@ Proposal ID: ${proposal.id}. Use check-pending-commitments to track status.`
|
|
|
4645
4825
|
}
|
|
4646
4826
|
});
|
|
4647
4827
|
}
|
|
4648
|
-
function
|
|
4828
|
+
function noMandateRefusal(toolName, profile) {
|
|
4829
|
+
return `Refused: no mandate covers ${toolName} (${shortProfile(profile)}). Nothing was done. Ask the person to grant a ${shortProfile(profile)} mandate in the gateway, then try again. No ticket was requested.`;
|
|
4830
|
+
}
|
|
4831
|
+
function shortProfile(profileId) {
|
|
4832
|
+
const parts = profileId.replace(/@.*$/, "").split("/");
|
|
4833
|
+
return parts[parts.length - 1];
|
|
4834
|
+
}
|
|
4835
|
+
function buildProxiedToolDescription(tool) {
|
|
4649
4836
|
if (!tool.gating || !tool.gating.profile) {
|
|
4650
4837
|
return `[Suveren: no gating config] ${tool.description}`;
|
|
4651
4838
|
}
|
|
4652
4839
|
const profile = tool.gating.profile;
|
|
4653
|
-
const
|
|
4654
|
-
const hasAuth = auths.some(
|
|
4655
|
-
(a) => a.complete && profileMatches(a.profileId, profile)
|
|
4656
|
-
);
|
|
4657
|
-
if (!hasAuth) {
|
|
4658
|
-
return `[Suveren: ${profile} \u2014 no active authorization] ${tool.description}`;
|
|
4659
|
-
}
|
|
4840
|
+
const condition = `works only under a ${shortProfile(profile)} mandate, check list-authorizations`;
|
|
4660
4841
|
if (tool.gating.category === "read") {
|
|
4661
|
-
return `[Suveren: ${profile} \u2014 read] ${tool.description}`;
|
|
4842
|
+
return `[Suveren: ${profile} \u2014 read; ${condition}] ${tool.description}`;
|
|
4662
4843
|
}
|
|
4663
4844
|
const parts = [];
|
|
4664
4845
|
if (tool.gating.staticExecution?.action_type) {
|
|
@@ -4671,7 +4852,7 @@ function buildProxiedToolDescription(tool, state2) {
|
|
|
4671
4852
|
parts.push(`${mappedFields.join(", ")} checked`);
|
|
4672
4853
|
}
|
|
4673
4854
|
const tag = parts.length > 0 ? parts.join(", ") : "gated";
|
|
4674
|
-
return `[Suveren: ${profile} \u2014 ${tag}] ${tool.description}`;
|
|
4855
|
+
return `[Suveren: ${profile} \u2014 ${tag}; ${condition}] ${tool.description}`;
|
|
4675
4856
|
}
|
|
4676
4857
|
|
|
4677
4858
|
// src/lib/agent-visibility.ts
|
|
@@ -4727,7 +4908,8 @@ function buildMandateBrief(opts) {
|
|
|
4727
4908
|
"You are connected to Suveren \u2014 the gateway that gates every privileged tool call you make.",
|
|
4728
4909
|
"Suveren implements the bounded-authority model from the open Human Agency Protocol (HAP).",
|
|
4729
4910
|
"You have bounded authorities granted by human decision owners.",
|
|
4730
|
-
"You MUST stay within these bounds \u2014 the Gatekeeper will reject actions that exceed them."
|
|
4911
|
+
"You MUST stay within these bounds \u2014 the Gatekeeper will reject actions that exceed them.",
|
|
4912
|
+
"Tools are listed even where no mandate covers them yet. If a call is refused because no mandate covers it, tell the person and ask them to grant one \u2014 do not retry it or work around it with another tool."
|
|
4731
4913
|
];
|
|
4732
4914
|
if (opts.asVersionRefusal) {
|
|
4733
4915
|
lines.push("");
|
|
@@ -4823,7 +5005,7 @@ function buildCapabilityMap(profileId, integrationManager2) {
|
|
|
4823
5005
|
const readOnly = [];
|
|
4824
5006
|
const undescribed = [];
|
|
4825
5007
|
for (const tool of allTools) {
|
|
4826
|
-
if (!tool.gating || !tool.gating.profile) {
|
|
5008
|
+
if (tool.internal || !tool.gating || !tool.gating.profile) {
|
|
4827
5009
|
continue;
|
|
4828
5010
|
}
|
|
4829
5011
|
if (!profileMatches(tool.gating.profile, shortName2) && tool.gating.profile !== profileId) {
|
|
@@ -4864,6 +5046,20 @@ function buildCapabilityMap(profileId, integrationManager2) {
|
|
|
4864
5046
|
}
|
|
4865
5047
|
return lines.join("\n");
|
|
4866
5048
|
}
|
|
5049
|
+
function listedWithoutMandate(authorizations, integrationManager2) {
|
|
5050
|
+
if (!integrationManager2) return [];
|
|
5051
|
+
const isBuiltin = (id) => integrationManager2.isBuiltin?.(id) ?? false;
|
|
5052
|
+
const byProfile = /* @__PURE__ */ new Map();
|
|
5053
|
+
for (const tool of integrationManager2.getAllTools()) {
|
|
5054
|
+
if (!toolAlwaysListed(tool, isBuiltin)) continue;
|
|
5055
|
+
const profile = tool.gating.profile;
|
|
5056
|
+
if (authorizations.some((a) => a.complete && profileMatches(a.profileId, profile))) continue;
|
|
5057
|
+
const short = shortProfileName2(profile);
|
|
5058
|
+
byProfile.set(short, [...byProfile.get(short) ?? [], tool.namespacedName]);
|
|
5059
|
+
}
|
|
5060
|
+
return [...byProfile.entries()].map(([profile, tools]) => ` ${profile}: ${tools.join(", ")}`);
|
|
5061
|
+
}
|
|
5062
|
+
var LISTED_WITHOUT_MANDATE_HEADING = "Listed but not usable yet \u2014 no mandate covers these; every call is refused until the person grants one:";
|
|
4867
5063
|
function listAuthorizationsHandler(state2, integrationManager2, contextDir) {
|
|
4868
5064
|
return async (args) => {
|
|
4869
5065
|
if (!state2.spClient.isUnlocked()) {
|
|
@@ -4872,13 +5068,11 @@ function listAuthorizationsHandler(state2, integrationManager2, contextDir) {
|
|
|
4872
5068
|
const authorizations = agentVisibleAuthorizations(state2.getEnrichedAuthorizations(), integrationManager2);
|
|
4873
5069
|
const now = Math.floor(Date.now() / 1e3);
|
|
4874
5070
|
const domain = args?.domain;
|
|
5071
|
+
const notYet = listedWithoutMandate(authorizations, integrationManager2);
|
|
4875
5072
|
if (authorizations.length === 0) {
|
|
4876
|
-
|
|
4877
|
-
|
|
4878
|
-
|
|
4879
|
-
text: "No active authorizations. A decision owner must grant authority via the Authority UI."
|
|
4880
|
-
}]
|
|
4881
|
-
};
|
|
5073
|
+
const text = ["No active authorizations \u2014 no tool is usable yet. Ask the person to grant a mandate in the gateway."];
|
|
5074
|
+
if (notYet.length > 0) text.push("", LISTED_WITHOUT_MANDATE_HEADING, ...notYet);
|
|
5075
|
+
return { content: [{ type: "text", text: text.join("\n") }] };
|
|
4882
5076
|
}
|
|
4883
5077
|
if (domain) {
|
|
4884
5078
|
const matching = authorizations.filter((auth) => {
|
|
@@ -4999,7 +5193,7 @@ function listAuthorizationsHandler(state2, integrationManager2, contextDir) {
|
|
|
4999
5193
|
output.push("");
|
|
5000
5194
|
}
|
|
5001
5195
|
if (active.length > 0) {
|
|
5002
|
-
output.push("Active authorizations:");
|
|
5196
|
+
output.push("Active authorizations (usable now):");
|
|
5003
5197
|
output.push(...active);
|
|
5004
5198
|
}
|
|
5005
5199
|
if (pending.length > 0) {
|
|
@@ -5007,6 +5201,11 @@ function listAuthorizationsHandler(state2, integrationManager2, contextDir) {
|
|
|
5007
5201
|
output.push("Pending (missing owners):");
|
|
5008
5202
|
output.push(...pending);
|
|
5009
5203
|
}
|
|
5204
|
+
if (notYet.length > 0) {
|
|
5205
|
+
if (output.length > 0) output.push("");
|
|
5206
|
+
output.push(LISTED_WITHOUT_MANDATE_HEADING);
|
|
5207
|
+
output.push(...notYet);
|
|
5208
|
+
}
|
|
5010
5209
|
return {
|
|
5011
5210
|
content: [{
|
|
5012
5211
|
type: "text",
|
|
@@ -5110,6 +5309,22 @@ function listIntegrationsHandler(state2, integrationManager2) {
|
|
|
5110
5309
|
|
|
5111
5310
|
// src/tools/commitments.ts
|
|
5112
5311
|
import { ContentBindingError as ContentBindingError2 } from "@hap/core";
|
|
5312
|
+
|
|
5313
|
+
// src/lib/who-label.ts
|
|
5314
|
+
var UNKNOWN_DECIDER = "the person";
|
|
5315
|
+
async function resolveWhoLabel(archive, domainKey, userId) {
|
|
5316
|
+
if (!archive) return UNKNOWN_DECIDER;
|
|
5317
|
+
try {
|
|
5318
|
+
const dir = await getIdentityDirectory(archive);
|
|
5319
|
+
const did = dir.domainDids.get(domainKey) ?? dir.domainDids.get(userId);
|
|
5320
|
+
const name = did ? dir.names.get(did) : void 0;
|
|
5321
|
+
return name ?? UNKNOWN_DECIDER;
|
|
5322
|
+
} catch {
|
|
5323
|
+
return UNKNOWN_DECIDER;
|
|
5324
|
+
}
|
|
5325
|
+
}
|
|
5326
|
+
|
|
5327
|
+
// src/tools/commitments.ts
|
|
5113
5328
|
function buildSkippedProposalNote(proposalId) {
|
|
5114
5329
|
return `Proposal ${proposalId} was approved, but was submitted from another device/installation of this operator (or one whose local record is gone) \u2014 this gateway will not execute it. Re-run the same request here to execute it from this gateway.`;
|
|
5115
5330
|
}
|
|
@@ -5408,11 +5623,34 @@ Result: ${JSON.stringify(match.executionResult, null, 2)}` : " The action ran; t
|
|
|
5408
5623
|
}
|
|
5409
5624
|
if (match.status === "rejected") {
|
|
5410
5625
|
const by = match.approverRejectedBy ?? match.rejectedBy;
|
|
5411
|
-
const
|
|
5626
|
+
const outcome = by?.outcome ?? "rejected";
|
|
5627
|
+
const legacyReason = match.approverRejectedBy?.reason;
|
|
5628
|
+
const who = by ? await resolveWhoLabel(
|
|
5629
|
+
state2.receiptArchive,
|
|
5630
|
+
"domain" in by ? by.domain : by.userId,
|
|
5631
|
+
by.userId
|
|
5632
|
+
) : "a person";
|
|
5633
|
+
if (outcome === "change_needed") {
|
|
5634
|
+
return {
|
|
5635
|
+
content: [{
|
|
5636
|
+
type: "text",
|
|
5637
|
+
text: `Proposal ${match.id} was SENT BACK FOR A CHANGE by ${who}: "${by?.note ?? ""}". The action did not run. You may adjust it and propose again \u2014 a new proposal the person approves again. Do not resend the same values.`
|
|
5638
|
+
}]
|
|
5639
|
+
};
|
|
5640
|
+
}
|
|
5641
|
+
if (outcome === "replaced") {
|
|
5642
|
+
return {
|
|
5643
|
+
content: [{
|
|
5644
|
+
type: "text",
|
|
5645
|
+
text: `Proposal ${match.id} was REPLACED: the person signed their own version of this mandate on the mandate screen. Nothing was created from this proposal. Use the person's mandate \u2014 check list-authorizations.`
|
|
5646
|
+
}]
|
|
5647
|
+
};
|
|
5648
|
+
}
|
|
5649
|
+
const reasonSuffix = legacyReason ? `: "${legacyReason}".` : ".";
|
|
5412
5650
|
return {
|
|
5413
5651
|
content: [{
|
|
5414
5652
|
type: "text",
|
|
5415
|
-
text: `Proposal ${match.id} was REJECTED
|
|
5653
|
+
text: `Proposal ${match.id} was REJECTED by ${who}${reasonSuffix} The action did not run and must not be retried. Ask the person before proposing anything similar.`
|
|
5416
5654
|
}]
|
|
5417
5655
|
};
|
|
5418
5656
|
}
|
|
@@ -5561,6 +5799,7 @@ function createMcpServer(state2, integrationManager2) {
|
|
|
5561
5799
|
{ name: "suveren-gateway", version: "0.1.0" },
|
|
5562
5800
|
{ instructions }
|
|
5563
5801
|
);
|
|
5802
|
+
const withOutcomeOutbox = (handler) => wrapWithOutcomeOutbox(state2.outcomeOutbox, handler);
|
|
5564
5803
|
server.registerTool(
|
|
5565
5804
|
"list-authorizations",
|
|
5566
5805
|
{
|
|
@@ -5569,7 +5808,7 @@ function createMcpServer(state2, integrationManager2) {
|
|
|
5569
5808
|
domain: z2.string().optional().describe('Profile domain to show full details for (e.g., "charge", "deploy"). Omit for compact overview.')
|
|
5570
5809
|
}
|
|
5571
5810
|
},
|
|
5572
|
-
listAuthorizationsHandler(state2, integrationManager2)
|
|
5811
|
+
withOutcomeOutbox(listAuthorizationsHandler(state2, integrationManager2))
|
|
5573
5812
|
);
|
|
5574
5813
|
server.registerTool(
|
|
5575
5814
|
"check-pending-mandates",
|
|
@@ -5579,7 +5818,7 @@ function createMcpServer(state2, integrationManager2) {
|
|
|
5579
5818
|
domain: z2.string().describe(`The owner's domain (e.g., "compliance")`)
|
|
5580
5819
|
}
|
|
5581
5820
|
},
|
|
5582
|
-
checkPendingHandler(cache)
|
|
5821
|
+
withOutcomeOutbox(checkPendingHandler(cache))
|
|
5583
5822
|
);
|
|
5584
5823
|
server.registerTool(
|
|
5585
5824
|
"list-integrations",
|
|
@@ -5587,7 +5826,7 @@ function createMcpServer(state2, integrationManager2) {
|
|
|
5587
5826
|
description: "List all running integrations and their authorization status. Returns a compact overview \u2014 use list-authorizations(domain) for full details on a specific profile.",
|
|
5588
5827
|
inputSchema: {}
|
|
5589
5828
|
},
|
|
5590
|
-
listIntegrationsHandler(state2, integrationManager2)
|
|
5829
|
+
withOutcomeOutbox(listIntegrationsHandler(state2, integrationManager2))
|
|
5591
5830
|
);
|
|
5592
5831
|
server.registerTool(
|
|
5593
5832
|
"check-pending-commitments",
|
|
@@ -5597,7 +5836,7 @@ function createMcpServer(state2, integrationManager2) {
|
|
|
5597
5836
|
proposal_id: z2.string().optional().describe("Specific proposal ID to check. Omit to see all.")
|
|
5598
5837
|
}
|
|
5599
5838
|
},
|
|
5600
|
-
checkPendingCommitmentsHandler(state2, integrationManager2)
|
|
5839
|
+
withOutcomeOutbox(checkPendingCommitmentsHandler(state2, integrationManager2))
|
|
5601
5840
|
);
|
|
5602
5841
|
const proxiedTools = /* @__PURE__ */ new Map();
|
|
5603
5842
|
function registerProxiedTools() {
|
|
@@ -5623,7 +5862,7 @@ function createMcpServer(state2, integrationManager2) {
|
|
|
5623
5862
|
}
|
|
5624
5863
|
const handler = createGatedToolHandler(tool, integrationManager2, state2);
|
|
5625
5864
|
const zodShape = jsonSchemaToZodShape(tool.inputSchema);
|
|
5626
|
-
const description = buildProxiedToolDescription(tool
|
|
5865
|
+
const description = buildProxiedToolDescription(tool);
|
|
5627
5866
|
try {
|
|
5628
5867
|
const registered = server.registerTool(
|
|
5629
5868
|
tool.namespacedName,
|
|
@@ -5631,7 +5870,10 @@ function createMcpServer(state2, integrationManager2) {
|
|
|
5631
5870
|
description,
|
|
5632
5871
|
...Object.keys(zodShape).length > 0 ? { inputSchema: zodShape } : {}
|
|
5633
5872
|
},
|
|
5634
|
-
|
|
5873
|
+
// Built-in tools (reports, setup) never carry the outcome line: a
|
|
5874
|
+
// report's output is evidence and must not mix in a notice (nor the
|
|
5875
|
+
// decider's identity); setup tools are not the working agent's.
|
|
5876
|
+
integrationManager2?.isBuiltin?.(tool.integrationId) ? handler : withOutcomeOutbox(handler)
|
|
5635
5877
|
);
|
|
5636
5878
|
proxiedTools.set(tool.namespacedName, { tool, registered });
|
|
5637
5879
|
} catch (err) {
|
|
@@ -5641,10 +5883,11 @@ function createMcpServer(state2, integrationManager2) {
|
|
|
5641
5883
|
}
|
|
5642
5884
|
function refreshTools() {
|
|
5643
5885
|
const auths = state2.getEnrichedAuthorizations();
|
|
5886
|
+
const isBuiltin = (id) => integrationManager2?.isBuiltin?.(id) ?? false;
|
|
5644
5887
|
for (const [, { tool, registered }] of proxiedTools) {
|
|
5645
|
-
|
|
5646
|
-
|
|
5647
|
-
if (tool.gating?.profile) {
|
|
5888
|
+
if (toolAlwaysListed(tool, isBuiltin)) {
|
|
5889
|
+
registered.enable();
|
|
5890
|
+
} else if (tool.gating?.profile) {
|
|
5648
5891
|
const matchingAuths = auths.filter(
|
|
5649
5892
|
(a) => a.complete && profileMatches(a.profileId, tool.gating.profile)
|
|
5650
5893
|
);
|
|
@@ -5663,21 +5906,21 @@ function createMcpServer(state2, integrationManager2) {
|
|
|
5663
5906
|
}
|
|
5664
5907
|
|
|
5665
5908
|
// src/lib/agent-contact.ts
|
|
5666
|
-
import { existsSync as
|
|
5667
|
-
import { dirname as
|
|
5909
|
+
import { existsSync as existsSync15, mkdirSync as mkdirSync12, readFileSync as readFileSync16, renameSync as renameSync5, writeFileSync as writeFileSync12 } from "fs";
|
|
5910
|
+
import { dirname as dirname12, join as join15 } from "path";
|
|
5668
5911
|
var FILE = "agent-contact.json";
|
|
5669
5912
|
var REWRITE_AFTER_MS = 6e4;
|
|
5670
5913
|
var AgentContactStore = class {
|
|
5671
5914
|
path;
|
|
5672
5915
|
current;
|
|
5673
5916
|
constructor(dataDir2) {
|
|
5674
|
-
this.path =
|
|
5917
|
+
this.path = join15(dataDir2, FILE);
|
|
5675
5918
|
this.current = this.load();
|
|
5676
5919
|
}
|
|
5677
5920
|
load() {
|
|
5678
|
-
if (!
|
|
5921
|
+
if (!existsSync15(this.path)) return null;
|
|
5679
5922
|
try {
|
|
5680
|
-
const v = JSON.parse(
|
|
5923
|
+
const v = JSON.parse(readFileSync16(this.path, "utf8"));
|
|
5681
5924
|
if (typeof v.clientName !== "string" || typeof v.firstSeenAt !== "string" || typeof v.lastSeenAt !== "string") return null;
|
|
5682
5925
|
return { clientName: v.clientName, clientVersion: v.clientVersion, firstSeenAt: v.firstSeenAt, lastSeenAt: v.lastSeenAt };
|
|
5683
5926
|
} catch {
|
|
@@ -5700,9 +5943,9 @@ var AgentContactStore = class {
|
|
|
5700
5943
|
};
|
|
5701
5944
|
if (!due) return;
|
|
5702
5945
|
try {
|
|
5703
|
-
|
|
5946
|
+
mkdirSync12(dirname12(this.path), { recursive: true });
|
|
5704
5947
|
const tmp = `${this.path}.tmp`;
|
|
5705
|
-
|
|
5948
|
+
writeFileSync12(tmp, JSON.stringify(this.current, null, 2));
|
|
5706
5949
|
renameSync5(tmp, this.path);
|
|
5707
5950
|
} catch (err) {
|
|
5708
5951
|
console.error("[Suveren MCP] Could not save agent contact:", err);
|
|
@@ -5737,16 +5980,16 @@ function verifyGateContentHashes(content, auth) {
|
|
|
5737
5980
|
}
|
|
5738
5981
|
|
|
5739
5982
|
// src/lib/integration-registry.ts
|
|
5740
|
-
import { mkdirSync as
|
|
5741
|
-
import { homedir as
|
|
5742
|
-
import { dirname as
|
|
5743
|
-
var DEFAULT_DATA_DIR = process.env.SUVEREN_DATA_DIR ??
|
|
5983
|
+
import { mkdirSync as mkdirSync13, readFileSync as readFileSync17, writeFileSync as writeFileSync13, existsSync as existsSync16 } from "fs";
|
|
5984
|
+
import { homedir as homedir11 } from "os";
|
|
5985
|
+
import { dirname as dirname13, join as join16 } from "path";
|
|
5986
|
+
var DEFAULT_DATA_DIR = process.env.SUVEREN_DATA_DIR ?? join16(homedir11(), ".suveren");
|
|
5744
5987
|
var IntegrationRegistry = class {
|
|
5745
5988
|
integrations = /* @__PURE__ */ new Map();
|
|
5746
5989
|
filePath;
|
|
5747
5990
|
constructor(dataDir2) {
|
|
5748
5991
|
const dir = dataDir2 ?? DEFAULT_DATA_DIR;
|
|
5749
|
-
this.filePath =
|
|
5992
|
+
this.filePath = join16(dir, "integrations.json");
|
|
5750
5993
|
this.load();
|
|
5751
5994
|
}
|
|
5752
5995
|
// ─── CRUD ───────────────────────────────────────────────────────────────
|
|
@@ -5777,12 +6020,12 @@ var IntegrationRegistry = class {
|
|
|
5777
6020
|
}
|
|
5778
6021
|
// ─── Load / Save ────────────────────────────────────────────────────────
|
|
5779
6022
|
load() {
|
|
5780
|
-
if (!
|
|
5781
|
-
|
|
6023
|
+
if (!existsSync16(this.filePath)) {
|
|
6024
|
+
mkdirSync13(dirname13(this.filePath), { recursive: true });
|
|
5782
6025
|
return;
|
|
5783
6026
|
}
|
|
5784
6027
|
try {
|
|
5785
|
-
const raw =
|
|
6028
|
+
const raw = readFileSync17(this.filePath, "utf-8");
|
|
5786
6029
|
const data = JSON.parse(raw);
|
|
5787
6030
|
const migrated = data.integrations.map((i) => {
|
|
5788
6031
|
if ("toolGating" in i && !("profile" in i)) {
|
|
@@ -5811,15 +6054,15 @@ var IntegrationRegistry = class {
|
|
|
5811
6054
|
version: 1,
|
|
5812
6055
|
integrations: Array.from(this.integrations.values())
|
|
5813
6056
|
};
|
|
5814
|
-
|
|
5815
|
-
|
|
6057
|
+
mkdirSync13(dirname13(this.filePath), { recursive: true });
|
|
6058
|
+
writeFileSync13(this.filePath, JSON.stringify(data, null, 2), { encoding: "utf-8" });
|
|
5816
6059
|
}
|
|
5817
6060
|
};
|
|
5818
6061
|
|
|
5819
6062
|
// src/lib/integration-manager.ts
|
|
5820
|
-
import { homedir as
|
|
5821
|
-
import { join as
|
|
5822
|
-
import { existsSync as
|
|
6063
|
+
import { homedir as homedir12 } from "os";
|
|
6064
|
+
import { join as join17, delimiter } from "path";
|
|
6065
|
+
import { existsSync as existsSync17, writeFileSync as writeFileSync14, mkdirSync as mkdirSync14, readFileSync as readFileSync18, rmSync } from "fs";
|
|
5823
6066
|
import { execFile } from "child_process";
|
|
5824
6067
|
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
5825
6068
|
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
|
|
@@ -5949,8 +6192,8 @@ function inheritedConnectorEnv(parent, passEnv = []) {
|
|
|
5949
6192
|
}
|
|
5950
6193
|
|
|
5951
6194
|
// src/lib/integration-manager.ts
|
|
5952
|
-
var DEFAULT_DATA_DIR2 = process.env.SUVEREN_DATA_DIR ??
|
|
5953
|
-
var INTEGRATIONS_DIR = process.env.SUVEREN_INTEGRATIONS_DIR ??
|
|
6195
|
+
var DEFAULT_DATA_DIR2 = process.env.SUVEREN_DATA_DIR ?? join17(homedir12(), ".suveren");
|
|
6196
|
+
var INTEGRATIONS_DIR = process.env.SUVEREN_INTEGRATIONS_DIR ?? join17(DEFAULT_DATA_DIR2, "integrations");
|
|
5954
6197
|
var OFFLINE_MODE = process.env.SUVEREN_OFFLINE === "1";
|
|
5955
6198
|
var installLock = Promise.resolve();
|
|
5956
6199
|
function withInstallLock(task) {
|
|
@@ -5958,7 +6201,7 @@ function withInstallLock(task) {
|
|
|
5958
6201
|
installLock = run.then(() => void 0, () => void 0);
|
|
5959
6202
|
return run;
|
|
5960
6203
|
}
|
|
5961
|
-
var INTEGRATIONS_BIN =
|
|
6204
|
+
var INTEGRATIONS_BIN = join17(INTEGRATIONS_DIR, "node_modules", ".bin");
|
|
5962
6205
|
function getIntegrationsBinDir() {
|
|
5963
6206
|
return INTEGRATIONS_BIN;
|
|
5964
6207
|
}
|
|
@@ -5967,12 +6210,12 @@ function buildPath() {
|
|
|
5967
6210
|
return [INTEGRATIONS_BIN, base].join(delimiter);
|
|
5968
6211
|
}
|
|
5969
6212
|
function ensureIntegrationsDir() {
|
|
5970
|
-
if (!
|
|
5971
|
-
|
|
6213
|
+
if (!existsSync17(INTEGRATIONS_DIR)) {
|
|
6214
|
+
mkdirSync14(INTEGRATIONS_DIR, { recursive: true });
|
|
5972
6215
|
}
|
|
5973
|
-
const pkgPath =
|
|
5974
|
-
if (!
|
|
5975
|
-
|
|
6216
|
+
const pkgPath = join17(INTEGRATIONS_DIR, "package.json");
|
|
6217
|
+
if (!existsSync17(pkgPath)) {
|
|
6218
|
+
writeFileSync14(pkgPath, JSON.stringify({ name: "suveren-integrations", version: "1.0.0", private: true }, null, 2));
|
|
5976
6219
|
}
|
|
5977
6220
|
}
|
|
5978
6221
|
function readAgeOf(config) {
|
|
@@ -6061,19 +6304,19 @@ var IntegrationManager = class {
|
|
|
6061
6304
|
* exist, and treat anything else as not-installed (and reinstallable).
|
|
6062
6305
|
*/
|
|
6063
6306
|
isUsableInstall(npmPackage) {
|
|
6064
|
-
const pkgDir =
|
|
6065
|
-
const pkgJson =
|
|
6066
|
-
if (!
|
|
6307
|
+
const pkgDir = join17(INTEGRATIONS_DIR, "node_modules", ...npmPackage.split("/"));
|
|
6308
|
+
const pkgJson = join17(pkgDir, "package.json");
|
|
6309
|
+
if (!existsSync17(pkgDir) || !existsSync17(pkgJson)) return false;
|
|
6067
6310
|
try {
|
|
6068
|
-
const pkg = JSON.parse(
|
|
6311
|
+
const pkg = JSON.parse(readFileSync18(pkgJson, "utf8"));
|
|
6069
6312
|
const defaultBinName = (pkg.name ?? npmPackage).split("/").pop() ?? "";
|
|
6070
6313
|
const binMap = typeof pkg.bin === "string" ? { [defaultBinName]: pkg.bin } : pkg.bin ?? {};
|
|
6071
6314
|
const binEntries = Object.entries(binMap);
|
|
6072
6315
|
for (const [binName, target] of binEntries) {
|
|
6073
|
-
if (!
|
|
6316
|
+
if (!existsSync17(join17(pkgDir, target))) return false;
|
|
6074
6317
|
if (!this.binShimExists(binName)) return false;
|
|
6075
6318
|
}
|
|
6076
|
-
if (binEntries.length === 0 && pkg.main && !
|
|
6319
|
+
if (binEntries.length === 0 && pkg.main && !existsSync17(join17(pkgDir, pkg.main))) return false;
|
|
6077
6320
|
return true;
|
|
6078
6321
|
} catch {
|
|
6079
6322
|
return false;
|
|
@@ -6088,9 +6331,9 @@ var IntegrationManager = class {
|
|
|
6088
6331
|
*/
|
|
6089
6332
|
readInstalledVersion(npmPackage) {
|
|
6090
6333
|
if (!this.isUsableInstall(npmPackage)) return null;
|
|
6091
|
-
const pkgJson =
|
|
6334
|
+
const pkgJson = join17(INTEGRATIONS_DIR, "node_modules", ...npmPackage.split("/"), "package.json");
|
|
6092
6335
|
try {
|
|
6093
|
-
const pkg = JSON.parse(
|
|
6336
|
+
const pkg = JSON.parse(readFileSync18(pkgJson, "utf8"));
|
|
6094
6337
|
return typeof pkg.version === "string" ? pkg.version : null;
|
|
6095
6338
|
} catch {
|
|
6096
6339
|
return null;
|
|
@@ -6104,7 +6347,7 @@ var IntegrationManager = class {
|
|
|
6104
6347
|
binShimExists(binName) {
|
|
6105
6348
|
if (!binName) return false;
|
|
6106
6349
|
const candidates = process.platform === "win32" ? [binName, `${binName}.cmd`, `${binName}.CMD`, `${binName}.ps1`] : [binName];
|
|
6107
|
-
return candidates.some((c) =>
|
|
6350
|
+
return candidates.some((c) => existsSync17(join17(INTEGRATIONS_BIN, c)));
|
|
6108
6351
|
}
|
|
6109
6352
|
/**
|
|
6110
6353
|
* The exact version THIS gateway's CURRENTLY LOADED manifest pins
|
|
@@ -6194,8 +6437,8 @@ var IntegrationManager = class {
|
|
|
6194
6437
|
* nothing at all.
|
|
6195
6438
|
*/
|
|
6196
6439
|
async installNow(npmPackage, exactVersion, cleanFirst = true) {
|
|
6197
|
-
const pkgDir =
|
|
6198
|
-
if (cleanFirst &&
|
|
6440
|
+
const pkgDir = join17(INTEGRATIONS_DIR, "node_modules", ...npmPackage.split("/"));
|
|
6441
|
+
if (cleanFirst && existsSync17(pkgDir)) {
|
|
6199
6442
|
console.error(`[IntegrationManager] Removing ${exactVersion ? "outdated" : "unusable"} install of ${npmPackage}`);
|
|
6200
6443
|
try {
|
|
6201
6444
|
rmSync(pkgDir, { recursive: true, force: true });
|
|
@@ -7135,28 +7378,28 @@ function agentAccess(token, opensSession) {
|
|
|
7135
7378
|
}
|
|
7136
7379
|
|
|
7137
7380
|
// src/lib/profile-loader.ts
|
|
7138
|
-
import { readFileSync as
|
|
7139
|
-
import { join as
|
|
7381
|
+
import { readFileSync as readFileSync19, existsSync as existsSync18 } from "fs";
|
|
7382
|
+
import { join as join18, resolve as resolve2 } from "path";
|
|
7140
7383
|
import { registerProfile } from "@hap/core";
|
|
7141
7384
|
function loadProfiles(profilesDir) {
|
|
7142
|
-
const dir = resolve2(profilesDir ?? process.env.SUVEREN_PROFILES_DIR ??
|
|
7143
|
-
const indexPath =
|
|
7144
|
-
if (!
|
|
7385
|
+
const dir = resolve2(profilesDir ?? process.env.SUVEREN_PROFILES_DIR ?? join18(import.meta.dirname ?? __dirname, "..", "..", "..", "..", "..", "hap-profiles"));
|
|
7386
|
+
const indexPath = join18(dir, "index.json");
|
|
7387
|
+
if (!existsSync18(indexPath)) {
|
|
7145
7388
|
warnNoProfiles(`No index.json at ${indexPath}`);
|
|
7146
7389
|
return 0;
|
|
7147
7390
|
}
|
|
7148
7391
|
let index;
|
|
7149
7392
|
try {
|
|
7150
|
-
index = JSON.parse(
|
|
7393
|
+
index = JSON.parse(readFileSync19(indexPath, "utf-8"));
|
|
7151
7394
|
} catch (err) {
|
|
7152
7395
|
console.error(`[ProfileLoader] Failed to parse ${indexPath}:`, err);
|
|
7153
7396
|
return 0;
|
|
7154
7397
|
}
|
|
7155
7398
|
let loaded = 0;
|
|
7156
7399
|
for (const [profileId, relativePath] of Object.entries(index.profiles)) {
|
|
7157
|
-
const profilePath =
|
|
7400
|
+
const profilePath = join18(dir, relativePath);
|
|
7158
7401
|
try {
|
|
7159
|
-
const profile = JSON.parse(
|
|
7402
|
+
const profile = JSON.parse(readFileSync19(profilePath, "utf-8"));
|
|
7160
7403
|
registerProfile(profileId, profile);
|
|
7161
7404
|
const shortName2 = profileId.split("/").pop()?.replace(/@.*$/, "");
|
|
7162
7405
|
if (shortName2 && shortName2 !== profileId) {
|
|
@@ -7497,20 +7740,20 @@ async function controlPlaneMandate(dryRun, request) {
|
|
|
7497
7740
|
}
|
|
7498
7741
|
|
|
7499
7742
|
// src/lib/guides.ts
|
|
7500
|
-
import { existsSync as
|
|
7501
|
-
import { dirname as
|
|
7743
|
+
import { existsSync as existsSync19, readdirSync, readFileSync as readFileSync20 } from "fs";
|
|
7744
|
+
import { dirname as dirname14, join as join19 } from "path";
|
|
7502
7745
|
var FILE_RE = /^(?:(\d+)-)?([a-z0-9][a-z0-9-]*)\.md$/;
|
|
7503
7746
|
function builtinGuidesDir() {
|
|
7504
7747
|
if (process.env.SUVEREN_BUILTIN_GUIDES_DIR) return process.env.SUVEREN_BUILTIN_GUIDES_DIR;
|
|
7505
7748
|
let dir = import.meta.dirname ?? __dirname;
|
|
7506
7749
|
for (let i = 0; i < 8; i++) {
|
|
7507
|
-
const candidate =
|
|
7508
|
-
if (
|
|
7509
|
-
const parent =
|
|
7750
|
+
const candidate = join19(dir, "content", "guides");
|
|
7751
|
+
if (existsSync19(candidate)) return candidate;
|
|
7752
|
+
const parent = dirname14(dir);
|
|
7510
7753
|
if (parent === dir) break;
|
|
7511
7754
|
dir = parent;
|
|
7512
7755
|
}
|
|
7513
|
-
return
|
|
7756
|
+
return join19(import.meta.dirname ?? __dirname, "content", "guides");
|
|
7514
7757
|
}
|
|
7515
7758
|
function parse(body) {
|
|
7516
7759
|
const lines = body.split(/\r?\n/);
|
|
@@ -7521,14 +7764,14 @@ function parse(body) {
|
|
|
7521
7764
|
return { title, summary };
|
|
7522
7765
|
}
|
|
7523
7766
|
function readDir(dir, source) {
|
|
7524
|
-
if (!dir || !
|
|
7767
|
+
if (!dir || !existsSync19(dir)) return [];
|
|
7525
7768
|
const out = [];
|
|
7526
7769
|
for (const file of readdirSync(dir)) {
|
|
7527
7770
|
const m = FILE_RE.exec(file);
|
|
7528
7771
|
if (!m) continue;
|
|
7529
7772
|
let body;
|
|
7530
7773
|
try {
|
|
7531
|
-
body =
|
|
7774
|
+
body = readFileSync20(join19(dir, file), "utf8");
|
|
7532
7775
|
} catch {
|
|
7533
7776
|
continue;
|
|
7534
7777
|
}
|
|
@@ -8079,8 +8322,8 @@ function registerBuiltins(deps, factories = BUILTIN_FACTORIES) {
|
|
|
8079
8322
|
import { decodeMandateBlob as decodeMandateBlob3 } from "@hap/core";
|
|
8080
8323
|
|
|
8081
8324
|
// src/lib/committed-executor.ts
|
|
8082
|
-
import { existsSync as
|
|
8083
|
-
import { dirname as
|
|
8325
|
+
import { existsSync as existsSync20, mkdirSync as mkdirSync15, readFileSync as readFileSync21, unlinkSync as unlinkSync7, writeFileSync as writeFileSync15 } from "fs";
|
|
8326
|
+
import { dirname as dirname15, join as join20 } from "path";
|
|
8084
8327
|
var CommittedExecutor = class {
|
|
8085
8328
|
constructor(run) {
|
|
8086
8329
|
this.run = run;
|
|
@@ -8108,7 +8351,7 @@ var ExecutorLock = class {
|
|
|
8108
8351
|
path;
|
|
8109
8352
|
held = false;
|
|
8110
8353
|
constructor(dataDir2) {
|
|
8111
|
-
this.path =
|
|
8354
|
+
this.path = join20(dataDir2, "executor.lock");
|
|
8112
8355
|
}
|
|
8113
8356
|
/** Try to take the lock. Returns the holder's pid when someone else has it. */
|
|
8114
8357
|
acquire() {
|
|
@@ -8116,9 +8359,9 @@ var ExecutorLock = class {
|
|
|
8116
8359
|
if (current && current.pid !== process.pid && isAlive(current.pid)) {
|
|
8117
8360
|
return { held: false, holderPid: current.pid };
|
|
8118
8361
|
}
|
|
8119
|
-
|
|
8362
|
+
mkdirSync15(dirname15(this.path), { recursive: true, mode: 448 });
|
|
8120
8363
|
const lock = { pid: process.pid, startedAt: Math.floor(Date.now() / 1e3) };
|
|
8121
|
-
|
|
8364
|
+
writeFileSync15(this.path, JSON.stringify(lock), { mode: 384 });
|
|
8122
8365
|
this.held = true;
|
|
8123
8366
|
return { held: true };
|
|
8124
8367
|
}
|
|
@@ -8134,15 +8377,15 @@ var ExecutorLock = class {
|
|
|
8134
8377
|
const current = this.read();
|
|
8135
8378
|
if (current?.pid === process.pid) {
|
|
8136
8379
|
try {
|
|
8137
|
-
|
|
8380
|
+
unlinkSync7(this.path);
|
|
8138
8381
|
} catch {
|
|
8139
8382
|
}
|
|
8140
8383
|
}
|
|
8141
8384
|
}
|
|
8142
8385
|
read() {
|
|
8143
|
-
if (!
|
|
8386
|
+
if (!existsSync20(this.path)) return null;
|
|
8144
8387
|
try {
|
|
8145
|
-
const parsed = JSON.parse(
|
|
8388
|
+
const parsed = JSON.parse(readFileSync21(this.path, "utf8"));
|
|
8146
8389
|
return typeof parsed?.pid === "number" ? parsed : null;
|
|
8147
8390
|
} catch {
|
|
8148
8391
|
return null;
|
|
@@ -8159,20 +8402,20 @@ function isAlive(pid) {
|
|
|
8159
8402
|
}
|
|
8160
8403
|
|
|
8161
8404
|
// bin/http.ts
|
|
8162
|
-
import { homedir as
|
|
8163
|
-
import { join as
|
|
8405
|
+
import { homedir as homedir13 } from "os";
|
|
8406
|
+
import { join as join22 } from "path";
|
|
8164
8407
|
|
|
8165
8408
|
// src/lib/mcp-as-tracker.ts
|
|
8166
|
-
import { existsSync as
|
|
8167
|
-
import { dirname as
|
|
8409
|
+
import { existsSync as existsSync21, mkdirSync as mkdirSync16, readFileSync as readFileSync22, writeFileSync as writeFileSync16 } from "fs";
|
|
8410
|
+
import { dirname as dirname16, join as join21 } from "path";
|
|
8168
8411
|
function trackerPath(dataDir2) {
|
|
8169
|
-
return
|
|
8412
|
+
return join21(dataDir2, "mcp-as-pairing.json");
|
|
8170
8413
|
}
|
|
8171
8414
|
function readMcpPairedAsUrl(dataDir2) {
|
|
8172
8415
|
const path = trackerPath(dataDir2);
|
|
8173
|
-
if (!
|
|
8416
|
+
if (!existsSync21(path)) return null;
|
|
8174
8417
|
try {
|
|
8175
|
-
const data = JSON.parse(
|
|
8418
|
+
const data = JSON.parse(readFileSync22(path, "utf-8"));
|
|
8176
8419
|
return typeof data.asUrl === "string" ? data.asUrl : null;
|
|
8177
8420
|
} catch {
|
|
8178
8421
|
return null;
|
|
@@ -8180,12 +8423,12 @@ function readMcpPairedAsUrl(dataDir2) {
|
|
|
8180
8423
|
}
|
|
8181
8424
|
function writeMcpPairedAsUrl(dataDir2, asUrl) {
|
|
8182
8425
|
const path = trackerPath(dataDir2);
|
|
8183
|
-
|
|
8184
|
-
|
|
8426
|
+
mkdirSync16(dirname16(path), { recursive: true, mode: 448 });
|
|
8427
|
+
writeFileSync16(path, JSON.stringify({ asUrl }, null, 2), { encoding: "utf-8", mode: 384 });
|
|
8185
8428
|
}
|
|
8186
8429
|
|
|
8187
8430
|
// bin/http.ts
|
|
8188
|
-
var dataDir = process.env.SUVEREN_DATA_DIR ??
|
|
8431
|
+
var dataDir = process.env.SUVEREN_DATA_DIR ?? join22(homedir13(), ".suveren");
|
|
8189
8432
|
var spUrl = resolveAsUrl(dataDir);
|
|
8190
8433
|
validatePinTlsForUrl(resolvePinTls(dataDir), spUrl);
|
|
8191
8434
|
var port = parseInt(process.env.SUVEREN_MCP_PORT ?? "3430", 10);
|
|
@@ -8270,7 +8513,8 @@ app.post("/internal/configure", internalOnly, (req, res) => {
|
|
|
8270
8513
|
state.denialLog.setVaultKey(key);
|
|
8271
8514
|
state.receiptArchive.setVaultKey(key);
|
|
8272
8515
|
state.reportStore.setVaultKey(key);
|
|
8273
|
-
|
|
8516
|
+
state.outcomeOutbox.setVaultKey(key);
|
|
8517
|
+
console.error("[Suveren MCP] Vault key configured \u2014 gate store + denial log + receipt archive + report store + outcome outbox encryption active");
|
|
8274
8518
|
}
|
|
8275
8519
|
if (apiKey) {
|
|
8276
8520
|
state.spClient.setApiKey(apiKey);
|
|
@@ -8911,7 +9155,7 @@ app.listen(port, bindHost, () => {
|
|
|
8911
9155
|
(proposal) => executeCommitted(proposal, state, integrationManager)
|
|
8912
9156
|
);
|
|
8913
9157
|
installCommittedExecutor(committedExecutor);
|
|
8914
|
-
const dataDir2 = process.env.SUVEREN_DATA_DIR ??
|
|
9158
|
+
const dataDir2 = process.env.SUVEREN_DATA_DIR ?? join22(homedir13(), ".suveren");
|
|
8915
9159
|
const executorLock = new ExecutorLock(dataDir2);
|
|
8916
9160
|
const lock = executorLock.acquire();
|
|
8917
9161
|
if (!lock.held) {
|
|
@@ -8922,8 +9166,28 @@ app.listen(port, bindHost, () => {
|
|
|
8922
9166
|
for (const sig of ["SIGINT", "SIGTERM", "exit"]) {
|
|
8923
9167
|
process.once(sig, () => executorLock.release());
|
|
8924
9168
|
}
|
|
9169
|
+
async function pollRejectedOutcomes() {
|
|
9170
|
+
try {
|
|
9171
|
+
const rejected = await state.spClient.getRejectedProposals();
|
|
9172
|
+
for (const proposal of rejected) {
|
|
9173
|
+
const by = proposal.approverRejectedBy ?? proposal.rejectedBy;
|
|
9174
|
+
if (!by) continue;
|
|
9175
|
+
const submittedHere = state.proposalSubmissions.get(proposal.id) !== void 0;
|
|
9176
|
+
if (!outcomeEntryFor(proposal, submittedHere, by.userId, Date.now())) continue;
|
|
9177
|
+
const who = await resolveWhoLabel(
|
|
9178
|
+
state.receiptArchive,
|
|
9179
|
+
"domain" in by ? by.domain : by.userId,
|
|
9180
|
+
by.userId
|
|
9181
|
+
);
|
|
9182
|
+
const entry = outcomeEntryFor(proposal, submittedHere, who, Date.now());
|
|
9183
|
+
if (entry) state.outcomeOutbox.recordIfNew(entry);
|
|
9184
|
+
}
|
|
9185
|
+
} catch {
|
|
9186
|
+
}
|
|
9187
|
+
}
|
|
8925
9188
|
let draining = false;
|
|
8926
9189
|
async function executeCommittedProposals() {
|
|
9190
|
+
void pollRejectedOutcomes();
|
|
8927
9191
|
if (!executorLock.isHeld()) return;
|
|
8928
9192
|
if (draining) return;
|
|
8929
9193
|
draining = true;
|