@suveren/gateway 0.20.2 → 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 +15 -3
- package/dist/control-plane/index.mjs +179 -46
- package/dist/mcp-server/http.mjs +623 -181
- package/dist/ui/assets/{index-blnpvM2g.css → index-BSAkn_Of.css} +1 -1
- package/dist/ui/assets/index-CBoGbefI.js +116 -0
- package/dist/ui/index.html +2 -2
- package/node_modules/@modelcontextprotocol/sdk/node_modules/content-disposition/README.md +105 -35
- package/node_modules/@modelcontextprotocol/sdk/node_modules/content-disposition/dist/index.d.ts +72 -0
- package/node_modules/@modelcontextprotocol/sdk/node_modules/content-disposition/dist/index.js +388 -0
- package/node_modules/@modelcontextprotocol/sdk/node_modules/content-disposition/dist/index.js.map +1 -0
- package/node_modules/@modelcontextprotocol/sdk/node_modules/content-disposition/package.json +31 -19
- package/node_modules/@modelcontextprotocol/sdk/node_modules/express/Readme.md +15 -9
- package/node_modules/@modelcontextprotocol/sdk/node_modules/express/lib/application.js +2 -2
- package/node_modules/@modelcontextprotocol/sdk/node_modules/express/lib/request.js +32 -19
- package/node_modules/@modelcontextprotocol/sdk/node_modules/express/lib/response.js +27 -30
- package/node_modules/@modelcontextprotocol/sdk/node_modules/express/node_modules/content-type/LICENSE +22 -0
- package/node_modules/@modelcontextprotocol/sdk/node_modules/express/node_modules/content-type/README.md +71 -0
- package/node_modules/@modelcontextprotocol/sdk/node_modules/express/node_modules/content-type/dist/index.d.ts +46 -0
- package/node_modules/@modelcontextprotocol/sdk/node_modules/express/node_modules/content-type/dist/index.js +176 -0
- package/node_modules/@modelcontextprotocol/sdk/node_modules/express/node_modules/content-type/dist/index.js.map +1 -0
- package/node_modules/@modelcontextprotocol/sdk/node_modules/express/node_modules/content-type/package.json +52 -0
- package/node_modules/@modelcontextprotocol/sdk/node_modules/express/package.json +10 -10
- package/node_modules/@types/node/README.md +1 -1
- package/node_modules/@types/node/package.json +2 -2
- package/node_modules/@types/node/util.d.ts +6 -3
- package/package.json +1 -1
- package/dist/ui/assets/index-1s1q-XLL.js +0 -116
- package/node_modules/@modelcontextprotocol/sdk/node_modules/content-disposition/index.js +0 -536
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,15 +1970,30 @@ 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;
|
|
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
|
+
}
|
|
1978
1987
|
row.finishedAt = nowSec();
|
|
1979
1988
|
this.persist(file);
|
|
1980
1989
|
}
|
|
1990
|
+
/** The newest row for a review-path proposal — what really happened when it
|
|
1991
|
+
* ran on this gateway (the AS marks a proposal "executed" when it issues the
|
|
1992
|
+
* ticket, before the action runs). Undefined when it never ran here. */
|
|
1993
|
+
findByProposal(proposalId) {
|
|
1994
|
+
const rows = this.load().entries.filter((e) => e.proposalId === proposalId);
|
|
1995
|
+
return rows[rows.length - 1];
|
|
1996
|
+
}
|
|
1981
1997
|
/** All rows, newest last. Diagnostic. */
|
|
1982
1998
|
getAll() {
|
|
1983
1999
|
return [...this.load().entries];
|
|
@@ -2037,7 +2053,8 @@ var ProposalSubmissionStore = class {
|
|
|
2037
2053
|
executionContextHash: hashToolArgs(entry.executionContext),
|
|
2038
2054
|
authorizationId: entry.authorizationId,
|
|
2039
2055
|
profileId: entry.profileId,
|
|
2040
|
-
submittedAt: nowSec2()
|
|
2056
|
+
submittedAt: nowSec2(),
|
|
2057
|
+
...entry.previewHash ? { previewHash: entry.previewHash } : {}
|
|
2041
2058
|
});
|
|
2042
2059
|
this.persist(file);
|
|
2043
2060
|
}
|
|
@@ -2065,6 +2082,56 @@ var ProposalSubmissionStore = class {
|
|
|
2065
2082
|
}
|
|
2066
2083
|
};
|
|
2067
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
|
+
|
|
2068
2135
|
// src/lib/gatekeeper.ts
|
|
2069
2136
|
import { verify } from "@hap/core";
|
|
2070
2137
|
var MCPGatekeeper = class {
|
|
@@ -2157,9 +2224,9 @@ var MCPGatekeeper = class {
|
|
|
2157
2224
|
};
|
|
2158
2225
|
|
|
2159
2226
|
// src/lib/report/report-store.ts
|
|
2160
|
-
import { mkdirSync as
|
|
2161
|
-
import { homedir as
|
|
2162
|
-
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";
|
|
2163
2230
|
import { createCipheriv as createCipheriv5, createDecipheriv as createDecipheriv5, randomBytes as randomBytes5 } from "crypto";
|
|
2164
2231
|
|
|
2165
2232
|
// src/lib/report/record-resolvers.ts
|
|
@@ -2468,27 +2535,27 @@ async function buildCoverage(caseElements, getExport, archive, referenced, windo
|
|
|
2468
2535
|
}
|
|
2469
2536
|
|
|
2470
2537
|
// src/lib/report/report-store.ts
|
|
2471
|
-
var
|
|
2538
|
+
var DEFAULT_DIR8 = process.env.SUVEREN_DATA_DIR ?? join11(homedir8(), ".suveren");
|
|
2472
2539
|
var ReportStore = class {
|
|
2473
2540
|
report = null;
|
|
2474
2541
|
baseDir;
|
|
2475
2542
|
vaultKey = null;
|
|
2476
2543
|
constructor(filePath) {
|
|
2477
2544
|
if (filePath && filePath.endsWith(".json")) {
|
|
2478
|
-
this.baseDir =
|
|
2545
|
+
this.baseDir = dirname9(filePath);
|
|
2479
2546
|
} else {
|
|
2480
|
-
this.baseDir = filePath ??
|
|
2547
|
+
this.baseDir = filePath ?? DEFAULT_DIR8;
|
|
2481
2548
|
}
|
|
2482
2549
|
this.loadPlaintext();
|
|
2483
2550
|
}
|
|
2484
2551
|
/** Set the vault key for encryption. Triggers migration from plaintext if needed. */
|
|
2485
2552
|
setVaultKey(key) {
|
|
2486
2553
|
this.vaultKey = key;
|
|
2487
|
-
if (
|
|
2554
|
+
if (existsSync11(this.encryptedFilePath)) {
|
|
2488
2555
|
this.loadEncrypted();
|
|
2489
2556
|
} else if (this.report !== null) {
|
|
2490
2557
|
this.persistEncrypted();
|
|
2491
|
-
if (
|
|
2558
|
+
if (existsSync11(this.plaintextFilePath)) {
|
|
2492
2559
|
try {
|
|
2493
2560
|
unlinkSync5(this.plaintextFilePath);
|
|
2494
2561
|
} catch {
|
|
@@ -2503,7 +2570,7 @@ var ReportStore = class {
|
|
|
2503
2570
|
* "the report exists but is locked".
|
|
2504
2571
|
*/
|
|
2505
2572
|
isLocked() {
|
|
2506
|
-
return this.vaultKey === null &&
|
|
2573
|
+
return this.vaultKey === null && existsSync11(this.encryptedFilePath);
|
|
2507
2574
|
}
|
|
2508
2575
|
/** The stored, already-verified report — or null if none has been saved
|
|
2509
2576
|
* (and the store is not locked; check `isLocked()` first). */
|
|
@@ -2552,34 +2619,34 @@ var ReportStore = class {
|
|
|
2552
2619
|
}
|
|
2553
2620
|
// ─── File paths ─────────────────────────────────────────────────────────
|
|
2554
2621
|
get plaintextFilePath() {
|
|
2555
|
-
return
|
|
2622
|
+
return join11(this.baseDir, "report.json");
|
|
2556
2623
|
}
|
|
2557
2624
|
get encryptedFilePath() {
|
|
2558
|
-
return
|
|
2625
|
+
return join11(this.baseDir, "report.enc.json");
|
|
2559
2626
|
}
|
|
2560
2627
|
// ─── Load / Persist ─────────────────────────────────────────────────────
|
|
2561
2628
|
loadPlaintext() {
|
|
2562
|
-
if (!
|
|
2563
|
-
|
|
2629
|
+
if (!existsSync11(this.plaintextFilePath)) {
|
|
2630
|
+
mkdirSync9(this.baseDir, { recursive: true });
|
|
2564
2631
|
return;
|
|
2565
2632
|
}
|
|
2566
2633
|
try {
|
|
2567
|
-
const raw =
|
|
2634
|
+
const raw = readFileSync12(this.plaintextFilePath, "utf-8");
|
|
2568
2635
|
const parsed = JSON.parse(raw);
|
|
2569
2636
|
this.report = parsed.report ?? null;
|
|
2570
2637
|
} catch {
|
|
2571
2638
|
console.error(`[ReportStore] Could not parse ${this.plaintextFilePath} \u2014 preserving as .corrupt`);
|
|
2572
2639
|
try {
|
|
2573
|
-
|
|
2640
|
+
writeFileSync9(`${this.plaintextFilePath}.corrupt-${Date.now()}`, readFileSync12(this.plaintextFilePath));
|
|
2574
2641
|
} catch {
|
|
2575
2642
|
}
|
|
2576
2643
|
this.report = null;
|
|
2577
2644
|
}
|
|
2578
2645
|
}
|
|
2579
2646
|
loadEncrypted() {
|
|
2580
|
-
if (!
|
|
2647
|
+
if (!existsSync11(this.encryptedFilePath)) return;
|
|
2581
2648
|
try {
|
|
2582
|
-
const raw =
|
|
2649
|
+
const raw = readFileSync12(this.encryptedFilePath, "utf-8");
|
|
2583
2650
|
const data = JSON.parse(raw);
|
|
2584
2651
|
const decrypted = this.decrypt(data.blob);
|
|
2585
2652
|
const parsed = JSON.parse(decrypted);
|
|
@@ -2587,7 +2654,7 @@ var ReportStore = class {
|
|
|
2587
2654
|
} catch (err) {
|
|
2588
2655
|
console.error(`[ReportStore] Could not decrypt ${this.encryptedFilePath} \u2014 preserving as .corrupt:`, err);
|
|
2589
2656
|
try {
|
|
2590
|
-
|
|
2657
|
+
writeFileSync9(`${this.encryptedFilePath}.corrupt-${Date.now()}`, readFileSync12(this.encryptedFilePath));
|
|
2591
2658
|
} catch {
|
|
2592
2659
|
}
|
|
2593
2660
|
this.report = null;
|
|
@@ -2602,16 +2669,16 @@ var ReportStore = class {
|
|
|
2602
2669
|
}
|
|
2603
2670
|
persistPlaintext() {
|
|
2604
2671
|
const data = { version: 1, report: this.report };
|
|
2605
|
-
|
|
2606
|
-
|
|
2672
|
+
mkdirSync9(this.baseDir, { recursive: true, mode: 448 });
|
|
2673
|
+
writeFileSync9(this.plaintextFilePath, JSON.stringify(data, null, 2), { encoding: "utf-8", mode: 384 });
|
|
2607
2674
|
}
|
|
2608
2675
|
persistEncrypted() {
|
|
2609
2676
|
const data = {
|
|
2610
2677
|
version: 1,
|
|
2611
2678
|
blob: this.encrypt(JSON.stringify({ version: 1, report: this.report }))
|
|
2612
2679
|
};
|
|
2613
|
-
|
|
2614
|
-
|
|
2680
|
+
mkdirSync9(this.baseDir, { recursive: true, mode: 448 });
|
|
2681
|
+
writeFileSync9(this.encryptedFilePath, JSON.stringify(data, null, 2), { encoding: "utf-8", mode: 384 });
|
|
2615
2682
|
}
|
|
2616
2683
|
};
|
|
2617
2684
|
|
|
@@ -2632,6 +2699,8 @@ var SharedState = class {
|
|
|
2632
2699
|
/** What THIS gateway submitted for each review-mode proposal it created —
|
|
2633
2700
|
* see ticket-verify.ts / commitments.ts. */
|
|
2634
2701
|
proposalSubmissions;
|
|
2702
|
+
/** AU3 — local, content-free record of preview reads. See preview-log.ts. */
|
|
2703
|
+
previewLog;
|
|
2635
2704
|
gatekeeper;
|
|
2636
2705
|
/**
|
|
2637
2706
|
* V7 — set by {@link checkAsCompat} (called once at process start, and
|
|
@@ -2665,6 +2734,7 @@ var SharedState = class {
|
|
|
2665
2734
|
this.reportStore = new ReportStore(gateStorePath);
|
|
2666
2735
|
this.executionJournal = new ExecutionJournal(gateStorePath);
|
|
2667
2736
|
this.proposalSubmissions = new ProposalSubmissionStore(gateStorePath);
|
|
2737
|
+
this.previewLog = new PreviewLog(gateStorePath);
|
|
2668
2738
|
this.gatekeeper = new MCPGatekeeper(this.cache);
|
|
2669
2739
|
}
|
|
2670
2740
|
/**
|
|
@@ -2853,29 +2923,29 @@ function formatConsumptionFull(entries) {
|
|
|
2853
2923
|
}
|
|
2854
2924
|
|
|
2855
2925
|
// src/lib/context-loader.ts
|
|
2856
|
-
import { readFileSync as
|
|
2857
|
-
import { homedir as
|
|
2858
|
-
import { join as
|
|
2859
|
-
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");
|
|
2860
2930
|
var CONTEXT_MAX_BYTES = 16 * 1024;
|
|
2861
2931
|
function contextFilePath(dataDir2) {
|
|
2862
|
-
return
|
|
2932
|
+
return join12(dataDir2 ?? DEFAULT_DIR9, "context.md");
|
|
2863
2933
|
}
|
|
2864
2934
|
function writeContextFile(content, dataDir2) {
|
|
2865
2935
|
const bytes = Buffer.byteLength(content, "utf-8");
|
|
2866
2936
|
if (bytes > CONTEXT_MAX_BYTES) throw new Error(`the brief is ${bytes} bytes; the limit is ${CONTEXT_MAX_BYTES}.`);
|
|
2867
2937
|
const filePath = contextFilePath(dataDir2);
|
|
2868
|
-
|
|
2938
|
+
mkdirSync10(dirname10(filePath), { recursive: true });
|
|
2869
2939
|
const tmpPath = `${filePath}.tmp`;
|
|
2870
|
-
|
|
2871
|
-
|
|
2940
|
+
writeFileSync10(tmpPath, content, "utf-8");
|
|
2941
|
+
renameSync4(tmpPath, filePath);
|
|
2872
2942
|
}
|
|
2873
2943
|
var BRIEF_MAX_CHARS = 16 * 1024;
|
|
2874
2944
|
function readContextFile(dataDir2) {
|
|
2875
2945
|
const filePath = contextFilePath(dataDir2);
|
|
2876
|
-
if (!
|
|
2946
|
+
if (!existsSync12(filePath)) return null;
|
|
2877
2947
|
try {
|
|
2878
|
-
const content =
|
|
2948
|
+
const content = readFileSync13(filePath, "utf-8").trim();
|
|
2879
2949
|
return content || null;
|
|
2880
2950
|
} catch {
|
|
2881
2951
|
return null;
|
|
@@ -3061,6 +3131,252 @@ function attachTicketId(tool, args, ticketId) {
|
|
|
3061
3131
|
return { ...args, ticket_id: ticketId };
|
|
3062
3132
|
}
|
|
3063
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
|
+
|
|
3064
3380
|
// src/lib/ticket-verify.ts
|
|
3065
3381
|
import { verifyTicketSignature } from "@hap/core";
|
|
3066
3382
|
var TicketBindingMismatchError = class extends Error {
|
|
@@ -3563,6 +3879,7 @@ function denyRead(state2, tool, reason, detail, target) {
|
|
|
3563
3879
|
tool: tool.originalName,
|
|
3564
3880
|
integrationId: tool.integrationId,
|
|
3565
3881
|
profile: tool.gating?.profile ?? null,
|
|
3882
|
+
kind: "read",
|
|
3566
3883
|
reason,
|
|
3567
3884
|
detail,
|
|
3568
3885
|
target
|
|
@@ -3571,15 +3888,62 @@ function denyRead(state2, tool, reason, detail, target) {
|
|
|
3571
3888
|
}
|
|
3572
3889
|
return { content: [{ type: "text", text: `Read blocked by Gatekeeper: ${detail}` }], isError: true };
|
|
3573
3890
|
}
|
|
3891
|
+
function recordBlocked(state2, tool, rec) {
|
|
3892
|
+
state2.denialLog?.record({
|
|
3893
|
+
ts: Date.now(),
|
|
3894
|
+
tool: tool.originalName,
|
|
3895
|
+
integrationId: tool.integrationId,
|
|
3896
|
+
profile: tool.gating?.profile ?? null,
|
|
3897
|
+
...rec
|
|
3898
|
+
});
|
|
3899
|
+
}
|
|
3900
|
+
function recordGatekeeperRefusal(state2, tool, refusals) {
|
|
3901
|
+
if (refusals.length === 0) return;
|
|
3902
|
+
const pick = refusals.find((r) => r.bound !== void 0 && r.actual !== void 0) ?? refusals[0];
|
|
3903
|
+
const scoped = pick.field ? Boolean(getProfile2(pick.profileId)?.scopeSchema?.fields?.[pick.field]) : false;
|
|
3904
|
+
const asValueOrString = (v) => typeof v === "number" || typeof v === "string" ? v : void 0;
|
|
3905
|
+
recordBlocked(state2, tool, {
|
|
3906
|
+
kind: scoped ? "scope" : "bound",
|
|
3907
|
+
detail: "Refused locally by the Gatekeeper.",
|
|
3908
|
+
mandateId: pick.authorizationId,
|
|
3909
|
+
field: pick.field,
|
|
3910
|
+
value: asValueOrString(pick.actual),
|
|
3911
|
+
limit: asValueOrString(pick.bound),
|
|
3912
|
+
who: "gateway",
|
|
3913
|
+
code: pick.code
|
|
3914
|
+
});
|
|
3915
|
+
}
|
|
3916
|
+
function recordAsRefusal(state2, tool, authorizationId, err) {
|
|
3917
|
+
const code = typeof err?.code === "string" ? err.code : void 0;
|
|
3918
|
+
const asValueOrString = (v) => typeof v === "number" || typeof v === "string" ? v : void 0;
|
|
3919
|
+
recordBlocked(state2, tool, {
|
|
3920
|
+
kind: code === "CUMULATIVE_LIMIT_EXCEEDED" ? "cumulative" : code === "BOUND_EXCEEDED" ? "bound" : "not_authorized",
|
|
3921
|
+
detail: "Refused by the Authority Server.",
|
|
3922
|
+
mandateId: authorizationId,
|
|
3923
|
+
field: typeof err?.field === "string" ? err.field : void 0,
|
|
3924
|
+
value: asValueOrString(err?.actual),
|
|
3925
|
+
limit: asValueOrString(err?.expected),
|
|
3926
|
+
who: "authority-server",
|
|
3927
|
+
code
|
|
3928
|
+
});
|
|
3929
|
+
}
|
|
3574
3930
|
function createGatedToolHandler(tool, integrationManager2, state2) {
|
|
3575
3931
|
if (isSimulationMode() && !integrationManager2.isSimulationSafe(tool.integrationId)) {
|
|
3576
|
-
return async () =>
|
|
3577
|
-
|
|
3578
|
-
|
|
3579
|
-
|
|
3580
|
-
|
|
3581
|
-
|
|
3582
|
-
|
|
3932
|
+
return async () => {
|
|
3933
|
+
recordBlocked(state2, tool, {
|
|
3934
|
+
kind: "simulation",
|
|
3935
|
+
detail: "Real systems are blocked while simulation mode is on.",
|
|
3936
|
+
who: "gateway",
|
|
3937
|
+
code: "SIMULATION_MODE"
|
|
3938
|
+
});
|
|
3939
|
+
return {
|
|
3940
|
+
content: [{
|
|
3941
|
+
type: "text",
|
|
3942
|
+
text: `Refused: "${tool.namespacedName}" is not available. No ticket was requested.`
|
|
3943
|
+
}],
|
|
3944
|
+
isError: true
|
|
3945
|
+
};
|
|
3946
|
+
};
|
|
3583
3947
|
}
|
|
3584
3948
|
const gated = createGatedToolHandlerInner(tool, integrationManager2, state2);
|
|
3585
3949
|
const gateable = !!tool.gating?.profile && tool.gating.category !== "disabled";
|
|
@@ -3642,6 +4006,15 @@ function createGatedToolHandlerInner(tool, integrationManager2, state2) {
|
|
|
3642
4006
|
};
|
|
3643
4007
|
const noEligibleMandate = (refusals) => {
|
|
3644
4008
|
const reasons = [...new Set(refusals)].join(" ");
|
|
4009
|
+
recordBlocked(state2, tool, {
|
|
4010
|
+
kind: "not_authorized",
|
|
4011
|
+
// Fixed text only: `reasons` can name resources or correspondents (read
|
|
4012
|
+
// refusals), and a blocked-action record carries no content.
|
|
4013
|
+
detail: `No active authorization matching profile "${profile}".`,
|
|
4014
|
+
field: profile,
|
|
4015
|
+
who: "gateway",
|
|
4016
|
+
code: "NO_MATCHING_MANDATE"
|
|
4017
|
+
});
|
|
3645
4018
|
return {
|
|
3646
4019
|
content: [{
|
|
3647
4020
|
type: "text",
|
|
@@ -3847,6 +4220,13 @@ function createGatedToolHandlerInner(tool, integrationManager2, state2) {
|
|
|
3847
4220
|
}
|
|
3848
4221
|
const declaredActionType = typeof execution.action_type === "string" && execution.action_type.length > 0 ? execution.action_type : void 0;
|
|
3849
4222
|
if (!declaredActionType) {
|
|
4223
|
+
recordBlocked(state2, tool, {
|
|
4224
|
+
kind: "bound",
|
|
4225
|
+
detail: `${tool.namespacedName} declares no action_type in its manifest's staticExecution.`,
|
|
4226
|
+
field: "action_type",
|
|
4227
|
+
who: "gateway",
|
|
4228
|
+
code: "MISSING_ACTION_TYPE"
|
|
4229
|
+
});
|
|
3850
4230
|
return {
|
|
3851
4231
|
content: [{
|
|
3852
4232
|
type: "text",
|
|
@@ -3859,6 +4239,14 @@ function createGatedToolHandlerInner(tool, integrationManager2, state2) {
|
|
|
3859
4239
|
const profileDef = getProfile2(profile);
|
|
3860
4240
|
const registry = profileDef?.boundsSchema?.actionTypes;
|
|
3861
4241
|
if (Array.isArray(registry) && registry.length > 0 && !registry.includes(declaredActionType)) {
|
|
4242
|
+
recordBlocked(state2, tool, {
|
|
4243
|
+
kind: "bound",
|
|
4244
|
+
detail: `action_type "${declaredActionType}" is not in profile ${profile}'s actionTypes registry.`,
|
|
4245
|
+
field: "action_type",
|
|
4246
|
+
value: declaredActionType,
|
|
4247
|
+
who: "gateway",
|
|
4248
|
+
code: "INVALID_ACTION_TYPE"
|
|
4249
|
+
});
|
|
3862
4250
|
return {
|
|
3863
4251
|
content: [{
|
|
3864
4252
|
type: "text",
|
|
@@ -3872,6 +4260,7 @@ function createGatedToolHandlerInner(tool, integrationManager2, state2) {
|
|
|
3872
4260
|
if (matchingAuths.length === 0) return noEligibleMandate(refusals);
|
|
3873
4261
|
const errors = [];
|
|
3874
4262
|
const passers = [];
|
|
4263
|
+
const structuredRefusals = [];
|
|
3875
4264
|
for (const candidate of matchingAuths) {
|
|
3876
4265
|
const { result } = await state2.gatekeeper.verifyExecution(candidate.authorizationId, execution, {
|
|
3877
4266
|
bounds: candidate.bounds,
|
|
@@ -3880,6 +4269,13 @@ function createGatedToolHandlerInner(tool, integrationManager2, state2) {
|
|
|
3880
4269
|
const capped = result.approved ? zeroCappedBound(candidate.profileId, candidate.bounds ?? candidate.frame, typeof execution.action_type === "string" ? execution.action_type : void 0) : null;
|
|
3881
4270
|
if (result.approved && capped) {
|
|
3882
4271
|
errors.push(`${candidate.path}: ${capped} is 0 \u2014 this mandate does not allow "${execution.action_type}"`);
|
|
4272
|
+
structuredRefusals.push({
|
|
4273
|
+
authorizationId: candidate.authorizationId,
|
|
4274
|
+
profileId: candidate.profileId,
|
|
4275
|
+
code: "BOUND_EXCEEDED",
|
|
4276
|
+
field: typeof execution.action_type === "string" ? execution.action_type : void 0,
|
|
4277
|
+
bound: 0
|
|
4278
|
+
});
|
|
3883
4279
|
} else if (result.approved) {
|
|
3884
4280
|
passers.push(candidate);
|
|
3885
4281
|
} else {
|
|
@@ -3890,9 +4286,20 @@ function createGatedToolHandlerInner(tool, integrationManager2, state2) {
|
|
|
3890
4286
|
return `${candidate.path}: ${e.message}`;
|
|
3891
4287
|
});
|
|
3892
4288
|
errors.push(...reasons);
|
|
4289
|
+
for (const e of result.errors) {
|
|
4290
|
+
structuredRefusals.push({
|
|
4291
|
+
authorizationId: candidate.authorizationId,
|
|
4292
|
+
profileId: candidate.profileId,
|
|
4293
|
+
code: e.code,
|
|
4294
|
+
field: e.field,
|
|
4295
|
+
bound: e.bound,
|
|
4296
|
+
actual: e.actual
|
|
4297
|
+
});
|
|
4298
|
+
}
|
|
3893
4299
|
}
|
|
3894
4300
|
}
|
|
3895
4301
|
if (passers.length === 0) {
|
|
4302
|
+
recordGatekeeperRefusal(state2, tool, structuredRefusals);
|
|
3896
4303
|
return {
|
|
3897
4304
|
content: [{
|
|
3898
4305
|
type: "text",
|
|
@@ -3943,13 +4350,20 @@ function createGatedToolHandlerInner(tool, integrationManager2, state2) {
|
|
|
3943
4350
|
toolArgs: enrichedArgs,
|
|
3944
4351
|
executionContext: wireExecCtx
|
|
3945
4352
|
});
|
|
4353
|
+
const previewHash = await computeSubmissionPreviewHash(
|
|
4354
|
+
integrationManager2,
|
|
4355
|
+
tool.integrationId,
|
|
4356
|
+
tool.originalName,
|
|
4357
|
+
enrichedArgs
|
|
4358
|
+
);
|
|
3946
4359
|
state2.proposalSubmissions.record({
|
|
3947
4360
|
proposalId: proposal.id,
|
|
3948
4361
|
tool: tool.namespacedName,
|
|
3949
4362
|
toolArgs: enrichedArgs,
|
|
3950
4363
|
executionContext: wireExecCtx,
|
|
3951
4364
|
authorizationId: authzId,
|
|
3952
|
-
profileId: auth.profileId
|
|
4365
|
+
profileId: auth.profileId,
|
|
4366
|
+
previewHash
|
|
3953
4367
|
});
|
|
3954
4368
|
return {
|
|
3955
4369
|
content: [{
|
|
@@ -4044,6 +4458,7 @@ Proposal ID: ${proposal.id}. Check status with check-pending-commitments(proposa
|
|
|
4044
4458
|
state2.cache.markNeedsReapproval(auth.authorizationId);
|
|
4045
4459
|
const remaining = candidates.filter((c) => c.authorizationId !== auth.authorizationId);
|
|
4046
4460
|
if (remaining.length === 0) {
|
|
4461
|
+
recordAsRefusal(state2, tool, auth.authorizationId, { code: "VERSION_UNSUPPORTED" });
|
|
4047
4462
|
return {
|
|
4048
4463
|
content: [{
|
|
4049
4464
|
type: "text",
|
|
@@ -4059,9 +4474,11 @@ Proposal ID: ${proposal.id}. Check status with check-pending-commitments(proposa
|
|
|
4059
4474
|
}
|
|
4060
4475
|
if (err instanceof SPReceiptError && isStaleMandateRefusal(err)) {
|
|
4061
4476
|
state2.cache.invalidate(auth.authorizationId);
|
|
4062
|
-
const
|
|
4477
|
+
const staleErr = err.body?.errors?.[0];
|
|
4478
|
+
const code = staleErr?.code ?? "unknown";
|
|
4063
4479
|
const remaining = candidates.filter((c) => c.authorizationId !== auth.authorizationId);
|
|
4064
4480
|
if (remaining.length === 0) {
|
|
4481
|
+
recordAsRefusal(state2, tool, auth.authorizationId, staleErr);
|
|
4065
4482
|
console.error(
|
|
4066
4483
|
`[Suveren MCP] fallback(${tool.namespacedName}): mandate ${auth.authorizationId} invalid (${code}) \u2014 no remaining candidates`
|
|
4067
4484
|
);
|
|
@@ -4112,13 +4529,20 @@ Proposal ID: ${proposal.id}. Check status with check-pending-commitments(proposa
|
|
|
4112
4529
|
executionContext: wireExecCtx,
|
|
4113
4530
|
pendingApprovers: uniqueApprovers
|
|
4114
4531
|
});
|
|
4532
|
+
const previewHash = await computeSubmissionPreviewHash(
|
|
4533
|
+
integrationManager2,
|
|
4534
|
+
tool.integrationId,
|
|
4535
|
+
tool.originalName,
|
|
4536
|
+
enrichedArgs
|
|
4537
|
+
);
|
|
4115
4538
|
state2.proposalSubmissions.record({
|
|
4116
4539
|
proposalId: proposal.id,
|
|
4117
4540
|
tool: tool.namespacedName,
|
|
4118
4541
|
toolArgs: enrichedArgs,
|
|
4119
4542
|
executionContext: wireExecCtx,
|
|
4120
4543
|
authorizationId: authzId,
|
|
4121
|
-
profileId: auth.profileId
|
|
4544
|
+
profileId: auth.profileId,
|
|
4545
|
+
previewHash
|
|
4122
4546
|
});
|
|
4123
4547
|
return {
|
|
4124
4548
|
content: [{
|
|
@@ -4141,6 +4565,7 @@ Proposal ID: ${proposal.id}. Use check-pending-commitments to track status.`
|
|
|
4141
4565
|
}
|
|
4142
4566
|
}
|
|
4143
4567
|
if (err instanceof SPReceiptError && err.statusCode === 422) {
|
|
4568
|
+
recordAsRefusal(state2, tool, auth.authorizationId, err.body?.errors?.[0]);
|
|
4144
4569
|
return {
|
|
4145
4570
|
content: [{ type: "text", text: `Action blocked: ${err.message} (hard team ceiling \u2014 contact the team admin)` }],
|
|
4146
4571
|
isError: true
|
|
@@ -4151,6 +4576,7 @@ Proposal ID: ${proposal.id}. Use check-pending-commitments to track status.`
|
|
|
4151
4576
|
if (errors403?.[0]?.code === "MANDATE_REVOKED") {
|
|
4152
4577
|
state2.cache.invalidate(auth.authorizationId);
|
|
4153
4578
|
}
|
|
4579
|
+
recordAsRefusal(state2, tool, auth.authorizationId, errors403?.[0]);
|
|
4154
4580
|
return {
|
|
4155
4581
|
content: [{ type: "text", text: `Blocked by SP: ${err.message}` }],
|
|
4156
4582
|
isError: true
|
|
@@ -4798,6 +5224,8 @@ async function executeCommitted(proposal, state2, integrationManager2) {
|
|
|
4798
5224
|
if (err instanceof SPReceiptError) {
|
|
4799
5225
|
const code = err.body.errors?.[0]?.code;
|
|
4800
5226
|
if (code === "PROPOSAL_ALREADY_EXECUTED") {
|
|
5227
|
+
const local = localOutcomeOfExecuted(proposal, state2);
|
|
5228
|
+
if (local) return local;
|
|
4801
5229
|
return {
|
|
4802
5230
|
text: `Proposal ${proposal.id} has already been executed by another request.`
|
|
4803
5231
|
};
|
|
@@ -4849,6 +5277,22 @@ async function executeCommitted(proposal, state2, integrationManager2) {
|
|
|
4849
5277
|
`[Suveren MCP] Proposal ${proposal.id}: ticket ${receiptId} was replayed by the AS with no local execution record \u2014 executing once (lost-response recovery).`
|
|
4850
5278
|
);
|
|
4851
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
|
+
}
|
|
4852
5296
|
try {
|
|
4853
5297
|
let outgoingArgs = proposal.toolArgs;
|
|
4854
5298
|
if (discovered && receiptId) {
|
|
@@ -4865,6 +5309,15 @@ async function executeCommitted(proposal, state2, integrationManager2) {
|
|
|
4865
5309
|
state2.executionJournal.complete(receiptId, "failed");
|
|
4866
5310
|
throw err;
|
|
4867
5311
|
}
|
|
5312
|
+
const resultText = result.content?.[0]?.text ?? JSON.stringify(result);
|
|
5313
|
+
if (result.isError) {
|
|
5314
|
+
state2.executionJournal.complete(receiptId, "failed", "refused", resultText);
|
|
5315
|
+
return {
|
|
5316
|
+
text: `Proposal ${proposal.id} was approved, but ${integrationId} refused to run it \u2014 nothing was done.
|
|
5317
|
+
Reason: ${resultText}`,
|
|
5318
|
+
isError: true
|
|
5319
|
+
};
|
|
5320
|
+
}
|
|
4868
5321
|
state2.executionJournal.complete(receiptId, "done");
|
|
4869
5322
|
state2.executionLog.record({
|
|
4870
5323
|
profileId: proposal.profileId,
|
|
@@ -4872,7 +5325,6 @@ async function executeCommitted(proposal, state2, integrationManager2) {
|
|
|
4872
5325
|
execution: proposal.executionContext,
|
|
4873
5326
|
timestamp: Math.floor(Date.now() / 1e3)
|
|
4874
5327
|
});
|
|
4875
|
-
const resultText = result.content?.[0]?.text ?? JSON.stringify(result);
|
|
4876
5328
|
return { text: `Proposal ${proposal.id} committed and executed.
|
|
4877
5329
|
Result: ${resultText}` };
|
|
4878
5330
|
} catch (err) {
|
|
@@ -4883,6 +5335,21 @@ Result: ${resultText}` };
|
|
|
4883
5335
|
};
|
|
4884
5336
|
}
|
|
4885
5337
|
}
|
|
5338
|
+
function localOutcomeOfExecuted(proposal, state2) {
|
|
5339
|
+
const entry = state2.executionJournal?.findByProposal?.(proposal.id);
|
|
5340
|
+
if (!entry || entry.state === "done") return null;
|
|
5341
|
+
const integration = proposal.tool.split("__")[0];
|
|
5342
|
+
if (entry.state === "failed") {
|
|
5343
|
+
return {
|
|
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.`,
|
|
5345
|
+
isError: true
|
|
5346
|
+
};
|
|
5347
|
+
}
|
|
5348
|
+
return {
|
|
5349
|
+
text: `Proposal ${proposal.id} was approved and started running; the outcome is not known yet. Do not call the tool again.`,
|
|
5350
|
+
isError: false
|
|
5351
|
+
};
|
|
5352
|
+
}
|
|
4886
5353
|
function checkPendingCommitmentsHandler(state2, integrationManager2) {
|
|
4887
5354
|
return async (args) => {
|
|
4888
5355
|
if (!state2.spClient.isUnlocked()) {
|
|
@@ -4907,6 +5374,13 @@ function checkPendingCommitmentsHandler(state2, integrationManager2) {
|
|
|
4907
5374
|
};
|
|
4908
5375
|
}
|
|
4909
5376
|
if (match.status === "executed") {
|
|
5377
|
+
const local = localOutcomeOfExecuted(match, state2);
|
|
5378
|
+
if (local) {
|
|
5379
|
+
return {
|
|
5380
|
+
content: [{ type: "text", text: local.text }],
|
|
5381
|
+
...local.isError ? { isError: true } : {}
|
|
5382
|
+
};
|
|
5383
|
+
}
|
|
4910
5384
|
const result = match.executionResult ? `
|
|
4911
5385
|
Result: ${JSON.stringify(match.executionResult, null, 2)}` : " The action ran; the gateway did not retain its output.";
|
|
4912
5386
|
return {
|
|
@@ -5172,21 +5646,21 @@ function createMcpServer(state2, integrationManager2) {
|
|
|
5172
5646
|
}
|
|
5173
5647
|
|
|
5174
5648
|
// src/lib/agent-contact.ts
|
|
5175
|
-
import { existsSync as
|
|
5176
|
-
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";
|
|
5177
5651
|
var FILE = "agent-contact.json";
|
|
5178
5652
|
var REWRITE_AFTER_MS = 6e4;
|
|
5179
5653
|
var AgentContactStore = class {
|
|
5180
5654
|
path;
|
|
5181
5655
|
current;
|
|
5182
5656
|
constructor(dataDir2) {
|
|
5183
|
-
this.path =
|
|
5657
|
+
this.path = join14(dataDir2, FILE);
|
|
5184
5658
|
this.current = this.load();
|
|
5185
5659
|
}
|
|
5186
5660
|
load() {
|
|
5187
|
-
if (!
|
|
5661
|
+
if (!existsSync14(this.path)) return null;
|
|
5188
5662
|
try {
|
|
5189
|
-
const v = JSON.parse(
|
|
5663
|
+
const v = JSON.parse(readFileSync15(this.path, "utf8"));
|
|
5190
5664
|
if (typeof v.clientName !== "string" || typeof v.firstSeenAt !== "string" || typeof v.lastSeenAt !== "string") return null;
|
|
5191
5665
|
return { clientName: v.clientName, clientVersion: v.clientVersion, firstSeenAt: v.firstSeenAt, lastSeenAt: v.lastSeenAt };
|
|
5192
5666
|
} catch {
|
|
@@ -5209,10 +5683,10 @@ var AgentContactStore = class {
|
|
|
5209
5683
|
};
|
|
5210
5684
|
if (!due) return;
|
|
5211
5685
|
try {
|
|
5212
|
-
|
|
5686
|
+
mkdirSync11(dirname11(this.path), { recursive: true });
|
|
5213
5687
|
const tmp = `${this.path}.tmp`;
|
|
5214
|
-
|
|
5215
|
-
|
|
5688
|
+
writeFileSync11(tmp, JSON.stringify(this.current, null, 2));
|
|
5689
|
+
renameSync5(tmp, this.path);
|
|
5216
5690
|
} catch (err) {
|
|
5217
5691
|
console.error("[Suveren MCP] Could not save agent contact:", err);
|
|
5218
5692
|
}
|
|
@@ -5246,16 +5720,16 @@ function verifyGateContentHashes(content, auth) {
|
|
|
5246
5720
|
}
|
|
5247
5721
|
|
|
5248
5722
|
// src/lib/integration-registry.ts
|
|
5249
|
-
import { mkdirSync as
|
|
5250
|
-
import { homedir as
|
|
5251
|
-
import { dirname as
|
|
5252
|
-
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");
|
|
5253
5727
|
var IntegrationRegistry = class {
|
|
5254
5728
|
integrations = /* @__PURE__ */ new Map();
|
|
5255
5729
|
filePath;
|
|
5256
5730
|
constructor(dataDir2) {
|
|
5257
5731
|
const dir = dataDir2 ?? DEFAULT_DATA_DIR;
|
|
5258
|
-
this.filePath =
|
|
5732
|
+
this.filePath = join15(dir, "integrations.json");
|
|
5259
5733
|
this.load();
|
|
5260
5734
|
}
|
|
5261
5735
|
// ─── CRUD ───────────────────────────────────────────────────────────────
|
|
@@ -5286,12 +5760,12 @@ var IntegrationRegistry = class {
|
|
|
5286
5760
|
}
|
|
5287
5761
|
// ─── Load / Save ────────────────────────────────────────────────────────
|
|
5288
5762
|
load() {
|
|
5289
|
-
if (!
|
|
5290
|
-
|
|
5763
|
+
if (!existsSync15(this.filePath)) {
|
|
5764
|
+
mkdirSync12(dirname12(this.filePath), { recursive: true });
|
|
5291
5765
|
return;
|
|
5292
5766
|
}
|
|
5293
5767
|
try {
|
|
5294
|
-
const raw =
|
|
5768
|
+
const raw = readFileSync16(this.filePath, "utf-8");
|
|
5295
5769
|
const data = JSON.parse(raw);
|
|
5296
5770
|
const migrated = data.integrations.map((i) => {
|
|
5297
5771
|
if ("toolGating" in i && !("profile" in i)) {
|
|
@@ -5320,79 +5794,19 @@ var IntegrationRegistry = class {
|
|
|
5320
5794
|
version: 1,
|
|
5321
5795
|
integrations: Array.from(this.integrations.values())
|
|
5322
5796
|
};
|
|
5323
|
-
|
|
5324
|
-
|
|
5797
|
+
mkdirSync12(dirname12(this.filePath), { recursive: true });
|
|
5798
|
+
writeFileSync12(this.filePath, JSON.stringify(data, null, 2), { encoding: "utf-8" });
|
|
5325
5799
|
}
|
|
5326
5800
|
};
|
|
5327
5801
|
|
|
5328
5802
|
// src/lib/integration-manager.ts
|
|
5329
|
-
import { homedir as
|
|
5330
|
-
import { join as
|
|
5331
|
-
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";
|
|
5332
5806
|
import { execFile } from "child_process";
|
|
5333
5807
|
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
5334
5808
|
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
|
|
5335
5809
|
|
|
5336
|
-
// src/lib/manifest-loader.ts
|
|
5337
|
-
import { readFileSync as readFileSync15, existsSync as existsSync14 } from "fs";
|
|
5338
|
-
import { join as join14, resolve } from "path";
|
|
5339
|
-
var EXACT_SEMVER_RE = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/;
|
|
5340
|
-
function isExactSemver(value) {
|
|
5341
|
-
return typeof value === "string" && EXACT_SEMVER_RE.test(value);
|
|
5342
|
-
}
|
|
5343
|
-
function invalidNpmPinReason(manifest) {
|
|
5344
|
-
if (!manifest.npmPackage) return null;
|
|
5345
|
-
if (manifest.npmVersion === void 0) {
|
|
5346
|
-
return `npmPackage "${manifest.npmPackage}" is set but npmVersion is missing \u2014 every connector manifest must pin an exact tested version`;
|
|
5347
|
-
}
|
|
5348
|
-
if (!isExactSemver(manifest.npmVersion)) {
|
|
5349
|
-
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`;
|
|
5350
|
-
}
|
|
5351
|
-
return null;
|
|
5352
|
-
}
|
|
5353
|
-
var manifests = /* @__PURE__ */ new Map();
|
|
5354
|
-
function loadManifests(integrationsDir) {
|
|
5355
|
-
const dir = resolve(
|
|
5356
|
-
integrationsDir ?? process.env.SUVEREN_MANIFESTS_DIR ?? join14(import.meta.dirname ?? __dirname, "..", "..", "..", "..", "content", "integrations")
|
|
5357
|
-
);
|
|
5358
|
-
const indexPath = join14(dir, "index.json");
|
|
5359
|
-
if (!existsSync14(indexPath)) {
|
|
5360
|
-
console.error(`[ManifestLoader] No index.json found at ${indexPath}, skipping manifest loading`);
|
|
5361
|
-
return 0;
|
|
5362
|
-
}
|
|
5363
|
-
let index;
|
|
5364
|
-
try {
|
|
5365
|
-
index = JSON.parse(readFileSync15(indexPath, "utf-8"));
|
|
5366
|
-
} catch (err) {
|
|
5367
|
-
console.error(`[ManifestLoader] Failed to parse ${indexPath}:`, err);
|
|
5368
|
-
return 0;
|
|
5369
|
-
}
|
|
5370
|
-
let loaded = 0;
|
|
5371
|
-
for (const [id, relativePath] of Object.entries(index.integrations)) {
|
|
5372
|
-
const manifestPath = join14(dir, relativePath);
|
|
5373
|
-
try {
|
|
5374
|
-
const manifest = JSON.parse(readFileSync15(manifestPath, "utf-8"));
|
|
5375
|
-
const pinReason = invalidNpmPinReason(manifest);
|
|
5376
|
-
if (pinReason) {
|
|
5377
|
-
console.error(`[ManifestLoader] Refusing manifest ${id} (${manifestPath}): ${pinReason}`);
|
|
5378
|
-
continue;
|
|
5379
|
-
}
|
|
5380
|
-
manifests.set(id, manifest);
|
|
5381
|
-
loaded++;
|
|
5382
|
-
} catch (err) {
|
|
5383
|
-
console.error(`[ManifestLoader] Failed to load manifest ${id} from ${manifestPath}:`, err);
|
|
5384
|
-
}
|
|
5385
|
-
}
|
|
5386
|
-
console.error(`[ManifestLoader] Loaded ${loaded} integration manifest(s) from ${dir}`);
|
|
5387
|
-
return loaded;
|
|
5388
|
-
}
|
|
5389
|
-
function getManifest(id) {
|
|
5390
|
-
return manifests.get(id);
|
|
5391
|
-
}
|
|
5392
|
-
function getAllManifests() {
|
|
5393
|
-
return Array.from(manifests.values());
|
|
5394
|
-
}
|
|
5395
|
-
|
|
5396
5810
|
// src/lib/remote-auth-preflight.ts
|
|
5397
5811
|
var AUTH_HEADER_RE = /^authorization:\s*(.+)$/i;
|
|
5398
5812
|
function remotePreflightTarget(command, args) {
|
|
@@ -5518,8 +5932,8 @@ function inheritedConnectorEnv(parent, passEnv = []) {
|
|
|
5518
5932
|
}
|
|
5519
5933
|
|
|
5520
5934
|
// src/lib/integration-manager.ts
|
|
5521
|
-
var DEFAULT_DATA_DIR2 = process.env.SUVEREN_DATA_DIR ??
|
|
5522
|
-
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");
|
|
5523
5937
|
var OFFLINE_MODE = process.env.SUVEREN_OFFLINE === "1";
|
|
5524
5938
|
var installLock = Promise.resolve();
|
|
5525
5939
|
function withInstallLock(task) {
|
|
@@ -5527,7 +5941,7 @@ function withInstallLock(task) {
|
|
|
5527
5941
|
installLock = run.then(() => void 0, () => void 0);
|
|
5528
5942
|
return run;
|
|
5529
5943
|
}
|
|
5530
|
-
var INTEGRATIONS_BIN =
|
|
5944
|
+
var INTEGRATIONS_BIN = join16(INTEGRATIONS_DIR, "node_modules", ".bin");
|
|
5531
5945
|
function getIntegrationsBinDir() {
|
|
5532
5946
|
return INTEGRATIONS_BIN;
|
|
5533
5947
|
}
|
|
@@ -5536,12 +5950,12 @@ function buildPath() {
|
|
|
5536
5950
|
return [INTEGRATIONS_BIN, base].join(delimiter);
|
|
5537
5951
|
}
|
|
5538
5952
|
function ensureIntegrationsDir() {
|
|
5539
|
-
if (!
|
|
5540
|
-
|
|
5953
|
+
if (!existsSync16(INTEGRATIONS_DIR)) {
|
|
5954
|
+
mkdirSync13(INTEGRATIONS_DIR, { recursive: true });
|
|
5541
5955
|
}
|
|
5542
|
-
const pkgPath =
|
|
5543
|
-
if (!
|
|
5544
|
-
|
|
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));
|
|
5545
5959
|
}
|
|
5546
5960
|
}
|
|
5547
5961
|
function readAgeOf(config) {
|
|
@@ -5630,19 +6044,19 @@ var IntegrationManager = class {
|
|
|
5630
6044
|
* exist, and treat anything else as not-installed (and reinstallable).
|
|
5631
6045
|
*/
|
|
5632
6046
|
isUsableInstall(npmPackage) {
|
|
5633
|
-
const pkgDir =
|
|
5634
|
-
const pkgJson =
|
|
5635
|
-
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;
|
|
5636
6050
|
try {
|
|
5637
|
-
const pkg = JSON.parse(
|
|
6051
|
+
const pkg = JSON.parse(readFileSync17(pkgJson, "utf8"));
|
|
5638
6052
|
const defaultBinName = (pkg.name ?? npmPackage).split("/").pop() ?? "";
|
|
5639
6053
|
const binMap = typeof pkg.bin === "string" ? { [defaultBinName]: pkg.bin } : pkg.bin ?? {};
|
|
5640
6054
|
const binEntries = Object.entries(binMap);
|
|
5641
6055
|
for (const [binName, target] of binEntries) {
|
|
5642
|
-
if (!
|
|
6056
|
+
if (!existsSync16(join16(pkgDir, target))) return false;
|
|
5643
6057
|
if (!this.binShimExists(binName)) return false;
|
|
5644
6058
|
}
|
|
5645
|
-
if (binEntries.length === 0 && pkg.main && !
|
|
6059
|
+
if (binEntries.length === 0 && pkg.main && !existsSync16(join16(pkgDir, pkg.main))) return false;
|
|
5646
6060
|
return true;
|
|
5647
6061
|
} catch {
|
|
5648
6062
|
return false;
|
|
@@ -5657,9 +6071,9 @@ var IntegrationManager = class {
|
|
|
5657
6071
|
*/
|
|
5658
6072
|
readInstalledVersion(npmPackage) {
|
|
5659
6073
|
if (!this.isUsableInstall(npmPackage)) return null;
|
|
5660
|
-
const pkgJson =
|
|
6074
|
+
const pkgJson = join16(INTEGRATIONS_DIR, "node_modules", ...npmPackage.split("/"), "package.json");
|
|
5661
6075
|
try {
|
|
5662
|
-
const pkg = JSON.parse(
|
|
6076
|
+
const pkg = JSON.parse(readFileSync17(pkgJson, "utf8"));
|
|
5663
6077
|
return typeof pkg.version === "string" ? pkg.version : null;
|
|
5664
6078
|
} catch {
|
|
5665
6079
|
return null;
|
|
@@ -5673,7 +6087,7 @@ var IntegrationManager = class {
|
|
|
5673
6087
|
binShimExists(binName) {
|
|
5674
6088
|
if (!binName) return false;
|
|
5675
6089
|
const candidates = process.platform === "win32" ? [binName, `${binName}.cmd`, `${binName}.CMD`, `${binName}.ps1`] : [binName];
|
|
5676
|
-
return candidates.some((c) =>
|
|
6090
|
+
return candidates.some((c) => existsSync16(join16(INTEGRATIONS_BIN, c)));
|
|
5677
6091
|
}
|
|
5678
6092
|
/**
|
|
5679
6093
|
* The exact version THIS gateway's CURRENTLY LOADED manifest pins
|
|
@@ -5763,8 +6177,8 @@ var IntegrationManager = class {
|
|
|
5763
6177
|
* nothing at all.
|
|
5764
6178
|
*/
|
|
5765
6179
|
async installNow(npmPackage, exactVersion, cleanFirst = true) {
|
|
5766
|
-
const pkgDir =
|
|
5767
|
-
if (cleanFirst &&
|
|
6180
|
+
const pkgDir = join16(INTEGRATIONS_DIR, "node_modules", ...npmPackage.split("/"));
|
|
6181
|
+
if (cleanFirst && existsSync16(pkgDir)) {
|
|
5768
6182
|
console.error(`[IntegrationManager] Removing ${exactVersion ? "outdated" : "unusable"} install of ${npmPackage}`);
|
|
5769
6183
|
try {
|
|
5770
6184
|
rmSync(pkgDir, { recursive: true, force: true });
|
|
@@ -5957,6 +6371,7 @@ var IntegrationManager = class {
|
|
|
5957
6371
|
tool.inputSchema ?? {},
|
|
5958
6372
|
gating?.blockedArgs
|
|
5959
6373
|
),
|
|
6374
|
+
outputSchema: tool.outputSchema,
|
|
5960
6375
|
gating
|
|
5961
6376
|
};
|
|
5962
6377
|
});
|
|
@@ -6121,7 +6536,11 @@ var IntegrationManager = class {
|
|
|
6121
6536
|
type: item.type,
|
|
6122
6537
|
text: item.text ?? JSON.stringify(item)
|
|
6123
6538
|
}));
|
|
6124
|
-
return {
|
|
6539
|
+
return {
|
|
6540
|
+
content,
|
|
6541
|
+
isError: result.isError,
|
|
6542
|
+
structuredContent: result.structuredContent
|
|
6543
|
+
};
|
|
6125
6544
|
} catch (err) {
|
|
6126
6545
|
const message = err instanceof Error ? err.message : String(err);
|
|
6127
6546
|
return {
|
|
@@ -6670,28 +7089,28 @@ function agentAccess(token, opensSession) {
|
|
|
6670
7089
|
}
|
|
6671
7090
|
|
|
6672
7091
|
// src/lib/profile-loader.ts
|
|
6673
|
-
import { readFileSync as
|
|
6674
|
-
import { join as
|
|
7092
|
+
import { readFileSync as readFileSync18, existsSync as existsSync17 } from "fs";
|
|
7093
|
+
import { join as join17, resolve as resolve2 } from "path";
|
|
6675
7094
|
import { registerProfile } from "@hap/core";
|
|
6676
7095
|
function loadProfiles(profilesDir) {
|
|
6677
|
-
const dir = resolve2(profilesDir ?? process.env.SUVEREN_PROFILES_DIR ??
|
|
6678
|
-
const indexPath =
|
|
6679
|
-
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)) {
|
|
6680
7099
|
warnNoProfiles(`No index.json at ${indexPath}`);
|
|
6681
7100
|
return 0;
|
|
6682
7101
|
}
|
|
6683
7102
|
let index;
|
|
6684
7103
|
try {
|
|
6685
|
-
index = JSON.parse(
|
|
7104
|
+
index = JSON.parse(readFileSync18(indexPath, "utf-8"));
|
|
6686
7105
|
} catch (err) {
|
|
6687
7106
|
console.error(`[ProfileLoader] Failed to parse ${indexPath}:`, err);
|
|
6688
7107
|
return 0;
|
|
6689
7108
|
}
|
|
6690
7109
|
let loaded = 0;
|
|
6691
7110
|
for (const [profileId, relativePath] of Object.entries(index.profiles)) {
|
|
6692
|
-
const profilePath =
|
|
7111
|
+
const profilePath = join17(dir, relativePath);
|
|
6693
7112
|
try {
|
|
6694
|
-
const profile = JSON.parse(
|
|
7113
|
+
const profile = JSON.parse(readFileSync18(profilePath, "utf-8"));
|
|
6695
7114
|
registerProfile(profileId, profile);
|
|
6696
7115
|
const shortName = profileId.split("/").pop()?.replace(/@.*$/, "");
|
|
6697
7116
|
if (shortName && shortName !== profileId) {
|
|
@@ -7029,20 +7448,20 @@ async function controlPlaneMandate(dryRun, request) {
|
|
|
7029
7448
|
}
|
|
7030
7449
|
|
|
7031
7450
|
// src/lib/guides.ts
|
|
7032
|
-
import { existsSync as
|
|
7033
|
-
import { dirname as
|
|
7451
|
+
import { existsSync as existsSync18, readdirSync, readFileSync as readFileSync19 } from "fs";
|
|
7452
|
+
import { dirname as dirname13, join as join18 } from "path";
|
|
7034
7453
|
var FILE_RE = /^(?:(\d+)-)?([a-z0-9][a-z0-9-]*)\.md$/;
|
|
7035
7454
|
function builtinGuidesDir() {
|
|
7036
7455
|
if (process.env.SUVEREN_BUILTIN_GUIDES_DIR) return process.env.SUVEREN_BUILTIN_GUIDES_DIR;
|
|
7037
7456
|
let dir = import.meta.dirname ?? __dirname;
|
|
7038
7457
|
for (let i = 0; i < 8; i++) {
|
|
7039
|
-
const candidate =
|
|
7040
|
-
if (
|
|
7041
|
-
const parent =
|
|
7458
|
+
const candidate = join18(dir, "content", "guides");
|
|
7459
|
+
if (existsSync18(candidate)) return candidate;
|
|
7460
|
+
const parent = dirname13(dir);
|
|
7042
7461
|
if (parent === dir) break;
|
|
7043
7462
|
dir = parent;
|
|
7044
7463
|
}
|
|
7045
|
-
return
|
|
7464
|
+
return join18(import.meta.dirname ?? __dirname, "content", "guides");
|
|
7046
7465
|
}
|
|
7047
7466
|
function parse(body) {
|
|
7048
7467
|
const lines = body.split(/\r?\n/);
|
|
@@ -7053,14 +7472,14 @@ function parse(body) {
|
|
|
7053
7472
|
return { title, summary };
|
|
7054
7473
|
}
|
|
7055
7474
|
function readDir(dir, source) {
|
|
7056
|
-
if (!dir || !
|
|
7475
|
+
if (!dir || !existsSync18(dir)) return [];
|
|
7057
7476
|
const out = [];
|
|
7058
7477
|
for (const file of readdirSync(dir)) {
|
|
7059
7478
|
const m = FILE_RE.exec(file);
|
|
7060
7479
|
if (!m) continue;
|
|
7061
7480
|
let body;
|
|
7062
7481
|
try {
|
|
7063
|
-
body =
|
|
7482
|
+
body = readFileSync19(join18(dir, file), "utf8");
|
|
7064
7483
|
} catch {
|
|
7065
7484
|
continue;
|
|
7066
7485
|
}
|
|
@@ -7370,8 +7789,8 @@ function registerBuiltins(deps, factories = BUILTIN_FACTORIES) {
|
|
|
7370
7789
|
import { decodeMandateBlob as decodeMandateBlob3 } from "@hap/core";
|
|
7371
7790
|
|
|
7372
7791
|
// src/lib/committed-executor.ts
|
|
7373
|
-
import { existsSync as
|
|
7374
|
-
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";
|
|
7375
7794
|
var CommittedExecutor = class {
|
|
7376
7795
|
constructor(run) {
|
|
7377
7796
|
this.run = run;
|
|
@@ -7399,7 +7818,7 @@ var ExecutorLock = class {
|
|
|
7399
7818
|
path;
|
|
7400
7819
|
held = false;
|
|
7401
7820
|
constructor(dataDir2) {
|
|
7402
|
-
this.path =
|
|
7821
|
+
this.path = join19(dataDir2, "executor.lock");
|
|
7403
7822
|
}
|
|
7404
7823
|
/** Try to take the lock. Returns the holder's pid when someone else has it. */
|
|
7405
7824
|
acquire() {
|
|
@@ -7407,9 +7826,9 @@ var ExecutorLock = class {
|
|
|
7407
7826
|
if (current && current.pid !== process.pid && isAlive(current.pid)) {
|
|
7408
7827
|
return { held: false, holderPid: current.pid };
|
|
7409
7828
|
}
|
|
7410
|
-
|
|
7829
|
+
mkdirSync14(dirname14(this.path), { recursive: true, mode: 448 });
|
|
7411
7830
|
const lock = { pid: process.pid, startedAt: Math.floor(Date.now() / 1e3) };
|
|
7412
|
-
|
|
7831
|
+
writeFileSync14(this.path, JSON.stringify(lock), { mode: 384 });
|
|
7413
7832
|
this.held = true;
|
|
7414
7833
|
return { held: true };
|
|
7415
7834
|
}
|
|
@@ -7431,9 +7850,9 @@ var ExecutorLock = class {
|
|
|
7431
7850
|
}
|
|
7432
7851
|
}
|
|
7433
7852
|
read() {
|
|
7434
|
-
if (!
|
|
7853
|
+
if (!existsSync19(this.path)) return null;
|
|
7435
7854
|
try {
|
|
7436
|
-
const parsed = JSON.parse(
|
|
7855
|
+
const parsed = JSON.parse(readFileSync20(this.path, "utf8"));
|
|
7437
7856
|
return typeof parsed?.pid === "number" ? parsed : null;
|
|
7438
7857
|
} catch {
|
|
7439
7858
|
return null;
|
|
@@ -7450,20 +7869,20 @@ function isAlive(pid) {
|
|
|
7450
7869
|
}
|
|
7451
7870
|
|
|
7452
7871
|
// bin/http.ts
|
|
7453
|
-
import { homedir as
|
|
7454
|
-
import { join as
|
|
7872
|
+
import { homedir as homedir12 } from "os";
|
|
7873
|
+
import { join as join21 } from "path";
|
|
7455
7874
|
|
|
7456
7875
|
// src/lib/mcp-as-tracker.ts
|
|
7457
|
-
import { existsSync as
|
|
7458
|
-
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";
|
|
7459
7878
|
function trackerPath(dataDir2) {
|
|
7460
|
-
return
|
|
7879
|
+
return join20(dataDir2, "mcp-as-pairing.json");
|
|
7461
7880
|
}
|
|
7462
7881
|
function readMcpPairedAsUrl(dataDir2) {
|
|
7463
7882
|
const path = trackerPath(dataDir2);
|
|
7464
|
-
if (!
|
|
7883
|
+
if (!existsSync20(path)) return null;
|
|
7465
7884
|
try {
|
|
7466
|
-
const data = JSON.parse(
|
|
7885
|
+
const data = JSON.parse(readFileSync21(path, "utf-8"));
|
|
7467
7886
|
return typeof data.asUrl === "string" ? data.asUrl : null;
|
|
7468
7887
|
} catch {
|
|
7469
7888
|
return null;
|
|
@@ -7471,12 +7890,12 @@ function readMcpPairedAsUrl(dataDir2) {
|
|
|
7471
7890
|
}
|
|
7472
7891
|
function writeMcpPairedAsUrl(dataDir2, asUrl) {
|
|
7473
7892
|
const path = trackerPath(dataDir2);
|
|
7474
|
-
|
|
7475
|
-
|
|
7893
|
+
mkdirSync15(dirname15(path), { recursive: true, mode: 448 });
|
|
7894
|
+
writeFileSync15(path, JSON.stringify({ asUrl }, null, 2), { encoding: "utf-8", mode: 384 });
|
|
7476
7895
|
}
|
|
7477
7896
|
|
|
7478
7897
|
// bin/http.ts
|
|
7479
|
-
var dataDir = process.env.SUVEREN_DATA_DIR ??
|
|
7898
|
+
var dataDir = process.env.SUVEREN_DATA_DIR ?? join21(homedir12(), ".suveren");
|
|
7480
7899
|
var spUrl = resolveAsUrl(dataDir);
|
|
7481
7900
|
validatePinTlsForUrl(resolvePinTls(dataDir), spUrl);
|
|
7482
7901
|
var port = parseInt(process.env.SUVEREN_MCP_PORT ?? "3430", 10);
|
|
@@ -7686,6 +8105,29 @@ app.post("/internal/resync-gates", internalOnly, async (_req, res) => {
|
|
|
7686
8105
|
}
|
|
7687
8106
|
res.json({ ok: true, synced, orphaned });
|
|
7688
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
|
+
});
|
|
7689
8131
|
app.post("/internal/add-integration", internalOnly, async (req, res) => {
|
|
7690
8132
|
try {
|
|
7691
8133
|
const config = req.body;
|
|
@@ -8179,7 +8621,7 @@ app.listen(port, bindHost, () => {
|
|
|
8179
8621
|
(proposal) => executeCommitted(proposal, state, integrationManager)
|
|
8180
8622
|
);
|
|
8181
8623
|
installCommittedExecutor(committedExecutor);
|
|
8182
|
-
const dataDir2 = process.env.SUVEREN_DATA_DIR ??
|
|
8624
|
+
const dataDir2 = process.env.SUVEREN_DATA_DIR ?? join21(homedir12(), ".suveren");
|
|
8183
8625
|
const executorLock = new ExecutorLock(dataDir2);
|
|
8184
8626
|
const lock = executorLock.acquire();
|
|
8185
8627
|
if (!lock.held) {
|