@suveren/gateway 0.21.0 → 0.22.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/content/integrations/erp.json +14 -2
- package/dist/control-plane/index.mjs +179 -46
- package/dist/mcp-server/http.mjs +480 -174
- package/dist/ui/assets/{index-D5hgQ_8Z.css → index-BSAkn_Of.css} +1 -1
- package/dist/ui/assets/index-CBoGbefI.js +116 -0
- package/dist/ui/index.html +2 -2
- package/package.json +1 -1
- package/dist/ui/assets/index-BBjT8ue7.js +0 -116
package/dist/mcp-server/http.mjs
CHANGED
|
@@ -1931,6 +1931,7 @@ import { createHash as createHash2 } from "crypto";
|
|
|
1931
1931
|
var DEFAULT_DIR5 = process.env.SUVEREN_DATA_DIR ?? join8(homedir5(), ".suveren");
|
|
1932
1932
|
var FILE_NAME = "execution-journal.json";
|
|
1933
1933
|
var MAX_AGE_SECONDS2 = 90 * 24 * 60 * 60;
|
|
1934
|
+
var DETAIL_CAP = 500;
|
|
1934
1935
|
function hashToolArgs(args) {
|
|
1935
1936
|
return "sha256:" + createHash2("sha256").update(stableStringify(args)).digest("hex");
|
|
1936
1937
|
}
|
|
@@ -1969,13 +1970,20 @@ var ExecutionJournal = class {
|
|
|
1969
1970
|
this.persist(file);
|
|
1970
1971
|
return { ok: true };
|
|
1971
1972
|
}
|
|
1972
|
-
/**
|
|
1973
|
-
|
|
1973
|
+
/**
|
|
1974
|
+
* Close an `intent` row. Missing row is a programming error, not a silent
|
|
1975
|
+
* no-op. `detail` (capped to DETAIL_CAP) is stored only alongside outcome
|
|
1976
|
+
* 'refused' — see JournalEntry's doc comment.
|
|
1977
|
+
*/
|
|
1978
|
+
complete(ticketId, state2, outcome, detail) {
|
|
1974
1979
|
const file = this.load();
|
|
1975
1980
|
const row = file.entries.find((e) => e.ticketId === ticketId);
|
|
1976
1981
|
if (!row) throw new Error(`execution journal: no row for ticket ${ticketId} to complete`);
|
|
1977
1982
|
row.state = state2;
|
|
1978
1983
|
if (outcome) row.outcome = outcome;
|
|
1984
|
+
if (detail !== void 0) {
|
|
1985
|
+
row.detail = detail.length > DETAIL_CAP ? detail.slice(0, DETAIL_CAP) : detail;
|
|
1986
|
+
}
|
|
1979
1987
|
row.finishedAt = nowSec();
|
|
1980
1988
|
this.persist(file);
|
|
1981
1989
|
}
|
|
@@ -2045,7 +2053,8 @@ var ProposalSubmissionStore = class {
|
|
|
2045
2053
|
executionContextHash: hashToolArgs(entry.executionContext),
|
|
2046
2054
|
authorizationId: entry.authorizationId,
|
|
2047
2055
|
profileId: entry.profileId,
|
|
2048
|
-
submittedAt: nowSec2()
|
|
2056
|
+
submittedAt: nowSec2(),
|
|
2057
|
+
...entry.previewHash ? { previewHash: entry.previewHash } : {}
|
|
2049
2058
|
});
|
|
2050
2059
|
this.persist(file);
|
|
2051
2060
|
}
|
|
@@ -2073,6 +2082,56 @@ var ProposalSubmissionStore = class {
|
|
|
2073
2082
|
}
|
|
2074
2083
|
};
|
|
2075
2084
|
|
|
2085
|
+
// src/lib/preview-log.ts
|
|
2086
|
+
import { mkdirSync as mkdirSync8, readFileSync as readFileSync11, writeFileSync as writeFileSync8, existsSync as existsSync10, renameSync as renameSync3 } from "fs";
|
|
2087
|
+
import { homedir as homedir7 } from "os";
|
|
2088
|
+
import { dirname as dirname8, join as join10 } from "path";
|
|
2089
|
+
var DEFAULT_DIR7 = process.env.SUVEREN_DATA_DIR ?? join10(homedir7(), ".suveren");
|
|
2090
|
+
var FILE_NAME3 = "preview-log.json";
|
|
2091
|
+
var MAX_RECORDS2 = 500;
|
|
2092
|
+
var MAX_AGE_MS2 = 30 * 24 * 60 * 60 * 1e3;
|
|
2093
|
+
var PreviewLog = class {
|
|
2094
|
+
filePath;
|
|
2095
|
+
constructor(filePath) {
|
|
2096
|
+
const baseDir = filePath && filePath.endsWith(".json") ? dirname8(filePath) : filePath ?? DEFAULT_DIR7;
|
|
2097
|
+
this.filePath = join10(baseDir, FILE_NAME3);
|
|
2098
|
+
}
|
|
2099
|
+
/** Record one preview read. Never throws — recording must not break the read. */
|
|
2100
|
+
record(entry) {
|
|
2101
|
+
try {
|
|
2102
|
+
const file = this.load();
|
|
2103
|
+
file.entries.push(entry);
|
|
2104
|
+
const cutoff = entry.ts - MAX_AGE_MS2;
|
|
2105
|
+
file.entries = file.entries.filter((e) => e.ts >= cutoff);
|
|
2106
|
+
if (file.entries.length > MAX_RECORDS2) {
|
|
2107
|
+
file.entries = file.entries.slice(file.entries.length - MAX_RECORDS2);
|
|
2108
|
+
}
|
|
2109
|
+
this.persist(file);
|
|
2110
|
+
} catch (err) {
|
|
2111
|
+
console.error("[PreviewLog] failed to record preview read:", err);
|
|
2112
|
+
}
|
|
2113
|
+
}
|
|
2114
|
+
/** Diagnostic only. */
|
|
2115
|
+
getAll() {
|
|
2116
|
+
return [...this.load().entries];
|
|
2117
|
+
}
|
|
2118
|
+
load() {
|
|
2119
|
+
if (!existsSync10(this.filePath)) return { version: 1, entries: [] };
|
|
2120
|
+
try {
|
|
2121
|
+
const parsed = JSON.parse(readFileSync11(this.filePath, "utf-8"));
|
|
2122
|
+
return { version: 1, entries: parsed.entries ?? [] };
|
|
2123
|
+
} catch {
|
|
2124
|
+
return { version: 1, entries: [] };
|
|
2125
|
+
}
|
|
2126
|
+
}
|
|
2127
|
+
persist(file) {
|
|
2128
|
+
mkdirSync8(dirname8(this.filePath), { recursive: true, mode: 448 });
|
|
2129
|
+
const tmp = `${this.filePath}.tmp`;
|
|
2130
|
+
writeFileSync8(tmp, JSON.stringify(file, null, 2), { encoding: "utf-8", mode: 384 });
|
|
2131
|
+
renameSync3(tmp, this.filePath);
|
|
2132
|
+
}
|
|
2133
|
+
};
|
|
2134
|
+
|
|
2076
2135
|
// src/lib/gatekeeper.ts
|
|
2077
2136
|
import { verify } from "@hap/core";
|
|
2078
2137
|
var MCPGatekeeper = class {
|
|
@@ -2165,9 +2224,9 @@ var MCPGatekeeper = class {
|
|
|
2165
2224
|
};
|
|
2166
2225
|
|
|
2167
2226
|
// src/lib/report/report-store.ts
|
|
2168
|
-
import { mkdirSync as
|
|
2169
|
-
import { homedir as
|
|
2170
|
-
import { dirname as
|
|
2227
|
+
import { mkdirSync as mkdirSync9, readFileSync as readFileSync12, writeFileSync as writeFileSync9, existsSync as existsSync11, unlinkSync as unlinkSync5 } from "fs";
|
|
2228
|
+
import { homedir as homedir8 } from "os";
|
|
2229
|
+
import { dirname as dirname9, join as join11 } from "path";
|
|
2171
2230
|
import { createCipheriv as createCipheriv5, createDecipheriv as createDecipheriv5, randomBytes as randomBytes5 } from "crypto";
|
|
2172
2231
|
|
|
2173
2232
|
// src/lib/report/record-resolvers.ts
|
|
@@ -2476,27 +2535,27 @@ async function buildCoverage(caseElements, getExport, archive, referenced, windo
|
|
|
2476
2535
|
}
|
|
2477
2536
|
|
|
2478
2537
|
// src/lib/report/report-store.ts
|
|
2479
|
-
var
|
|
2538
|
+
var DEFAULT_DIR8 = process.env.SUVEREN_DATA_DIR ?? join11(homedir8(), ".suveren");
|
|
2480
2539
|
var ReportStore = class {
|
|
2481
2540
|
report = null;
|
|
2482
2541
|
baseDir;
|
|
2483
2542
|
vaultKey = null;
|
|
2484
2543
|
constructor(filePath) {
|
|
2485
2544
|
if (filePath && filePath.endsWith(".json")) {
|
|
2486
|
-
this.baseDir =
|
|
2545
|
+
this.baseDir = dirname9(filePath);
|
|
2487
2546
|
} else {
|
|
2488
|
-
this.baseDir = filePath ??
|
|
2547
|
+
this.baseDir = filePath ?? DEFAULT_DIR8;
|
|
2489
2548
|
}
|
|
2490
2549
|
this.loadPlaintext();
|
|
2491
2550
|
}
|
|
2492
2551
|
/** Set the vault key for encryption. Triggers migration from plaintext if needed. */
|
|
2493
2552
|
setVaultKey(key) {
|
|
2494
2553
|
this.vaultKey = key;
|
|
2495
|
-
if (
|
|
2554
|
+
if (existsSync11(this.encryptedFilePath)) {
|
|
2496
2555
|
this.loadEncrypted();
|
|
2497
2556
|
} else if (this.report !== null) {
|
|
2498
2557
|
this.persistEncrypted();
|
|
2499
|
-
if (
|
|
2558
|
+
if (existsSync11(this.plaintextFilePath)) {
|
|
2500
2559
|
try {
|
|
2501
2560
|
unlinkSync5(this.plaintextFilePath);
|
|
2502
2561
|
} catch {
|
|
@@ -2511,7 +2570,7 @@ var ReportStore = class {
|
|
|
2511
2570
|
* "the report exists but is locked".
|
|
2512
2571
|
*/
|
|
2513
2572
|
isLocked() {
|
|
2514
|
-
return this.vaultKey === null &&
|
|
2573
|
+
return this.vaultKey === null && existsSync11(this.encryptedFilePath);
|
|
2515
2574
|
}
|
|
2516
2575
|
/** The stored, already-verified report — or null if none has been saved
|
|
2517
2576
|
* (and the store is not locked; check `isLocked()` first). */
|
|
@@ -2560,34 +2619,34 @@ var ReportStore = class {
|
|
|
2560
2619
|
}
|
|
2561
2620
|
// ─── File paths ─────────────────────────────────────────────────────────
|
|
2562
2621
|
get plaintextFilePath() {
|
|
2563
|
-
return
|
|
2622
|
+
return join11(this.baseDir, "report.json");
|
|
2564
2623
|
}
|
|
2565
2624
|
get encryptedFilePath() {
|
|
2566
|
-
return
|
|
2625
|
+
return join11(this.baseDir, "report.enc.json");
|
|
2567
2626
|
}
|
|
2568
2627
|
// ─── Load / Persist ─────────────────────────────────────────────────────
|
|
2569
2628
|
loadPlaintext() {
|
|
2570
|
-
if (!
|
|
2571
|
-
|
|
2629
|
+
if (!existsSync11(this.plaintextFilePath)) {
|
|
2630
|
+
mkdirSync9(this.baseDir, { recursive: true });
|
|
2572
2631
|
return;
|
|
2573
2632
|
}
|
|
2574
2633
|
try {
|
|
2575
|
-
const raw =
|
|
2634
|
+
const raw = readFileSync12(this.plaintextFilePath, "utf-8");
|
|
2576
2635
|
const parsed = JSON.parse(raw);
|
|
2577
2636
|
this.report = parsed.report ?? null;
|
|
2578
2637
|
} catch {
|
|
2579
2638
|
console.error(`[ReportStore] Could not parse ${this.plaintextFilePath} \u2014 preserving as .corrupt`);
|
|
2580
2639
|
try {
|
|
2581
|
-
|
|
2640
|
+
writeFileSync9(`${this.plaintextFilePath}.corrupt-${Date.now()}`, readFileSync12(this.plaintextFilePath));
|
|
2582
2641
|
} catch {
|
|
2583
2642
|
}
|
|
2584
2643
|
this.report = null;
|
|
2585
2644
|
}
|
|
2586
2645
|
}
|
|
2587
2646
|
loadEncrypted() {
|
|
2588
|
-
if (!
|
|
2647
|
+
if (!existsSync11(this.encryptedFilePath)) return;
|
|
2589
2648
|
try {
|
|
2590
|
-
const raw =
|
|
2649
|
+
const raw = readFileSync12(this.encryptedFilePath, "utf-8");
|
|
2591
2650
|
const data = JSON.parse(raw);
|
|
2592
2651
|
const decrypted = this.decrypt(data.blob);
|
|
2593
2652
|
const parsed = JSON.parse(decrypted);
|
|
@@ -2595,7 +2654,7 @@ var ReportStore = class {
|
|
|
2595
2654
|
} catch (err) {
|
|
2596
2655
|
console.error(`[ReportStore] Could not decrypt ${this.encryptedFilePath} \u2014 preserving as .corrupt:`, err);
|
|
2597
2656
|
try {
|
|
2598
|
-
|
|
2657
|
+
writeFileSync9(`${this.encryptedFilePath}.corrupt-${Date.now()}`, readFileSync12(this.encryptedFilePath));
|
|
2599
2658
|
} catch {
|
|
2600
2659
|
}
|
|
2601
2660
|
this.report = null;
|
|
@@ -2610,16 +2669,16 @@ var ReportStore = class {
|
|
|
2610
2669
|
}
|
|
2611
2670
|
persistPlaintext() {
|
|
2612
2671
|
const data = { version: 1, report: this.report };
|
|
2613
|
-
|
|
2614
|
-
|
|
2672
|
+
mkdirSync9(this.baseDir, { recursive: true, mode: 448 });
|
|
2673
|
+
writeFileSync9(this.plaintextFilePath, JSON.stringify(data, null, 2), { encoding: "utf-8", mode: 384 });
|
|
2615
2674
|
}
|
|
2616
2675
|
persistEncrypted() {
|
|
2617
2676
|
const data = {
|
|
2618
2677
|
version: 1,
|
|
2619
2678
|
blob: this.encrypt(JSON.stringify({ version: 1, report: this.report }))
|
|
2620
2679
|
};
|
|
2621
|
-
|
|
2622
|
-
|
|
2680
|
+
mkdirSync9(this.baseDir, { recursive: true, mode: 448 });
|
|
2681
|
+
writeFileSync9(this.encryptedFilePath, JSON.stringify(data, null, 2), { encoding: "utf-8", mode: 384 });
|
|
2623
2682
|
}
|
|
2624
2683
|
};
|
|
2625
2684
|
|
|
@@ -2640,6 +2699,8 @@ var SharedState = class {
|
|
|
2640
2699
|
/** What THIS gateway submitted for each review-mode proposal it created —
|
|
2641
2700
|
* see ticket-verify.ts / commitments.ts. */
|
|
2642
2701
|
proposalSubmissions;
|
|
2702
|
+
/** AU3 — local, content-free record of preview reads. See preview-log.ts. */
|
|
2703
|
+
previewLog;
|
|
2643
2704
|
gatekeeper;
|
|
2644
2705
|
/**
|
|
2645
2706
|
* V7 — set by {@link checkAsCompat} (called once at process start, and
|
|
@@ -2673,6 +2734,7 @@ var SharedState = class {
|
|
|
2673
2734
|
this.reportStore = new ReportStore(gateStorePath);
|
|
2674
2735
|
this.executionJournal = new ExecutionJournal(gateStorePath);
|
|
2675
2736
|
this.proposalSubmissions = new ProposalSubmissionStore(gateStorePath);
|
|
2737
|
+
this.previewLog = new PreviewLog(gateStorePath);
|
|
2676
2738
|
this.gatekeeper = new MCPGatekeeper(this.cache);
|
|
2677
2739
|
}
|
|
2678
2740
|
/**
|
|
@@ -2861,29 +2923,29 @@ function formatConsumptionFull(entries) {
|
|
|
2861
2923
|
}
|
|
2862
2924
|
|
|
2863
2925
|
// src/lib/context-loader.ts
|
|
2864
|
-
import { readFileSync as
|
|
2865
|
-
import { homedir as
|
|
2866
|
-
import { join as
|
|
2867
|
-
var
|
|
2926
|
+
import { readFileSync as readFileSync13, existsSync as existsSync12, mkdirSync as mkdirSync10, writeFileSync as writeFileSync10, renameSync as renameSync4 } from "fs";
|
|
2927
|
+
import { homedir as homedir9 } from "os";
|
|
2928
|
+
import { join as join12, dirname as dirname10 } from "path";
|
|
2929
|
+
var DEFAULT_DIR9 = process.env.SUVEREN_DATA_DIR ?? join12(homedir9(), ".suveren");
|
|
2868
2930
|
var CONTEXT_MAX_BYTES = 16 * 1024;
|
|
2869
2931
|
function contextFilePath(dataDir2) {
|
|
2870
|
-
return
|
|
2932
|
+
return join12(dataDir2 ?? DEFAULT_DIR9, "context.md");
|
|
2871
2933
|
}
|
|
2872
2934
|
function writeContextFile(content, dataDir2) {
|
|
2873
2935
|
const bytes = Buffer.byteLength(content, "utf-8");
|
|
2874
2936
|
if (bytes > CONTEXT_MAX_BYTES) throw new Error(`the brief is ${bytes} bytes; the limit is ${CONTEXT_MAX_BYTES}.`);
|
|
2875
2937
|
const filePath = contextFilePath(dataDir2);
|
|
2876
|
-
|
|
2938
|
+
mkdirSync10(dirname10(filePath), { recursive: true });
|
|
2877
2939
|
const tmpPath = `${filePath}.tmp`;
|
|
2878
|
-
|
|
2879
|
-
|
|
2940
|
+
writeFileSync10(tmpPath, content, "utf-8");
|
|
2941
|
+
renameSync4(tmpPath, filePath);
|
|
2880
2942
|
}
|
|
2881
2943
|
var BRIEF_MAX_CHARS = 16 * 1024;
|
|
2882
2944
|
function readContextFile(dataDir2) {
|
|
2883
2945
|
const filePath = contextFilePath(dataDir2);
|
|
2884
|
-
if (!
|
|
2946
|
+
if (!existsSync12(filePath)) return null;
|
|
2885
2947
|
try {
|
|
2886
|
-
const content =
|
|
2948
|
+
const content = readFileSync13(filePath, "utf-8").trim();
|
|
2887
2949
|
return content || null;
|
|
2888
2950
|
} catch {
|
|
2889
2951
|
return null;
|
|
@@ -3069,6 +3131,252 @@ function attachTicketId(tool, args, ticketId) {
|
|
|
3069
3131
|
return { ...args, ticket_id: ticketId };
|
|
3070
3132
|
}
|
|
3071
3133
|
|
|
3134
|
+
// src/lib/preview.ts
|
|
3135
|
+
import { computeContentHash as computeContentHash2 } from "@hap/core";
|
|
3136
|
+
|
|
3137
|
+
// src/lib/manifest-loader.ts
|
|
3138
|
+
import { readFileSync as readFileSync14, existsSync as existsSync13 } from "fs";
|
|
3139
|
+
import { join as join13, resolve } from "path";
|
|
3140
|
+
var EXACT_SEMVER_RE = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/;
|
|
3141
|
+
function isExactSemver(value) {
|
|
3142
|
+
return typeof value === "string" && EXACT_SEMVER_RE.test(value);
|
|
3143
|
+
}
|
|
3144
|
+
function invalidNpmPinReason(manifest) {
|
|
3145
|
+
if (!manifest.npmPackage) return null;
|
|
3146
|
+
if (manifest.npmVersion === void 0) {
|
|
3147
|
+
return `npmPackage "${manifest.npmPackage}" is set but npmVersion is missing \u2014 every connector manifest must pin an exact tested version`;
|
|
3148
|
+
}
|
|
3149
|
+
if (!isExactSemver(manifest.npmVersion)) {
|
|
3150
|
+
return `npmVersion ${JSON.stringify(manifest.npmVersion)} for "${manifest.npmPackage}" is not an exact semver (e.g. "1.2.3") \u2014 ranges, dist-tags like "latest", git specs, URLs, and file paths are not allowed`;
|
|
3151
|
+
}
|
|
3152
|
+
return null;
|
|
3153
|
+
}
|
|
3154
|
+
function invalidPreviewReason(toolName, entry) {
|
|
3155
|
+
if (entry === null || typeof entry !== "object") return null;
|
|
3156
|
+
const preview = entry.preview;
|
|
3157
|
+
if (preview === void 0) return null;
|
|
3158
|
+
if (preview === null || typeof preview !== "object" || Array.isArray(preview)) {
|
|
3159
|
+
return `override "${toolName}".preview must be an object`;
|
|
3160
|
+
}
|
|
3161
|
+
const p = preview;
|
|
3162
|
+
if (typeof p.tool !== "string" || p.tool.length === 0) {
|
|
3163
|
+
return `override "${toolName}".preview.tool must be a non-empty string`;
|
|
3164
|
+
}
|
|
3165
|
+
if (p.args === void 0 || p.args === null || typeof p.args !== "object" || Array.isArray(p.args)) {
|
|
3166
|
+
return `override "${toolName}".preview.args must be an object mapping preview-tool arg names to action arg names`;
|
|
3167
|
+
}
|
|
3168
|
+
for (const [argName, source] of Object.entries(p.args)) {
|
|
3169
|
+
if (typeof source !== "string" || source.length === 0) {
|
|
3170
|
+
return `override "${toolName}".preview.args.${argName} must name a non-empty source argument (string)`;
|
|
3171
|
+
}
|
|
3172
|
+
}
|
|
3173
|
+
if (p.version !== void 0) {
|
|
3174
|
+
if (p.version === null || typeof p.version !== "object" || Array.isArray(p.version)) {
|
|
3175
|
+
return `override "${toolName}".preview.version must be an object`;
|
|
3176
|
+
}
|
|
3177
|
+
const v = p.version;
|
|
3178
|
+
if (typeof v.arg !== "string" || v.arg.length === 0) {
|
|
3179
|
+
return `override "${toolName}".preview.version.arg must be a non-empty string`;
|
|
3180
|
+
}
|
|
3181
|
+
if (typeof v.field !== "string" || v.field.length === 0) {
|
|
3182
|
+
return `override "${toolName}".preview.version.field must be a non-empty string`;
|
|
3183
|
+
}
|
|
3184
|
+
}
|
|
3185
|
+
if (p.fields !== void 0) {
|
|
3186
|
+
if (!Array.isArray(p.fields)) {
|
|
3187
|
+
return `override "${toolName}".preview.fields must be an array of field names`;
|
|
3188
|
+
}
|
|
3189
|
+
for (const [i, f] of p.fields.entries()) {
|
|
3190
|
+
if (typeof f !== "string" || f.length === 0) {
|
|
3191
|
+
return `override "${toolName}".preview.fields[${i}] must be a non-empty string`;
|
|
3192
|
+
}
|
|
3193
|
+
}
|
|
3194
|
+
}
|
|
3195
|
+
return null;
|
|
3196
|
+
}
|
|
3197
|
+
function invalidManifestPreviewReason(manifest) {
|
|
3198
|
+
const overrides = manifest.toolGating?.overrides ?? {};
|
|
3199
|
+
for (const [toolName, entry] of Object.entries(overrides)) {
|
|
3200
|
+
const reason = invalidPreviewReason(toolName, entry);
|
|
3201
|
+
if (reason) return reason;
|
|
3202
|
+
}
|
|
3203
|
+
return null;
|
|
3204
|
+
}
|
|
3205
|
+
var manifests = /* @__PURE__ */ new Map();
|
|
3206
|
+
function loadManifests(integrationsDir) {
|
|
3207
|
+
const dir = resolve(
|
|
3208
|
+
integrationsDir ?? process.env.SUVEREN_MANIFESTS_DIR ?? join13(import.meta.dirname ?? __dirname, "..", "..", "..", "..", "content", "integrations")
|
|
3209
|
+
);
|
|
3210
|
+
const indexPath = join13(dir, "index.json");
|
|
3211
|
+
if (!existsSync13(indexPath)) {
|
|
3212
|
+
console.error(`[ManifestLoader] No index.json found at ${indexPath}, skipping manifest loading`);
|
|
3213
|
+
return 0;
|
|
3214
|
+
}
|
|
3215
|
+
let index;
|
|
3216
|
+
try {
|
|
3217
|
+
index = JSON.parse(readFileSync14(indexPath, "utf-8"));
|
|
3218
|
+
} catch (err) {
|
|
3219
|
+
console.error(`[ManifestLoader] Failed to parse ${indexPath}:`, err);
|
|
3220
|
+
return 0;
|
|
3221
|
+
}
|
|
3222
|
+
let loaded = 0;
|
|
3223
|
+
for (const [id, relativePath] of Object.entries(index.integrations)) {
|
|
3224
|
+
const manifestPath = join13(dir, relativePath);
|
|
3225
|
+
try {
|
|
3226
|
+
const manifest = JSON.parse(readFileSync14(manifestPath, "utf-8"));
|
|
3227
|
+
const pinReason = invalidNpmPinReason(manifest);
|
|
3228
|
+
if (pinReason) {
|
|
3229
|
+
console.error(`[ManifestLoader] Refusing manifest ${id} (${manifestPath}): ${pinReason}`);
|
|
3230
|
+
continue;
|
|
3231
|
+
}
|
|
3232
|
+
const previewReason = invalidManifestPreviewReason(manifest);
|
|
3233
|
+
if (previewReason) {
|
|
3234
|
+
console.error(`[ManifestLoader] Refusing manifest ${id} (${manifestPath}): ${previewReason}`);
|
|
3235
|
+
continue;
|
|
3236
|
+
}
|
|
3237
|
+
manifests.set(id, manifest);
|
|
3238
|
+
loaded++;
|
|
3239
|
+
} catch (err) {
|
|
3240
|
+
console.error(`[ManifestLoader] Failed to load manifest ${id} from ${manifestPath}:`, err);
|
|
3241
|
+
}
|
|
3242
|
+
}
|
|
3243
|
+
console.error(`[ManifestLoader] Loaded ${loaded} integration manifest(s) from ${dir}`);
|
|
3244
|
+
return loaded;
|
|
3245
|
+
}
|
|
3246
|
+
function getManifest(id) {
|
|
3247
|
+
return manifests.get(id);
|
|
3248
|
+
}
|
|
3249
|
+
function getAllManifests() {
|
|
3250
|
+
return Array.from(manifests.values());
|
|
3251
|
+
}
|
|
3252
|
+
|
|
3253
|
+
// src/lib/preview.ts
|
|
3254
|
+
var PREVIEW_READ_TIMEOUT_MS = 5e3;
|
|
3255
|
+
var PREVIEW_MESSAGE_CAP = 500;
|
|
3256
|
+
function capText(text, max = PREVIEW_MESSAGE_CAP) {
|
|
3257
|
+
if (text === void 0) return void 0;
|
|
3258
|
+
return text.length > max ? text.slice(0, max) : text;
|
|
3259
|
+
}
|
|
3260
|
+
function getToolPreviewConfig(integrationId, toolName) {
|
|
3261
|
+
const manifest = getManifest(integrationId);
|
|
3262
|
+
const overrides = manifest?.toolGating?.overrides ?? {};
|
|
3263
|
+
const entry = overrides[toolName];
|
|
3264
|
+
return entry?.preview;
|
|
3265
|
+
}
|
|
3266
|
+
function mapPreviewArgs(preview, actionArgs) {
|
|
3267
|
+
const mapped = {};
|
|
3268
|
+
for (const [targetArg, sourceArg] of Object.entries(preview.args)) {
|
|
3269
|
+
if (sourceArg in actionArgs) mapped[targetArg] = actionArgs[sourceArg];
|
|
3270
|
+
}
|
|
3271
|
+
return mapped;
|
|
3272
|
+
}
|
|
3273
|
+
var PreviewTimeoutError = class extends Error {
|
|
3274
|
+
};
|
|
3275
|
+
function withTimeout(p, ms) {
|
|
3276
|
+
let timer;
|
|
3277
|
+
const timeout = new Promise((_, reject) => {
|
|
3278
|
+
timer = setTimeout(() => reject(new PreviewTimeoutError("preview read timed out")), ms);
|
|
3279
|
+
});
|
|
3280
|
+
return Promise.race([p, timeout]).finally(() => clearTimeout(timer));
|
|
3281
|
+
}
|
|
3282
|
+
async function readPreview(integrationManager2, integrationId, previewTool, args) {
|
|
3283
|
+
const discovered = integrationManager2.getAllTools().find((t) => t.integrationId === integrationId && t.originalName === previewTool);
|
|
3284
|
+
if (!discovered) {
|
|
3285
|
+
return { status: "unavailable", reason: "no_connector" };
|
|
3286
|
+
}
|
|
3287
|
+
try {
|
|
3288
|
+
const result = await withTimeout(
|
|
3289
|
+
integrationManager2.callTool(integrationId, previewTool, args),
|
|
3290
|
+
PREVIEW_READ_TIMEOUT_MS
|
|
3291
|
+
);
|
|
3292
|
+
const text = result.content?.[0]?.text;
|
|
3293
|
+
if (result.isError) {
|
|
3294
|
+
return { status: "not_found", message: capText(text) };
|
|
3295
|
+
}
|
|
3296
|
+
return {
|
|
3297
|
+
status: "ok",
|
|
3298
|
+
body: {
|
|
3299
|
+
...result.structuredContent !== void 0 ? { structured: result.structuredContent } : {},
|
|
3300
|
+
...discovered.outputSchema ? { outputSchema: discovered.outputSchema } : {},
|
|
3301
|
+
...text !== void 0 ? { text } : {}
|
|
3302
|
+
}
|
|
3303
|
+
};
|
|
3304
|
+
} catch (err) {
|
|
3305
|
+
if (err instanceof PreviewTimeoutError) {
|
|
3306
|
+
return { status: "unavailable", reason: "timeout" };
|
|
3307
|
+
}
|
|
3308
|
+
return {
|
|
3309
|
+
status: "unavailable",
|
|
3310
|
+
reason: "connector_error",
|
|
3311
|
+
message: capText(err instanceof Error ? err.message : String(err))
|
|
3312
|
+
};
|
|
3313
|
+
}
|
|
3314
|
+
}
|
|
3315
|
+
function hashPreviewBody(body) {
|
|
3316
|
+
if (body.structured !== void 0) {
|
|
3317
|
+
return computeContentHash2({ kind: "jcs", version: "1" }, body.structured);
|
|
3318
|
+
}
|
|
3319
|
+
return computeContentHash2({ kind: "text", version: "1" }, body.text ?? "");
|
|
3320
|
+
}
|
|
3321
|
+
async function computeSubmissionPreviewHash(integrationManager2, integrationId, toolName, actionArgs) {
|
|
3322
|
+
const preview = getToolPreviewConfig(integrationId, toolName);
|
|
3323
|
+
if (!preview || preview.version) return void 0;
|
|
3324
|
+
const mapped = mapPreviewArgs(preview, actionArgs);
|
|
3325
|
+
const read = await readPreview(integrationManager2, integrationId, preview.tool, mapped);
|
|
3326
|
+
if (read.status !== "ok") return void 0;
|
|
3327
|
+
return hashPreviewBody(read.body);
|
|
3328
|
+
}
|
|
3329
|
+
async function previewChangedSinceSubmission(integrationManager2, integrationId, toolName, actionArgs, previousHash) {
|
|
3330
|
+
const preview = getToolPreviewConfig(integrationId, toolName);
|
|
3331
|
+
if (!preview) return true;
|
|
3332
|
+
const mapped = mapPreviewArgs(preview, actionArgs);
|
|
3333
|
+
const read = await readPreview(integrationManager2, integrationId, preview.tool, mapped);
|
|
3334
|
+
if (read.status !== "ok") return true;
|
|
3335
|
+
return hashPreviewBody(read.body) !== previousHash;
|
|
3336
|
+
}
|
|
3337
|
+
async function buildInternalPreview(integrationManager2, integrationId, toolName, actionArgs) {
|
|
3338
|
+
const preview = getToolPreviewConfig(integrationId, toolName);
|
|
3339
|
+
if (!preview) return { status: "none" };
|
|
3340
|
+
const mappedArgs = mapPreviewArgs(preview, actionArgs);
|
|
3341
|
+
const read1 = await readPreview(integrationManager2, integrationId, preview.tool, mappedArgs);
|
|
3342
|
+
if (read1.status !== "ok") return read1;
|
|
3343
|
+
const ok = (extra = {}) => ({
|
|
3344
|
+
status: "ok",
|
|
3345
|
+
integration: integrationId,
|
|
3346
|
+
tool: preview.tool,
|
|
3347
|
+
body: read1.body,
|
|
3348
|
+
...preview.fields ? { fields: preview.fields } : {},
|
|
3349
|
+
...extra
|
|
3350
|
+
});
|
|
3351
|
+
if (!preview.version) {
|
|
3352
|
+
return ok();
|
|
3353
|
+
}
|
|
3354
|
+
const approved = actionArgs[preview.version.arg];
|
|
3355
|
+
if (typeof approved !== "string" && typeof approved !== "number") {
|
|
3356
|
+
return ok();
|
|
3357
|
+
}
|
|
3358
|
+
const { [preview.version.arg]: _omit, ...withoutVersion } = actionArgs;
|
|
3359
|
+
const mappedCurrentArgs = mapPreviewArgs(preview, withoutVersion);
|
|
3360
|
+
const read2 = await readPreview(integrationManager2, integrationId, preview.tool, mappedCurrentArgs);
|
|
3361
|
+
if (read2.status !== "ok" || read2.body.structured === void 0) {
|
|
3362
|
+
return ok();
|
|
3363
|
+
}
|
|
3364
|
+
const current = read2.body.structured[preview.version.field];
|
|
3365
|
+
if (typeof current !== "string" && typeof current !== "number") {
|
|
3366
|
+
return ok();
|
|
3367
|
+
}
|
|
3368
|
+
const stale = String(current) !== String(approved);
|
|
3369
|
+
return ok({
|
|
3370
|
+
version: {
|
|
3371
|
+
field: preview.version.field,
|
|
3372
|
+
approved,
|
|
3373
|
+
current,
|
|
3374
|
+
stale,
|
|
3375
|
+
...stale ? { currentBody: read2.body } : {}
|
|
3376
|
+
}
|
|
3377
|
+
});
|
|
3378
|
+
}
|
|
3379
|
+
|
|
3072
3380
|
// src/lib/ticket-verify.ts
|
|
3073
3381
|
import { verifyTicketSignature } from "@hap/core";
|
|
3074
3382
|
var TicketBindingMismatchError = class extends Error {
|
|
@@ -4042,13 +4350,20 @@ function createGatedToolHandlerInner(tool, integrationManager2, state2) {
|
|
|
4042
4350
|
toolArgs: enrichedArgs,
|
|
4043
4351
|
executionContext: wireExecCtx
|
|
4044
4352
|
});
|
|
4353
|
+
const previewHash = await computeSubmissionPreviewHash(
|
|
4354
|
+
integrationManager2,
|
|
4355
|
+
tool.integrationId,
|
|
4356
|
+
tool.originalName,
|
|
4357
|
+
enrichedArgs
|
|
4358
|
+
);
|
|
4045
4359
|
state2.proposalSubmissions.record({
|
|
4046
4360
|
proposalId: proposal.id,
|
|
4047
4361
|
tool: tool.namespacedName,
|
|
4048
4362
|
toolArgs: enrichedArgs,
|
|
4049
4363
|
executionContext: wireExecCtx,
|
|
4050
4364
|
authorizationId: authzId,
|
|
4051
|
-
profileId: auth.profileId
|
|
4365
|
+
profileId: auth.profileId,
|
|
4366
|
+
previewHash
|
|
4052
4367
|
});
|
|
4053
4368
|
return {
|
|
4054
4369
|
content: [{
|
|
@@ -4214,13 +4529,20 @@ Proposal ID: ${proposal.id}. Check status with check-pending-commitments(proposa
|
|
|
4214
4529
|
executionContext: wireExecCtx,
|
|
4215
4530
|
pendingApprovers: uniqueApprovers
|
|
4216
4531
|
});
|
|
4532
|
+
const previewHash = await computeSubmissionPreviewHash(
|
|
4533
|
+
integrationManager2,
|
|
4534
|
+
tool.integrationId,
|
|
4535
|
+
tool.originalName,
|
|
4536
|
+
enrichedArgs
|
|
4537
|
+
);
|
|
4217
4538
|
state2.proposalSubmissions.record({
|
|
4218
4539
|
proposalId: proposal.id,
|
|
4219
4540
|
tool: tool.namespacedName,
|
|
4220
4541
|
toolArgs: enrichedArgs,
|
|
4221
4542
|
executionContext: wireExecCtx,
|
|
4222
4543
|
authorizationId: authzId,
|
|
4223
|
-
profileId: auth.profileId
|
|
4544
|
+
profileId: auth.profileId,
|
|
4545
|
+
previewHash
|
|
4224
4546
|
});
|
|
4225
4547
|
return {
|
|
4226
4548
|
content: [{
|
|
@@ -4955,6 +5277,22 @@ async function executeCommitted(proposal, state2, integrationManager2) {
|
|
|
4955
5277
|
`[Suveren MCP] Proposal ${proposal.id}: ticket ${receiptId} was replayed by the AS with no local execution record \u2014 executing once (lost-response recovery).`
|
|
4956
5278
|
);
|
|
4957
5279
|
}
|
|
5280
|
+
if (submitted.previewHash) {
|
|
5281
|
+
const changed = await previewChangedSinceSubmission(
|
|
5282
|
+
integrationManager2,
|
|
5283
|
+
integrationId,
|
|
5284
|
+
toolName,
|
|
5285
|
+
proposal.toolArgs,
|
|
5286
|
+
submitted.previewHash
|
|
5287
|
+
);
|
|
5288
|
+
if (changed) {
|
|
5289
|
+
state2.executionJournal.complete(receiptId, "failed", "changed");
|
|
5290
|
+
return {
|
|
5291
|
+
text: `Proposal ${proposal.id} was approved, but the record changed in ${integrationId} since it was requested \u2014 nothing was done. A new attempt needs a new request and approval.`,
|
|
5292
|
+
isError: true
|
|
5293
|
+
};
|
|
5294
|
+
}
|
|
5295
|
+
}
|
|
4958
5296
|
try {
|
|
4959
5297
|
let outgoingArgs = proposal.toolArgs;
|
|
4960
5298
|
if (discovered && receiptId) {
|
|
@@ -4973,7 +5311,7 @@ async function executeCommitted(proposal, state2, integrationManager2) {
|
|
|
4973
5311
|
}
|
|
4974
5312
|
const resultText = result.content?.[0]?.text ?? JSON.stringify(result);
|
|
4975
5313
|
if (result.isError) {
|
|
4976
|
-
state2.executionJournal.complete(receiptId, "failed", "refused");
|
|
5314
|
+
state2.executionJournal.complete(receiptId, "failed", "refused", resultText);
|
|
4977
5315
|
return {
|
|
4978
5316
|
text: `Proposal ${proposal.id} was approved, but ${integrationId} refused to run it \u2014 nothing was done.
|
|
4979
5317
|
Reason: ${resultText}`,
|
|
@@ -5003,7 +5341,7 @@ function localOutcomeOfExecuted(proposal, state2) {
|
|
|
5003
5341
|
const integration = proposal.tool.split("__")[0];
|
|
5004
5342
|
if (entry.state === "failed") {
|
|
5005
5343
|
return {
|
|
5006
|
-
text: entry.outcome === "refused" ? `Proposal ${proposal.id} was approved, but ${integration} refused to run it \u2014 nothing was done. Do not call the tool again with the same values; a new attempt needs a new request and approval.` : `Proposal ${proposal.id} was approved, but running it failed \u2014 whether anything happened in ${integration} is unknown. Check ${integration} before trying again; a new attempt needs a new request and approval.`,
|
|
5344
|
+
text: entry.outcome === "refused" ? `Proposal ${proposal.id} was approved, but ${integration} refused to run it \u2014 nothing was done. Do not call the tool again with the same values; a new attempt needs a new request and approval.` : entry.outcome === "changed" ? `Proposal ${proposal.id} was approved, but the record changed in ${integration} since it was requested \u2014 nothing was done. A new attempt needs a new request and approval.` : `Proposal ${proposal.id} was approved, but running it failed \u2014 whether anything happened in ${integration} is unknown. Check ${integration} before trying again; a new attempt needs a new request and approval.`,
|
|
5007
5345
|
isError: true
|
|
5008
5346
|
};
|
|
5009
5347
|
}
|
|
@@ -5308,21 +5646,21 @@ function createMcpServer(state2, integrationManager2) {
|
|
|
5308
5646
|
}
|
|
5309
5647
|
|
|
5310
5648
|
// src/lib/agent-contact.ts
|
|
5311
|
-
import { existsSync as
|
|
5312
|
-
import { dirname as
|
|
5649
|
+
import { existsSync as existsSync14, mkdirSync as mkdirSync11, readFileSync as readFileSync15, renameSync as renameSync5, writeFileSync as writeFileSync11 } from "fs";
|
|
5650
|
+
import { dirname as dirname11, join as join14 } from "path";
|
|
5313
5651
|
var FILE = "agent-contact.json";
|
|
5314
5652
|
var REWRITE_AFTER_MS = 6e4;
|
|
5315
5653
|
var AgentContactStore = class {
|
|
5316
5654
|
path;
|
|
5317
5655
|
current;
|
|
5318
5656
|
constructor(dataDir2) {
|
|
5319
|
-
this.path =
|
|
5657
|
+
this.path = join14(dataDir2, FILE);
|
|
5320
5658
|
this.current = this.load();
|
|
5321
5659
|
}
|
|
5322
5660
|
load() {
|
|
5323
|
-
if (!
|
|
5661
|
+
if (!existsSync14(this.path)) return null;
|
|
5324
5662
|
try {
|
|
5325
|
-
const v = JSON.parse(
|
|
5663
|
+
const v = JSON.parse(readFileSync15(this.path, "utf8"));
|
|
5326
5664
|
if (typeof v.clientName !== "string" || typeof v.firstSeenAt !== "string" || typeof v.lastSeenAt !== "string") return null;
|
|
5327
5665
|
return { clientName: v.clientName, clientVersion: v.clientVersion, firstSeenAt: v.firstSeenAt, lastSeenAt: v.lastSeenAt };
|
|
5328
5666
|
} catch {
|
|
@@ -5345,10 +5683,10 @@ var AgentContactStore = class {
|
|
|
5345
5683
|
};
|
|
5346
5684
|
if (!due) return;
|
|
5347
5685
|
try {
|
|
5348
|
-
|
|
5686
|
+
mkdirSync11(dirname11(this.path), { recursive: true });
|
|
5349
5687
|
const tmp = `${this.path}.tmp`;
|
|
5350
|
-
|
|
5351
|
-
|
|
5688
|
+
writeFileSync11(tmp, JSON.stringify(this.current, null, 2));
|
|
5689
|
+
renameSync5(tmp, this.path);
|
|
5352
5690
|
} catch (err) {
|
|
5353
5691
|
console.error("[Suveren MCP] Could not save agent contact:", err);
|
|
5354
5692
|
}
|
|
@@ -5382,16 +5720,16 @@ function verifyGateContentHashes(content, auth) {
|
|
|
5382
5720
|
}
|
|
5383
5721
|
|
|
5384
5722
|
// src/lib/integration-registry.ts
|
|
5385
|
-
import { mkdirSync as
|
|
5386
|
-
import { homedir as
|
|
5387
|
-
import { dirname as
|
|
5388
|
-
var DEFAULT_DATA_DIR = process.env.SUVEREN_DATA_DIR ??
|
|
5723
|
+
import { mkdirSync as mkdirSync12, readFileSync as readFileSync16, writeFileSync as writeFileSync12, existsSync as existsSync15 } from "fs";
|
|
5724
|
+
import { homedir as homedir10 } from "os";
|
|
5725
|
+
import { dirname as dirname12, join as join15 } from "path";
|
|
5726
|
+
var DEFAULT_DATA_DIR = process.env.SUVEREN_DATA_DIR ?? join15(homedir10(), ".suveren");
|
|
5389
5727
|
var IntegrationRegistry = class {
|
|
5390
5728
|
integrations = /* @__PURE__ */ new Map();
|
|
5391
5729
|
filePath;
|
|
5392
5730
|
constructor(dataDir2) {
|
|
5393
5731
|
const dir = dataDir2 ?? DEFAULT_DATA_DIR;
|
|
5394
|
-
this.filePath =
|
|
5732
|
+
this.filePath = join15(dir, "integrations.json");
|
|
5395
5733
|
this.load();
|
|
5396
5734
|
}
|
|
5397
5735
|
// ─── CRUD ───────────────────────────────────────────────────────────────
|
|
@@ -5422,12 +5760,12 @@ var IntegrationRegistry = class {
|
|
|
5422
5760
|
}
|
|
5423
5761
|
// ─── Load / Save ────────────────────────────────────────────────────────
|
|
5424
5762
|
load() {
|
|
5425
|
-
if (!
|
|
5426
|
-
|
|
5763
|
+
if (!existsSync15(this.filePath)) {
|
|
5764
|
+
mkdirSync12(dirname12(this.filePath), { recursive: true });
|
|
5427
5765
|
return;
|
|
5428
5766
|
}
|
|
5429
5767
|
try {
|
|
5430
|
-
const raw =
|
|
5768
|
+
const raw = readFileSync16(this.filePath, "utf-8");
|
|
5431
5769
|
const data = JSON.parse(raw);
|
|
5432
5770
|
const migrated = data.integrations.map((i) => {
|
|
5433
5771
|
if ("toolGating" in i && !("profile" in i)) {
|
|
@@ -5456,79 +5794,19 @@ var IntegrationRegistry = class {
|
|
|
5456
5794
|
version: 1,
|
|
5457
5795
|
integrations: Array.from(this.integrations.values())
|
|
5458
5796
|
};
|
|
5459
|
-
|
|
5460
|
-
|
|
5797
|
+
mkdirSync12(dirname12(this.filePath), { recursive: true });
|
|
5798
|
+
writeFileSync12(this.filePath, JSON.stringify(data, null, 2), { encoding: "utf-8" });
|
|
5461
5799
|
}
|
|
5462
5800
|
};
|
|
5463
5801
|
|
|
5464
5802
|
// src/lib/integration-manager.ts
|
|
5465
|
-
import { homedir as
|
|
5466
|
-
import { join as
|
|
5467
|
-
import { existsSync as
|
|
5803
|
+
import { homedir as homedir11 } from "os";
|
|
5804
|
+
import { join as join16, delimiter } from "path";
|
|
5805
|
+
import { existsSync as existsSync16, writeFileSync as writeFileSync13, mkdirSync as mkdirSync13, readFileSync as readFileSync17, rmSync } from "fs";
|
|
5468
5806
|
import { execFile } from "child_process";
|
|
5469
5807
|
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
5470
5808
|
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
|
|
5471
5809
|
|
|
5472
|
-
// src/lib/manifest-loader.ts
|
|
5473
|
-
import { readFileSync as readFileSync15, existsSync as existsSync14 } from "fs";
|
|
5474
|
-
import { join as join14, resolve } from "path";
|
|
5475
|
-
var EXACT_SEMVER_RE = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/;
|
|
5476
|
-
function isExactSemver(value) {
|
|
5477
|
-
return typeof value === "string" && EXACT_SEMVER_RE.test(value);
|
|
5478
|
-
}
|
|
5479
|
-
function invalidNpmPinReason(manifest) {
|
|
5480
|
-
if (!manifest.npmPackage) return null;
|
|
5481
|
-
if (manifest.npmVersion === void 0) {
|
|
5482
|
-
return `npmPackage "${manifest.npmPackage}" is set but npmVersion is missing \u2014 every connector manifest must pin an exact tested version`;
|
|
5483
|
-
}
|
|
5484
|
-
if (!isExactSemver(manifest.npmVersion)) {
|
|
5485
|
-
return `npmVersion ${JSON.stringify(manifest.npmVersion)} for "${manifest.npmPackage}" is not an exact semver (e.g. "1.2.3") \u2014 ranges, dist-tags like "latest", git specs, URLs, and file paths are not allowed`;
|
|
5486
|
-
}
|
|
5487
|
-
return null;
|
|
5488
|
-
}
|
|
5489
|
-
var manifests = /* @__PURE__ */ new Map();
|
|
5490
|
-
function loadManifests(integrationsDir) {
|
|
5491
|
-
const dir = resolve(
|
|
5492
|
-
integrationsDir ?? process.env.SUVEREN_MANIFESTS_DIR ?? join14(import.meta.dirname ?? __dirname, "..", "..", "..", "..", "content", "integrations")
|
|
5493
|
-
);
|
|
5494
|
-
const indexPath = join14(dir, "index.json");
|
|
5495
|
-
if (!existsSync14(indexPath)) {
|
|
5496
|
-
console.error(`[ManifestLoader] No index.json found at ${indexPath}, skipping manifest loading`);
|
|
5497
|
-
return 0;
|
|
5498
|
-
}
|
|
5499
|
-
let index;
|
|
5500
|
-
try {
|
|
5501
|
-
index = JSON.parse(readFileSync15(indexPath, "utf-8"));
|
|
5502
|
-
} catch (err) {
|
|
5503
|
-
console.error(`[ManifestLoader] Failed to parse ${indexPath}:`, err);
|
|
5504
|
-
return 0;
|
|
5505
|
-
}
|
|
5506
|
-
let loaded = 0;
|
|
5507
|
-
for (const [id, relativePath] of Object.entries(index.integrations)) {
|
|
5508
|
-
const manifestPath = join14(dir, relativePath);
|
|
5509
|
-
try {
|
|
5510
|
-
const manifest = JSON.parse(readFileSync15(manifestPath, "utf-8"));
|
|
5511
|
-
const pinReason = invalidNpmPinReason(manifest);
|
|
5512
|
-
if (pinReason) {
|
|
5513
|
-
console.error(`[ManifestLoader] Refusing manifest ${id} (${manifestPath}): ${pinReason}`);
|
|
5514
|
-
continue;
|
|
5515
|
-
}
|
|
5516
|
-
manifests.set(id, manifest);
|
|
5517
|
-
loaded++;
|
|
5518
|
-
} catch (err) {
|
|
5519
|
-
console.error(`[ManifestLoader] Failed to load manifest ${id} from ${manifestPath}:`, err);
|
|
5520
|
-
}
|
|
5521
|
-
}
|
|
5522
|
-
console.error(`[ManifestLoader] Loaded ${loaded} integration manifest(s) from ${dir}`);
|
|
5523
|
-
return loaded;
|
|
5524
|
-
}
|
|
5525
|
-
function getManifest(id) {
|
|
5526
|
-
return manifests.get(id);
|
|
5527
|
-
}
|
|
5528
|
-
function getAllManifests() {
|
|
5529
|
-
return Array.from(manifests.values());
|
|
5530
|
-
}
|
|
5531
|
-
|
|
5532
5810
|
// src/lib/remote-auth-preflight.ts
|
|
5533
5811
|
var AUTH_HEADER_RE = /^authorization:\s*(.+)$/i;
|
|
5534
5812
|
function remotePreflightTarget(command, args) {
|
|
@@ -5654,8 +5932,8 @@ function inheritedConnectorEnv(parent, passEnv = []) {
|
|
|
5654
5932
|
}
|
|
5655
5933
|
|
|
5656
5934
|
// src/lib/integration-manager.ts
|
|
5657
|
-
var DEFAULT_DATA_DIR2 = process.env.SUVEREN_DATA_DIR ??
|
|
5658
|
-
var INTEGRATIONS_DIR = process.env.SUVEREN_INTEGRATIONS_DIR ??
|
|
5935
|
+
var DEFAULT_DATA_DIR2 = process.env.SUVEREN_DATA_DIR ?? join16(homedir11(), ".suveren");
|
|
5936
|
+
var INTEGRATIONS_DIR = process.env.SUVEREN_INTEGRATIONS_DIR ?? join16(DEFAULT_DATA_DIR2, "integrations");
|
|
5659
5937
|
var OFFLINE_MODE = process.env.SUVEREN_OFFLINE === "1";
|
|
5660
5938
|
var installLock = Promise.resolve();
|
|
5661
5939
|
function withInstallLock(task) {
|
|
@@ -5663,7 +5941,7 @@ function withInstallLock(task) {
|
|
|
5663
5941
|
installLock = run.then(() => void 0, () => void 0);
|
|
5664
5942
|
return run;
|
|
5665
5943
|
}
|
|
5666
|
-
var INTEGRATIONS_BIN =
|
|
5944
|
+
var INTEGRATIONS_BIN = join16(INTEGRATIONS_DIR, "node_modules", ".bin");
|
|
5667
5945
|
function getIntegrationsBinDir() {
|
|
5668
5946
|
return INTEGRATIONS_BIN;
|
|
5669
5947
|
}
|
|
@@ -5672,12 +5950,12 @@ function buildPath() {
|
|
|
5672
5950
|
return [INTEGRATIONS_BIN, base].join(delimiter);
|
|
5673
5951
|
}
|
|
5674
5952
|
function ensureIntegrationsDir() {
|
|
5675
|
-
if (!
|
|
5676
|
-
|
|
5953
|
+
if (!existsSync16(INTEGRATIONS_DIR)) {
|
|
5954
|
+
mkdirSync13(INTEGRATIONS_DIR, { recursive: true });
|
|
5677
5955
|
}
|
|
5678
|
-
const pkgPath =
|
|
5679
|
-
if (!
|
|
5680
|
-
|
|
5956
|
+
const pkgPath = join16(INTEGRATIONS_DIR, "package.json");
|
|
5957
|
+
if (!existsSync16(pkgPath)) {
|
|
5958
|
+
writeFileSync13(pkgPath, JSON.stringify({ name: "suveren-integrations", version: "1.0.0", private: true }, null, 2));
|
|
5681
5959
|
}
|
|
5682
5960
|
}
|
|
5683
5961
|
function readAgeOf(config) {
|
|
@@ -5766,19 +6044,19 @@ var IntegrationManager = class {
|
|
|
5766
6044
|
* exist, and treat anything else as not-installed (and reinstallable).
|
|
5767
6045
|
*/
|
|
5768
6046
|
isUsableInstall(npmPackage) {
|
|
5769
|
-
const pkgDir =
|
|
5770
|
-
const pkgJson =
|
|
5771
|
-
if (!
|
|
6047
|
+
const pkgDir = join16(INTEGRATIONS_DIR, "node_modules", ...npmPackage.split("/"));
|
|
6048
|
+
const pkgJson = join16(pkgDir, "package.json");
|
|
6049
|
+
if (!existsSync16(pkgDir) || !existsSync16(pkgJson)) return false;
|
|
5772
6050
|
try {
|
|
5773
|
-
const pkg = JSON.parse(
|
|
6051
|
+
const pkg = JSON.parse(readFileSync17(pkgJson, "utf8"));
|
|
5774
6052
|
const defaultBinName = (pkg.name ?? npmPackage).split("/").pop() ?? "";
|
|
5775
6053
|
const binMap = typeof pkg.bin === "string" ? { [defaultBinName]: pkg.bin } : pkg.bin ?? {};
|
|
5776
6054
|
const binEntries = Object.entries(binMap);
|
|
5777
6055
|
for (const [binName, target] of binEntries) {
|
|
5778
|
-
if (!
|
|
6056
|
+
if (!existsSync16(join16(pkgDir, target))) return false;
|
|
5779
6057
|
if (!this.binShimExists(binName)) return false;
|
|
5780
6058
|
}
|
|
5781
|
-
if (binEntries.length === 0 && pkg.main && !
|
|
6059
|
+
if (binEntries.length === 0 && pkg.main && !existsSync16(join16(pkgDir, pkg.main))) return false;
|
|
5782
6060
|
return true;
|
|
5783
6061
|
} catch {
|
|
5784
6062
|
return false;
|
|
@@ -5793,9 +6071,9 @@ var IntegrationManager = class {
|
|
|
5793
6071
|
*/
|
|
5794
6072
|
readInstalledVersion(npmPackage) {
|
|
5795
6073
|
if (!this.isUsableInstall(npmPackage)) return null;
|
|
5796
|
-
const pkgJson =
|
|
6074
|
+
const pkgJson = join16(INTEGRATIONS_DIR, "node_modules", ...npmPackage.split("/"), "package.json");
|
|
5797
6075
|
try {
|
|
5798
|
-
const pkg = JSON.parse(
|
|
6076
|
+
const pkg = JSON.parse(readFileSync17(pkgJson, "utf8"));
|
|
5799
6077
|
return typeof pkg.version === "string" ? pkg.version : null;
|
|
5800
6078
|
} catch {
|
|
5801
6079
|
return null;
|
|
@@ -5809,7 +6087,7 @@ var IntegrationManager = class {
|
|
|
5809
6087
|
binShimExists(binName) {
|
|
5810
6088
|
if (!binName) return false;
|
|
5811
6089
|
const candidates = process.platform === "win32" ? [binName, `${binName}.cmd`, `${binName}.CMD`, `${binName}.ps1`] : [binName];
|
|
5812
|
-
return candidates.some((c) =>
|
|
6090
|
+
return candidates.some((c) => existsSync16(join16(INTEGRATIONS_BIN, c)));
|
|
5813
6091
|
}
|
|
5814
6092
|
/**
|
|
5815
6093
|
* The exact version THIS gateway's CURRENTLY LOADED manifest pins
|
|
@@ -5899,8 +6177,8 @@ var IntegrationManager = class {
|
|
|
5899
6177
|
* nothing at all.
|
|
5900
6178
|
*/
|
|
5901
6179
|
async installNow(npmPackage, exactVersion, cleanFirst = true) {
|
|
5902
|
-
const pkgDir =
|
|
5903
|
-
if (cleanFirst &&
|
|
6180
|
+
const pkgDir = join16(INTEGRATIONS_DIR, "node_modules", ...npmPackage.split("/"));
|
|
6181
|
+
if (cleanFirst && existsSync16(pkgDir)) {
|
|
5904
6182
|
console.error(`[IntegrationManager] Removing ${exactVersion ? "outdated" : "unusable"} install of ${npmPackage}`);
|
|
5905
6183
|
try {
|
|
5906
6184
|
rmSync(pkgDir, { recursive: true, force: true });
|
|
@@ -6093,6 +6371,7 @@ var IntegrationManager = class {
|
|
|
6093
6371
|
tool.inputSchema ?? {},
|
|
6094
6372
|
gating?.blockedArgs
|
|
6095
6373
|
),
|
|
6374
|
+
outputSchema: tool.outputSchema,
|
|
6096
6375
|
gating
|
|
6097
6376
|
};
|
|
6098
6377
|
});
|
|
@@ -6257,7 +6536,11 @@ var IntegrationManager = class {
|
|
|
6257
6536
|
type: item.type,
|
|
6258
6537
|
text: item.text ?? JSON.stringify(item)
|
|
6259
6538
|
}));
|
|
6260
|
-
return {
|
|
6539
|
+
return {
|
|
6540
|
+
content,
|
|
6541
|
+
isError: result.isError,
|
|
6542
|
+
structuredContent: result.structuredContent
|
|
6543
|
+
};
|
|
6261
6544
|
} catch (err) {
|
|
6262
6545
|
const message = err instanceof Error ? err.message : String(err);
|
|
6263
6546
|
return {
|
|
@@ -6806,28 +7089,28 @@ function agentAccess(token, opensSession) {
|
|
|
6806
7089
|
}
|
|
6807
7090
|
|
|
6808
7091
|
// src/lib/profile-loader.ts
|
|
6809
|
-
import { readFileSync as
|
|
6810
|
-
import { join as
|
|
7092
|
+
import { readFileSync as readFileSync18, existsSync as existsSync17 } from "fs";
|
|
7093
|
+
import { join as join17, resolve as resolve2 } from "path";
|
|
6811
7094
|
import { registerProfile } from "@hap/core";
|
|
6812
7095
|
function loadProfiles(profilesDir) {
|
|
6813
|
-
const dir = resolve2(profilesDir ?? process.env.SUVEREN_PROFILES_DIR ??
|
|
6814
|
-
const indexPath =
|
|
6815
|
-
if (!
|
|
7096
|
+
const dir = resolve2(profilesDir ?? process.env.SUVEREN_PROFILES_DIR ?? join17(import.meta.dirname ?? __dirname, "..", "..", "..", "..", "..", "hap-profiles"));
|
|
7097
|
+
const indexPath = join17(dir, "index.json");
|
|
7098
|
+
if (!existsSync17(indexPath)) {
|
|
6816
7099
|
warnNoProfiles(`No index.json at ${indexPath}`);
|
|
6817
7100
|
return 0;
|
|
6818
7101
|
}
|
|
6819
7102
|
let index;
|
|
6820
7103
|
try {
|
|
6821
|
-
index = JSON.parse(
|
|
7104
|
+
index = JSON.parse(readFileSync18(indexPath, "utf-8"));
|
|
6822
7105
|
} catch (err) {
|
|
6823
7106
|
console.error(`[ProfileLoader] Failed to parse ${indexPath}:`, err);
|
|
6824
7107
|
return 0;
|
|
6825
7108
|
}
|
|
6826
7109
|
let loaded = 0;
|
|
6827
7110
|
for (const [profileId, relativePath] of Object.entries(index.profiles)) {
|
|
6828
|
-
const profilePath =
|
|
7111
|
+
const profilePath = join17(dir, relativePath);
|
|
6829
7112
|
try {
|
|
6830
|
-
const profile = JSON.parse(
|
|
7113
|
+
const profile = JSON.parse(readFileSync18(profilePath, "utf-8"));
|
|
6831
7114
|
registerProfile(profileId, profile);
|
|
6832
7115
|
const shortName = profileId.split("/").pop()?.replace(/@.*$/, "");
|
|
6833
7116
|
if (shortName && shortName !== profileId) {
|
|
@@ -7165,20 +7448,20 @@ async function controlPlaneMandate(dryRun, request) {
|
|
|
7165
7448
|
}
|
|
7166
7449
|
|
|
7167
7450
|
// src/lib/guides.ts
|
|
7168
|
-
import { existsSync as
|
|
7169
|
-
import { dirname as
|
|
7451
|
+
import { existsSync as existsSync18, readdirSync, readFileSync as readFileSync19 } from "fs";
|
|
7452
|
+
import { dirname as dirname13, join as join18 } from "path";
|
|
7170
7453
|
var FILE_RE = /^(?:(\d+)-)?([a-z0-9][a-z0-9-]*)\.md$/;
|
|
7171
7454
|
function builtinGuidesDir() {
|
|
7172
7455
|
if (process.env.SUVEREN_BUILTIN_GUIDES_DIR) return process.env.SUVEREN_BUILTIN_GUIDES_DIR;
|
|
7173
7456
|
let dir = import.meta.dirname ?? __dirname;
|
|
7174
7457
|
for (let i = 0; i < 8; i++) {
|
|
7175
|
-
const candidate =
|
|
7176
|
-
if (
|
|
7177
|
-
const parent =
|
|
7458
|
+
const candidate = join18(dir, "content", "guides");
|
|
7459
|
+
if (existsSync18(candidate)) return candidate;
|
|
7460
|
+
const parent = dirname13(dir);
|
|
7178
7461
|
if (parent === dir) break;
|
|
7179
7462
|
dir = parent;
|
|
7180
7463
|
}
|
|
7181
|
-
return
|
|
7464
|
+
return join18(import.meta.dirname ?? __dirname, "content", "guides");
|
|
7182
7465
|
}
|
|
7183
7466
|
function parse(body) {
|
|
7184
7467
|
const lines = body.split(/\r?\n/);
|
|
@@ -7189,14 +7472,14 @@ function parse(body) {
|
|
|
7189
7472
|
return { title, summary };
|
|
7190
7473
|
}
|
|
7191
7474
|
function readDir(dir, source) {
|
|
7192
|
-
if (!dir || !
|
|
7475
|
+
if (!dir || !existsSync18(dir)) return [];
|
|
7193
7476
|
const out = [];
|
|
7194
7477
|
for (const file of readdirSync(dir)) {
|
|
7195
7478
|
const m = FILE_RE.exec(file);
|
|
7196
7479
|
if (!m) continue;
|
|
7197
7480
|
let body;
|
|
7198
7481
|
try {
|
|
7199
|
-
body =
|
|
7482
|
+
body = readFileSync19(join18(dir, file), "utf8");
|
|
7200
7483
|
} catch {
|
|
7201
7484
|
continue;
|
|
7202
7485
|
}
|
|
@@ -7506,8 +7789,8 @@ function registerBuiltins(deps, factories = BUILTIN_FACTORIES) {
|
|
|
7506
7789
|
import { decodeMandateBlob as decodeMandateBlob3 } from "@hap/core";
|
|
7507
7790
|
|
|
7508
7791
|
// src/lib/committed-executor.ts
|
|
7509
|
-
import { existsSync as
|
|
7510
|
-
import { dirname as
|
|
7792
|
+
import { existsSync as existsSync19, mkdirSync as mkdirSync14, readFileSync as readFileSync20, unlinkSync as unlinkSync6, writeFileSync as writeFileSync14 } from "fs";
|
|
7793
|
+
import { dirname as dirname14, join as join19 } from "path";
|
|
7511
7794
|
var CommittedExecutor = class {
|
|
7512
7795
|
constructor(run) {
|
|
7513
7796
|
this.run = run;
|
|
@@ -7535,7 +7818,7 @@ var ExecutorLock = class {
|
|
|
7535
7818
|
path;
|
|
7536
7819
|
held = false;
|
|
7537
7820
|
constructor(dataDir2) {
|
|
7538
|
-
this.path =
|
|
7821
|
+
this.path = join19(dataDir2, "executor.lock");
|
|
7539
7822
|
}
|
|
7540
7823
|
/** Try to take the lock. Returns the holder's pid when someone else has it. */
|
|
7541
7824
|
acquire() {
|
|
@@ -7543,9 +7826,9 @@ var ExecutorLock = class {
|
|
|
7543
7826
|
if (current && current.pid !== process.pid && isAlive(current.pid)) {
|
|
7544
7827
|
return { held: false, holderPid: current.pid };
|
|
7545
7828
|
}
|
|
7546
|
-
|
|
7829
|
+
mkdirSync14(dirname14(this.path), { recursive: true, mode: 448 });
|
|
7547
7830
|
const lock = { pid: process.pid, startedAt: Math.floor(Date.now() / 1e3) };
|
|
7548
|
-
|
|
7831
|
+
writeFileSync14(this.path, JSON.stringify(lock), { mode: 384 });
|
|
7549
7832
|
this.held = true;
|
|
7550
7833
|
return { held: true };
|
|
7551
7834
|
}
|
|
@@ -7567,9 +7850,9 @@ var ExecutorLock = class {
|
|
|
7567
7850
|
}
|
|
7568
7851
|
}
|
|
7569
7852
|
read() {
|
|
7570
|
-
if (!
|
|
7853
|
+
if (!existsSync19(this.path)) return null;
|
|
7571
7854
|
try {
|
|
7572
|
-
const parsed = JSON.parse(
|
|
7855
|
+
const parsed = JSON.parse(readFileSync20(this.path, "utf8"));
|
|
7573
7856
|
return typeof parsed?.pid === "number" ? parsed : null;
|
|
7574
7857
|
} catch {
|
|
7575
7858
|
return null;
|
|
@@ -7586,20 +7869,20 @@ function isAlive(pid) {
|
|
|
7586
7869
|
}
|
|
7587
7870
|
|
|
7588
7871
|
// bin/http.ts
|
|
7589
|
-
import { homedir as
|
|
7590
|
-
import { join as
|
|
7872
|
+
import { homedir as homedir12 } from "os";
|
|
7873
|
+
import { join as join21 } from "path";
|
|
7591
7874
|
|
|
7592
7875
|
// src/lib/mcp-as-tracker.ts
|
|
7593
|
-
import { existsSync as
|
|
7594
|
-
import { dirname as
|
|
7876
|
+
import { existsSync as existsSync20, mkdirSync as mkdirSync15, readFileSync as readFileSync21, writeFileSync as writeFileSync15 } from "fs";
|
|
7877
|
+
import { dirname as dirname15, join as join20 } from "path";
|
|
7595
7878
|
function trackerPath(dataDir2) {
|
|
7596
|
-
return
|
|
7879
|
+
return join20(dataDir2, "mcp-as-pairing.json");
|
|
7597
7880
|
}
|
|
7598
7881
|
function readMcpPairedAsUrl(dataDir2) {
|
|
7599
7882
|
const path = trackerPath(dataDir2);
|
|
7600
|
-
if (!
|
|
7883
|
+
if (!existsSync20(path)) return null;
|
|
7601
7884
|
try {
|
|
7602
|
-
const data = JSON.parse(
|
|
7885
|
+
const data = JSON.parse(readFileSync21(path, "utf-8"));
|
|
7603
7886
|
return typeof data.asUrl === "string" ? data.asUrl : null;
|
|
7604
7887
|
} catch {
|
|
7605
7888
|
return null;
|
|
@@ -7607,12 +7890,12 @@ function readMcpPairedAsUrl(dataDir2) {
|
|
|
7607
7890
|
}
|
|
7608
7891
|
function writeMcpPairedAsUrl(dataDir2, asUrl) {
|
|
7609
7892
|
const path = trackerPath(dataDir2);
|
|
7610
|
-
|
|
7611
|
-
|
|
7893
|
+
mkdirSync15(dirname15(path), { recursive: true, mode: 448 });
|
|
7894
|
+
writeFileSync15(path, JSON.stringify({ asUrl }, null, 2), { encoding: "utf-8", mode: 384 });
|
|
7612
7895
|
}
|
|
7613
7896
|
|
|
7614
7897
|
// bin/http.ts
|
|
7615
|
-
var dataDir = process.env.SUVEREN_DATA_DIR ??
|
|
7898
|
+
var dataDir = process.env.SUVEREN_DATA_DIR ?? join21(homedir12(), ".suveren");
|
|
7616
7899
|
var spUrl = resolveAsUrl(dataDir);
|
|
7617
7900
|
validatePinTlsForUrl(resolvePinTls(dataDir), spUrl);
|
|
7618
7901
|
var port = parseInt(process.env.SUVEREN_MCP_PORT ?? "3430", 10);
|
|
@@ -7822,6 +8105,29 @@ app.post("/internal/resync-gates", internalOnly, async (_req, res) => {
|
|
|
7822
8105
|
}
|
|
7823
8106
|
res.json({ ok: true, synced, orphaned });
|
|
7824
8107
|
});
|
|
8108
|
+
app.post("/internal/preview", internalOnly, async (req, res) => {
|
|
8109
|
+
const { proposalId, tool, toolArgs } = req.body;
|
|
8110
|
+
if (!proposalId || !tool || typeof toolArgs !== "object" || toolArgs === null) {
|
|
8111
|
+
res.status(400).json({ error: "Missing proposalId, tool, or toolArgs" });
|
|
8112
|
+
return;
|
|
8113
|
+
}
|
|
8114
|
+
const sep = tool.indexOf("__");
|
|
8115
|
+
if (sep < 0) {
|
|
8116
|
+
res.status(400).json({ error: `Invalid tool name: ${tool}` });
|
|
8117
|
+
return;
|
|
8118
|
+
}
|
|
8119
|
+
const integrationId = tool.slice(0, sep);
|
|
8120
|
+
const toolName = tool.slice(sep + 2);
|
|
8121
|
+
const result = await buildInternalPreview(integrationManager, integrationId, toolName, toolArgs);
|
|
8122
|
+
state.previewLog.record({
|
|
8123
|
+
ts: Date.now(),
|
|
8124
|
+
proposalId,
|
|
8125
|
+
integrationId,
|
|
8126
|
+
tool: toolName,
|
|
8127
|
+
status: result.status
|
|
8128
|
+
});
|
|
8129
|
+
res.json(result);
|
|
8130
|
+
});
|
|
7825
8131
|
app.post("/internal/add-integration", internalOnly, async (req, res) => {
|
|
7826
8132
|
try {
|
|
7827
8133
|
const config = req.body;
|
|
@@ -8315,7 +8621,7 @@ app.listen(port, bindHost, () => {
|
|
|
8315
8621
|
(proposal) => executeCommitted(proposal, state, integrationManager)
|
|
8316
8622
|
);
|
|
8317
8623
|
installCommittedExecutor(committedExecutor);
|
|
8318
|
-
const dataDir2 = process.env.SUVEREN_DATA_DIR ??
|
|
8624
|
+
const dataDir2 = process.env.SUVEREN_DATA_DIR ?? join21(homedir12(), ".suveren");
|
|
8319
8625
|
const executorLock = new ExecutorLock(dataDir2);
|
|
8320
8626
|
const lock = executorLock.acquire();
|
|
8321
8627
|
if (!lock.held) {
|