@suveren/gateway 0.8.0 → 0.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/content/integrations/deploy-github.json +10 -1
- package/dist/mcp-server/http.mjs +327 -59
- package/node_modules/hono/dist/cjs/client/client.js +4 -4
- package/node_modules/hono/dist/cjs/hono-base.js +7 -6
- package/node_modules/hono/dist/cjs/jsx/base.js +18 -1
- package/node_modules/hono/dist/cjs/jsx/components.js +25 -26
- package/node_modules/hono/dist/cjs/jsx/context.js +5 -5
- package/node_modules/hono/dist/cjs/jsx/dom/server.js +5 -1
- package/node_modules/hono/dist/cjs/jsx/intrinsic-element/components.js +2 -2
- package/node_modules/hono/dist/cjs/jsx/streaming.js +4 -5
- package/node_modules/hono/dist/client/client.js +4 -4
- package/node_modules/hono/dist/hono-base.js +7 -6
- package/node_modules/hono/dist/jsx/base.js +15 -1
- package/node_modules/hono/dist/jsx/components.js +26 -27
- package/node_modules/hono/dist/jsx/context.js +6 -6
- package/node_modules/hono/dist/jsx/dom/server.js +5 -1
- package/node_modules/hono/dist/jsx/intrinsic-element/components.js +3 -3
- package/node_modules/hono/dist/jsx/streaming.js +5 -6
- package/node_modules/hono/dist/types/context.d.ts +2 -2
- package/node_modules/hono/dist/types/jsx/base.d.ts +3 -0
- package/node_modules/hono/package.json +2 -2
- package/package.json +1 -1
|
@@ -20,10 +20,19 @@
|
|
|
20
20
|
"label": "GitHub token",
|
|
21
21
|
"type": "password",
|
|
22
22
|
"placeholder": "github_pat_..."
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
"key": "artifactPath",
|
|
26
|
+
"label": "Artifact directory (optional)",
|
|
27
|
+
"type": "text",
|
|
28
|
+
"optional": true,
|
|
29
|
+
"placeholder": "website/dist",
|
|
30
|
+
"description": "Repository-relative directory the host serves as-is, if the built artifact is committed. Lets get_deployment report a digest of the served bytes and derive the source commit from git, so a private staged build can still be released."
|
|
23
31
|
}
|
|
24
32
|
],
|
|
25
33
|
"envMapping": {
|
|
26
|
-
"GITHUB_TOKEN": "githubToken"
|
|
34
|
+
"GITHUB_TOKEN": "githubToken",
|
|
35
|
+
"HAP_DEPLOY_ARTIFACT_PATH": "artifactPath"
|
|
27
36
|
}
|
|
28
37
|
},
|
|
29
38
|
"oauth": null,
|
package/dist/mcp-server/http.mjs
CHANGED
|
@@ -254,7 +254,10 @@ var SPClient = class {
|
|
|
254
254
|
const message = first?.message ?? first?.code ?? respBody.error ?? `SP receipt request failed: ${res.status}`;
|
|
255
255
|
throw new SPReceiptError(message, res.status, respBody);
|
|
256
256
|
}
|
|
257
|
-
return {
|
|
257
|
+
return {
|
|
258
|
+
receipt: respBody.receipt,
|
|
259
|
+
idempotent: respBody.idempotent === true
|
|
260
|
+
};
|
|
258
261
|
}
|
|
259
262
|
throw lastError ?? new Error("postReceipt: retries exhausted");
|
|
260
263
|
}
|
|
@@ -1136,6 +1139,93 @@ var ReceiptArchive = class {
|
|
|
1136
1139
|
}
|
|
1137
1140
|
};
|
|
1138
1141
|
|
|
1142
|
+
// src/lib/execution-journal.ts
|
|
1143
|
+
import { mkdirSync as mkdirSync5, readFileSync as readFileSync6, writeFileSync as writeFileSync5, existsSync as existsSync6, renameSync } from "fs";
|
|
1144
|
+
import { homedir as homedir5 } from "os";
|
|
1145
|
+
import { dirname as dirname5, join as join6 } from "path";
|
|
1146
|
+
import { createHash } from "crypto";
|
|
1147
|
+
var DEFAULT_DIR5 = process.env.SUVEREN_DATA_DIR ?? join6(homedir5(), ".suveren");
|
|
1148
|
+
var FILE_NAME = "execution-journal.json";
|
|
1149
|
+
var MAX_AGE_SECONDS2 = 90 * 24 * 60 * 60;
|
|
1150
|
+
function hashToolArgs(args) {
|
|
1151
|
+
return "sha256:" + createHash("sha256").update(stableStringify(args)).digest("hex");
|
|
1152
|
+
}
|
|
1153
|
+
function stableStringify(value) {
|
|
1154
|
+
if (value === null || typeof value !== "object") return JSON.stringify(value) ?? "undefined";
|
|
1155
|
+
if (Array.isArray(value)) return "[" + value.map(stableStringify).join(",") + "]";
|
|
1156
|
+
const obj = value;
|
|
1157
|
+
const keys = Object.keys(obj).sort();
|
|
1158
|
+
return "{" + keys.map((k) => JSON.stringify(k) + ":" + stableStringify(obj[k])).join(",") + "}";
|
|
1159
|
+
}
|
|
1160
|
+
var ExecutionJournal = class {
|
|
1161
|
+
filePath;
|
|
1162
|
+
constructor(filePath) {
|
|
1163
|
+
const baseDir = filePath && filePath.endsWith(".json") ? dirname5(filePath) : filePath ?? DEFAULT_DIR5;
|
|
1164
|
+
this.filePath = join6(baseDir, FILE_NAME);
|
|
1165
|
+
}
|
|
1166
|
+
/** Look a ticket up. Always reads from disk — another process may have written. */
|
|
1167
|
+
get(ticketId) {
|
|
1168
|
+
return this.load().entries.find((e) => e.ticketId === ticketId);
|
|
1169
|
+
}
|
|
1170
|
+
/**
|
|
1171
|
+
* Record intent to execute under `ticketId`. Refuses if any row for that
|
|
1172
|
+
* ticket already exists, whatever its state — deciding what an existing row
|
|
1173
|
+
* means is the caller's job, and it differs by state (see commitments.ts).
|
|
1174
|
+
*/
|
|
1175
|
+
begin(entry) {
|
|
1176
|
+
const file = this.load();
|
|
1177
|
+
const existing = file.entries.find((e) => e.ticketId === entry.ticketId);
|
|
1178
|
+
if (existing) return { ok: false, existing };
|
|
1179
|
+
file.entries.push({
|
|
1180
|
+
...entry,
|
|
1181
|
+
state: "intent",
|
|
1182
|
+
startedAt: nowSec(),
|
|
1183
|
+
pid: process.pid
|
|
1184
|
+
});
|
|
1185
|
+
this.persist(file);
|
|
1186
|
+
return { ok: true };
|
|
1187
|
+
}
|
|
1188
|
+
/** Close an `intent` row. Missing row is a programming error, not a silent no-op. */
|
|
1189
|
+
complete(ticketId, state2) {
|
|
1190
|
+
const file = this.load();
|
|
1191
|
+
const row = file.entries.find((e) => e.ticketId === ticketId);
|
|
1192
|
+
if (!row) throw new Error(`execution journal: no row for ticket ${ticketId} to complete`);
|
|
1193
|
+
row.state = state2;
|
|
1194
|
+
row.finishedAt = nowSec();
|
|
1195
|
+
this.persist(file);
|
|
1196
|
+
}
|
|
1197
|
+
/** All rows, newest last. Diagnostic. */
|
|
1198
|
+
getAll() {
|
|
1199
|
+
return [...this.load().entries];
|
|
1200
|
+
}
|
|
1201
|
+
// ─── Persistence ────────────────────────────────────────────────────────
|
|
1202
|
+
load() {
|
|
1203
|
+
if (!existsSync6(this.filePath)) return { version: 1, entries: [] };
|
|
1204
|
+
try {
|
|
1205
|
+
const parsed = JSON.parse(readFileSync6(this.filePath, "utf8"));
|
|
1206
|
+
if (parsed?.version !== 1 || !Array.isArray(parsed.entries)) {
|
|
1207
|
+
throw new Error("unexpected shape");
|
|
1208
|
+
}
|
|
1209
|
+
return parsed;
|
|
1210
|
+
} catch (err) {
|
|
1211
|
+
throw new Error(
|
|
1212
|
+
`execution journal at ${this.filePath} is unreadable (${err instanceof Error ? err.message : String(err)}); refusing to execute until it is inspected`
|
|
1213
|
+
);
|
|
1214
|
+
}
|
|
1215
|
+
}
|
|
1216
|
+
persist(file) {
|
|
1217
|
+
const cutoff = nowSec() - MAX_AGE_SECONDS2;
|
|
1218
|
+
file.entries = file.entries.filter((e) => e.startedAt >= cutoff);
|
|
1219
|
+
mkdirSync5(dirname5(this.filePath), { recursive: true, mode: 448 });
|
|
1220
|
+
const tmp = this.filePath + ".tmp";
|
|
1221
|
+
writeFileSync5(tmp, JSON.stringify(file, null, 2), { mode: 384 });
|
|
1222
|
+
renameSync(tmp, this.filePath);
|
|
1223
|
+
}
|
|
1224
|
+
};
|
|
1225
|
+
function nowSec() {
|
|
1226
|
+
return Math.floor(Date.now() / 1e3);
|
|
1227
|
+
}
|
|
1228
|
+
|
|
1139
1229
|
// src/lib/gatekeeper.ts
|
|
1140
1230
|
import { verify } from "@hap/core";
|
|
1141
1231
|
var MCPGatekeeper = class {
|
|
@@ -1216,6 +1306,8 @@ var SharedState = class {
|
|
|
1216
1306
|
executionLog;
|
|
1217
1307
|
denialLog;
|
|
1218
1308
|
receiptArchive;
|
|
1309
|
+
/** Which tickets this Gatekeeper has executed — the "one execution per ticket" half of exactly-once. */
|
|
1310
|
+
executionJournal;
|
|
1219
1311
|
gatekeeper;
|
|
1220
1312
|
constructor(spUrl2, gateStorePath) {
|
|
1221
1313
|
this.spClient = new SPClient(spUrl2);
|
|
@@ -1224,6 +1316,7 @@ var SharedState = class {
|
|
|
1224
1316
|
this.executionLog = new ExecutionLog(gateStorePath);
|
|
1225
1317
|
this.denialLog = new DenialLog(gateStorePath);
|
|
1226
1318
|
this.receiptArchive = new ReceiptArchive(gateStorePath);
|
|
1319
|
+
this.executionJournal = new ExecutionJournal(gateStorePath);
|
|
1227
1320
|
this.gatekeeper = new MCPGatekeeper(this.cache);
|
|
1228
1321
|
}
|
|
1229
1322
|
/**
|
|
@@ -1372,17 +1465,17 @@ function formatConsumptionFull(entries) {
|
|
|
1372
1465
|
}
|
|
1373
1466
|
|
|
1374
1467
|
// src/lib/context-loader.ts
|
|
1375
|
-
import { readFileSync as
|
|
1376
|
-
import { homedir as
|
|
1377
|
-
import { join as
|
|
1378
|
-
var
|
|
1468
|
+
import { readFileSync as readFileSync7, existsSync as existsSync7 } from "fs";
|
|
1469
|
+
import { homedir as homedir6 } from "os";
|
|
1470
|
+
import { join as join7 } from "path";
|
|
1471
|
+
var DEFAULT_DIR6 = process.env.SUVEREN_DATA_DIR ?? join7(homedir6(), ".suveren");
|
|
1379
1472
|
var BRIEF_MAX_CHARS = 16 * 1024;
|
|
1380
1473
|
function readContextFile(dataDir) {
|
|
1381
|
-
const dir = dataDir ??
|
|
1382
|
-
const filePath =
|
|
1383
|
-
if (!
|
|
1474
|
+
const dir = dataDir ?? DEFAULT_DIR6;
|
|
1475
|
+
const filePath = join7(dir, "context.md");
|
|
1476
|
+
if (!existsSync7(filePath)) return null;
|
|
1384
1477
|
try {
|
|
1385
|
-
const content =
|
|
1478
|
+
const content = readFileSync7(filePath, "utf-8").trim();
|
|
1386
1479
|
return content || null;
|
|
1387
1480
|
} catch {
|
|
1388
1481
|
return null;
|
|
@@ -2458,6 +2551,30 @@ Proposal ID: ${proposal.id}. Use check-pending-commitments to track status.`
|
|
|
2458
2551
|
let outgoingArgs = shouldAttachFooter() && receiptId ? appendVerificationFooter(tool, args, receiptId, auth.subjects?.[0]) : args;
|
|
2459
2552
|
if (receiptId) outgoingArgs = attachReceiptId(tool, outgoingArgs, receiptId);
|
|
2460
2553
|
outgoingArgs = encodeOutgoingArgs(tool, outgoingArgs);
|
|
2554
|
+
if (receiptId) {
|
|
2555
|
+
const begun = state2.executionJournal.begin({
|
|
2556
|
+
ticketId: receiptId,
|
|
2557
|
+
tool: tool.namespacedName,
|
|
2558
|
+
argsHash: hashToolArgs(args)
|
|
2559
|
+
});
|
|
2560
|
+
if (!begun.ok) {
|
|
2561
|
+
return {
|
|
2562
|
+
content: [{
|
|
2563
|
+
type: "text",
|
|
2564
|
+
text: `Blocked: ticket ${receiptId} was already used to execute ${tool.namespacedName} (state: ${begun.existing.state}). Not running it again.`
|
|
2565
|
+
}],
|
|
2566
|
+
isError: true
|
|
2567
|
+
};
|
|
2568
|
+
}
|
|
2569
|
+
try {
|
|
2570
|
+
const result = await integrationManager2.callTool(tool.integrationId, tool.originalName, outgoingArgs);
|
|
2571
|
+
state2.executionJournal.complete(receiptId, "done");
|
|
2572
|
+
return result;
|
|
2573
|
+
} catch (err) {
|
|
2574
|
+
state2.executionJournal.complete(receiptId, "failed");
|
|
2575
|
+
throw err;
|
|
2576
|
+
}
|
|
2577
|
+
}
|
|
2461
2578
|
return integrationManager2.callTool(tool.integrationId, tool.originalName, outgoingArgs);
|
|
2462
2579
|
}
|
|
2463
2580
|
});
|
|
@@ -2786,6 +2903,13 @@ function listIntegrationsHandler(state2, integrationManager2) {
|
|
|
2786
2903
|
|
|
2787
2904
|
// src/tools/commitments.ts
|
|
2788
2905
|
import { ContentBindingError as ContentBindingError2 } from "@hap/core";
|
|
2906
|
+
var installedExecutor = null;
|
|
2907
|
+
function installCommittedExecutor(executor) {
|
|
2908
|
+
installedExecutor = executor;
|
|
2909
|
+
}
|
|
2910
|
+
function runCommitted(proposal, state2, integrationManager2) {
|
|
2911
|
+
return installedExecutor ? installedExecutor.execute(proposal) : executeCommitted(proposal, state2, integrationManager2);
|
|
2912
|
+
}
|
|
2789
2913
|
async function executeCommitted(proposal, state2, integrationManager2) {
|
|
2790
2914
|
if (!integrationManager2) {
|
|
2791
2915
|
return { text: `Proposal ${proposal.id} committed but integration manager unavailable for execution.`, isError: true };
|
|
@@ -2805,6 +2929,7 @@ async function executeCommitted(proposal, state2, integrationManager2) {
|
|
|
2805
2929
|
}
|
|
2806
2930
|
const cachedAuth = state2.cache.getAllAuthorizations().find((a) => a.authorizationId === proposal.authorizationId);
|
|
2807
2931
|
let receiptId;
|
|
2932
|
+
let replayed = false;
|
|
2808
2933
|
try {
|
|
2809
2934
|
const binding = computeContentBinding(
|
|
2810
2935
|
proposal.profileId,
|
|
@@ -2812,7 +2937,7 @@ async function executeCommitted(proposal, state2, integrationManager2) {
|
|
|
2812
2937
|
proposal.toolArgs,
|
|
2813
2938
|
proposalActionType
|
|
2814
2939
|
);
|
|
2815
|
-
const { receipt } = await state2.spClient.postReceipt({
|
|
2940
|
+
const { receipt, idempotent } = await state2.spClient.postReceipt({
|
|
2816
2941
|
authorizationId: proposal.authorizationId,
|
|
2817
2942
|
// Optional cross-check — the AS fails closed on a mismatch. Parity
|
|
2818
2943
|
// with the automatic path (tool-proxy.ts).
|
|
@@ -2837,6 +2962,7 @@ async function executeCommitted(proposal, state2, integrationManager2) {
|
|
|
2837
2962
|
...binding ? { contentHash: binding.contentHash, contentBinding: binding.contentBinding } : {}
|
|
2838
2963
|
});
|
|
2839
2964
|
receiptId = typeof receipt?.id === "string" ? receipt.id : void 0;
|
|
2965
|
+
replayed = idempotent;
|
|
2840
2966
|
await state2.archiveReceipt(receipt, {
|
|
2841
2967
|
authorizationId: proposal.authorizationId,
|
|
2842
2968
|
profileId: proposal.profileId,
|
|
@@ -2874,6 +3000,36 @@ async function executeCommitted(proposal, state2, integrationManager2) {
|
|
|
2874
3000
|
isError: true
|
|
2875
3001
|
};
|
|
2876
3002
|
}
|
|
3003
|
+
if (!receiptId) {
|
|
3004
|
+
return {
|
|
3005
|
+
text: `Proposal ${proposal.id}: the ticket carries no id \u2014 refusing to execute an action that could not be recorded.`,
|
|
3006
|
+
isError: true
|
|
3007
|
+
};
|
|
3008
|
+
}
|
|
3009
|
+
const begun = state2.executionJournal.begin({
|
|
3010
|
+
ticketId: receiptId,
|
|
3011
|
+
proposalId: proposal.id,
|
|
3012
|
+
tool: proposal.tool,
|
|
3013
|
+
argsHash: hashToolArgs(proposal.toolArgs)
|
|
3014
|
+
});
|
|
3015
|
+
if (!begun.ok) {
|
|
3016
|
+
const prior = begun.existing;
|
|
3017
|
+
const when = new Date(prior.startedAt * 1e3).toISOString();
|
|
3018
|
+
if (prior.state === "done") {
|
|
3019
|
+
return {
|
|
3020
|
+
text: `Proposal ${proposal.id} was already executed under ticket ${receiptId} at ${when}. Not running it again.`
|
|
3021
|
+
};
|
|
3022
|
+
}
|
|
3023
|
+
return {
|
|
3024
|
+
text: `Proposal ${proposal.id}: an execution under ticket ${receiptId} started at ${when} (pid ${prior.pid}) and ${prior.state === "failed" ? "reported failure" : "never reported completion"}. Not re-running: the action may already have taken effect. Check the downstream system before deciding.`,
|
|
3025
|
+
isError: true
|
|
3026
|
+
};
|
|
3027
|
+
}
|
|
3028
|
+
if (replayed) {
|
|
3029
|
+
console.error(
|
|
3030
|
+
`[Suveren MCP] Proposal ${proposal.id}: ticket ${receiptId} was replayed by the AS with no local execution record \u2014 executing once (lost-response recovery).`
|
|
3031
|
+
);
|
|
3032
|
+
}
|
|
2877
3033
|
try {
|
|
2878
3034
|
let outgoingArgs = proposal.toolArgs;
|
|
2879
3035
|
if (discovered && receiptId) {
|
|
@@ -2883,7 +3039,14 @@ async function executeCommitted(proposal, state2, integrationManager2) {
|
|
|
2883
3039
|
outgoingArgs = attachReceiptId(discovered, outgoingArgs, receiptId);
|
|
2884
3040
|
outgoingArgs = encodeOutgoingArgs(discovered, outgoingArgs);
|
|
2885
3041
|
}
|
|
2886
|
-
|
|
3042
|
+
let result;
|
|
3043
|
+
try {
|
|
3044
|
+
result = await integrationManager2.callTool(integrationId, toolName, outgoingArgs);
|
|
3045
|
+
} catch (err) {
|
|
3046
|
+
state2.executionJournal.complete(receiptId, "failed");
|
|
3047
|
+
throw err;
|
|
3048
|
+
}
|
|
3049
|
+
state2.executionJournal.complete(receiptId, "done");
|
|
2887
3050
|
state2.executionLog.record({
|
|
2888
3051
|
profileId: proposal.profileId,
|
|
2889
3052
|
path: proposal.path,
|
|
@@ -2918,7 +3081,7 @@ function checkPendingCommitmentsHandler(state2, integrationManager2) {
|
|
|
2918
3081
|
};
|
|
2919
3082
|
}
|
|
2920
3083
|
if (match.status === "committed") {
|
|
2921
|
-
const { text, isError } = await
|
|
3084
|
+
const { text, isError } = await runCommitted(match, state2, integrationManager2);
|
|
2922
3085
|
return {
|
|
2923
3086
|
content: [{ type: "text", text }],
|
|
2924
3087
|
...isError ? { isError: true } : {}
|
|
@@ -3172,16 +3335,16 @@ function verifyGateContentHashes(content, auth) {
|
|
|
3172
3335
|
}
|
|
3173
3336
|
|
|
3174
3337
|
// src/lib/integration-registry.ts
|
|
3175
|
-
import { mkdirSync as
|
|
3176
|
-
import { homedir as
|
|
3177
|
-
import { dirname as
|
|
3178
|
-
var DEFAULT_DATA_DIR = process.env.SUVEREN_DATA_DIR ??
|
|
3338
|
+
import { mkdirSync as mkdirSync6, readFileSync as readFileSync8, writeFileSync as writeFileSync6, existsSync as existsSync8 } from "fs";
|
|
3339
|
+
import { homedir as homedir7 } from "os";
|
|
3340
|
+
import { dirname as dirname6, join as join8 } from "path";
|
|
3341
|
+
var DEFAULT_DATA_DIR = process.env.SUVEREN_DATA_DIR ?? join8(homedir7(), ".suveren");
|
|
3179
3342
|
var IntegrationRegistry = class {
|
|
3180
3343
|
integrations = /* @__PURE__ */ new Map();
|
|
3181
3344
|
filePath;
|
|
3182
3345
|
constructor(dataDir) {
|
|
3183
3346
|
const dir = dataDir ?? DEFAULT_DATA_DIR;
|
|
3184
|
-
this.filePath =
|
|
3347
|
+
this.filePath = join8(dir, "integrations.json");
|
|
3185
3348
|
this.load();
|
|
3186
3349
|
}
|
|
3187
3350
|
// ─── CRUD ───────────────────────────────────────────────────────────────
|
|
@@ -3212,12 +3375,12 @@ var IntegrationRegistry = class {
|
|
|
3212
3375
|
}
|
|
3213
3376
|
// ─── Load / Save ────────────────────────────────────────────────────────
|
|
3214
3377
|
load() {
|
|
3215
|
-
if (!
|
|
3216
|
-
|
|
3378
|
+
if (!existsSync8(this.filePath)) {
|
|
3379
|
+
mkdirSync6(dirname6(this.filePath), { recursive: true });
|
|
3217
3380
|
return;
|
|
3218
3381
|
}
|
|
3219
3382
|
try {
|
|
3220
|
-
const raw =
|
|
3383
|
+
const raw = readFileSync8(this.filePath, "utf-8");
|
|
3221
3384
|
const data = JSON.parse(raw);
|
|
3222
3385
|
const migrated = data.integrations.map((i) => {
|
|
3223
3386
|
if ("toolGating" in i && !("profile" in i)) {
|
|
@@ -3246,39 +3409,39 @@ var IntegrationRegistry = class {
|
|
|
3246
3409
|
version: 1,
|
|
3247
3410
|
integrations: Array.from(this.integrations.values())
|
|
3248
3411
|
};
|
|
3249
|
-
|
|
3250
|
-
|
|
3412
|
+
mkdirSync6(dirname6(this.filePath), { recursive: true });
|
|
3413
|
+
writeFileSync6(this.filePath, JSON.stringify(data, null, 2), { encoding: "utf-8" });
|
|
3251
3414
|
}
|
|
3252
3415
|
};
|
|
3253
3416
|
|
|
3254
3417
|
// src/lib/integration-manager.ts
|
|
3255
|
-
import { homedir as
|
|
3256
|
-
import { join as
|
|
3257
|
-
import { existsSync as
|
|
3418
|
+
import { homedir as homedir8 } from "os";
|
|
3419
|
+
import { join as join9, delimiter } from "path";
|
|
3420
|
+
import { existsSync as existsSync9, writeFileSync as writeFileSync7, mkdirSync as mkdirSync7, readFileSync as readFileSync9, rmSync } from "fs";
|
|
3258
3421
|
import { execFile } from "child_process";
|
|
3259
3422
|
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
3260
3423
|
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
|
|
3261
3424
|
import { getProfile as getProfile5 } from "@hap/core";
|
|
3262
|
-
var DEFAULT_DATA_DIR2 = process.env.SUVEREN_DATA_DIR ??
|
|
3263
|
-
var INTEGRATIONS_DIR = process.env.SUVEREN_INTEGRATIONS_DIR ??
|
|
3425
|
+
var DEFAULT_DATA_DIR2 = process.env.SUVEREN_DATA_DIR ?? join9(homedir8(), ".suveren");
|
|
3426
|
+
var INTEGRATIONS_DIR = process.env.SUVEREN_INTEGRATIONS_DIR ?? join9(DEFAULT_DATA_DIR2, "integrations");
|
|
3264
3427
|
var installLock = Promise.resolve();
|
|
3265
3428
|
function withInstallLock(task) {
|
|
3266
3429
|
const run = installLock.then(task, task);
|
|
3267
3430
|
installLock = run.then(() => void 0, () => void 0);
|
|
3268
3431
|
return run;
|
|
3269
3432
|
}
|
|
3270
|
-
var INTEGRATIONS_BIN =
|
|
3433
|
+
var INTEGRATIONS_BIN = join9(INTEGRATIONS_DIR, "node_modules", ".bin");
|
|
3271
3434
|
function buildPath() {
|
|
3272
3435
|
const base = process.env.PATH ?? "";
|
|
3273
3436
|
return [INTEGRATIONS_BIN, base].join(delimiter);
|
|
3274
3437
|
}
|
|
3275
3438
|
function ensureIntegrationsDir() {
|
|
3276
|
-
if (!
|
|
3277
|
-
|
|
3439
|
+
if (!existsSync9(INTEGRATIONS_DIR)) {
|
|
3440
|
+
mkdirSync7(INTEGRATIONS_DIR, { recursive: true });
|
|
3278
3441
|
}
|
|
3279
|
-
const pkgPath =
|
|
3280
|
-
if (!
|
|
3281
|
-
|
|
3442
|
+
const pkgPath = join9(INTEGRATIONS_DIR, "package.json");
|
|
3443
|
+
if (!existsSync9(pkgPath)) {
|
|
3444
|
+
writeFileSync7(pkgPath, JSON.stringify({ name: "suveren-integrations", version: "1.0.0", private: true }, null, 2));
|
|
3282
3445
|
}
|
|
3283
3446
|
}
|
|
3284
3447
|
function readAgeOf(config) {
|
|
@@ -3344,19 +3507,19 @@ var IntegrationManager = class {
|
|
|
3344
3507
|
* exist, and treat anything else as not-installed (and reinstallable).
|
|
3345
3508
|
*/
|
|
3346
3509
|
isUsableInstall(npmPackage) {
|
|
3347
|
-
const pkgDir =
|
|
3348
|
-
const pkgJson =
|
|
3349
|
-
if (!
|
|
3510
|
+
const pkgDir = join9(INTEGRATIONS_DIR, "node_modules", ...npmPackage.split("/"));
|
|
3511
|
+
const pkgJson = join9(pkgDir, "package.json");
|
|
3512
|
+
if (!existsSync9(pkgDir) || !existsSync9(pkgJson)) return false;
|
|
3350
3513
|
try {
|
|
3351
|
-
const pkg = JSON.parse(
|
|
3514
|
+
const pkg = JSON.parse(readFileSync9(pkgJson, "utf8"));
|
|
3352
3515
|
const defaultBinName = (pkg.name ?? npmPackage).split("/").pop() ?? "";
|
|
3353
3516
|
const binMap = typeof pkg.bin === "string" ? { [defaultBinName]: pkg.bin } : pkg.bin ?? {};
|
|
3354
3517
|
const binEntries = Object.entries(binMap);
|
|
3355
3518
|
for (const [binName, target] of binEntries) {
|
|
3356
|
-
if (!
|
|
3519
|
+
if (!existsSync9(join9(pkgDir, target))) return false;
|
|
3357
3520
|
if (!this.binShimExists(binName)) return false;
|
|
3358
3521
|
}
|
|
3359
|
-
if (binEntries.length === 0 && pkg.main && !
|
|
3522
|
+
if (binEntries.length === 0 && pkg.main && !existsSync9(join9(pkgDir, pkg.main))) return false;
|
|
3360
3523
|
return true;
|
|
3361
3524
|
} catch {
|
|
3362
3525
|
return false;
|
|
@@ -3370,7 +3533,7 @@ var IntegrationManager = class {
|
|
|
3370
3533
|
binShimExists(binName) {
|
|
3371
3534
|
if (!binName) return false;
|
|
3372
3535
|
const candidates = process.platform === "win32" ? [binName, `${binName}.cmd`, `${binName}.CMD`, `${binName}.ps1`] : [binName];
|
|
3373
|
-
return candidates.some((c) =>
|
|
3536
|
+
return candidates.some((c) => existsSync9(join9(INTEGRATIONS_BIN, c)));
|
|
3374
3537
|
}
|
|
3375
3538
|
/**
|
|
3376
3539
|
* Install an npm package into the managed integrations directory if not
|
|
@@ -3394,8 +3557,8 @@ var IntegrationManager = class {
|
|
|
3394
3557
|
}
|
|
3395
3558
|
/** The actual install. Callers MUST hold the install lock. */
|
|
3396
3559
|
async installNow(npmPackage) {
|
|
3397
|
-
const pkgDir =
|
|
3398
|
-
if (
|
|
3560
|
+
const pkgDir = join9(INTEGRATIONS_DIR, "node_modules", ...npmPackage.split("/"));
|
|
3561
|
+
if (existsSync9(pkgDir)) {
|
|
3399
3562
|
console.error(`[IntegrationManager] Removing unusable install of ${npmPackage}`);
|
|
3400
3563
|
try {
|
|
3401
3564
|
rmSync(pkgDir, { recursive: true, force: true });
|
|
@@ -3811,28 +3974,28 @@ var IntegrationManager = class {
|
|
|
3811
3974
|
};
|
|
3812
3975
|
|
|
3813
3976
|
// src/lib/profile-loader.ts
|
|
3814
|
-
import { readFileSync as
|
|
3815
|
-
import { join as
|
|
3977
|
+
import { readFileSync as readFileSync10, existsSync as existsSync10 } from "fs";
|
|
3978
|
+
import { join as join10, resolve } from "path";
|
|
3816
3979
|
import { registerProfile } from "@hap/core";
|
|
3817
3980
|
function loadProfiles(profilesDir) {
|
|
3818
|
-
const dir = resolve(profilesDir ?? process.env.SUVEREN_PROFILES_DIR ??
|
|
3819
|
-
const indexPath =
|
|
3820
|
-
if (!
|
|
3981
|
+
const dir = resolve(profilesDir ?? process.env.SUVEREN_PROFILES_DIR ?? join10(import.meta.dirname ?? __dirname, "..", "..", "..", "..", "..", "hap-profiles"));
|
|
3982
|
+
const indexPath = join10(dir, "index.json");
|
|
3983
|
+
if (!existsSync10(indexPath)) {
|
|
3821
3984
|
warnNoProfiles(`No index.json at ${indexPath}`);
|
|
3822
3985
|
return 0;
|
|
3823
3986
|
}
|
|
3824
3987
|
let index;
|
|
3825
3988
|
try {
|
|
3826
|
-
index = JSON.parse(
|
|
3989
|
+
index = JSON.parse(readFileSync10(indexPath, "utf-8"));
|
|
3827
3990
|
} catch (err) {
|
|
3828
3991
|
console.error(`[ProfileLoader] Failed to parse ${indexPath}:`, err);
|
|
3829
3992
|
return 0;
|
|
3830
3993
|
}
|
|
3831
3994
|
let loaded = 0;
|
|
3832
3995
|
for (const [profileId, relativePath] of Object.entries(index.profiles)) {
|
|
3833
|
-
const profilePath =
|
|
3996
|
+
const profilePath = join10(dir, relativePath);
|
|
3834
3997
|
try {
|
|
3835
|
-
const profile = JSON.parse(
|
|
3998
|
+
const profile = JSON.parse(readFileSync10(profilePath, "utf-8"));
|
|
3836
3999
|
registerProfile(profileId, profile);
|
|
3837
4000
|
const shortName = profileId.split("/").pop()?.replace(/@.*$/, "");
|
|
3838
4001
|
if (shortName && shortName !== profileId) {
|
|
@@ -3865,30 +4028,30 @@ function warnNoProfiles(reason) {
|
|
|
3865
4028
|
}
|
|
3866
4029
|
|
|
3867
4030
|
// src/lib/manifest-loader.ts
|
|
3868
|
-
import { readFileSync as
|
|
3869
|
-
import { join as
|
|
4031
|
+
import { readFileSync as readFileSync11, existsSync as existsSync11 } from "fs";
|
|
4032
|
+
import { join as join11, resolve as resolve2 } from "path";
|
|
3870
4033
|
var manifests = /* @__PURE__ */ new Map();
|
|
3871
4034
|
function loadManifests(integrationsDir) {
|
|
3872
4035
|
const dir = resolve2(
|
|
3873
|
-
integrationsDir ?? process.env.SUVEREN_MANIFESTS_DIR ??
|
|
4036
|
+
integrationsDir ?? process.env.SUVEREN_MANIFESTS_DIR ?? join11(import.meta.dirname ?? __dirname, "..", "..", "..", "..", "content", "integrations")
|
|
3874
4037
|
);
|
|
3875
|
-
const indexPath =
|
|
3876
|
-
if (!
|
|
4038
|
+
const indexPath = join11(dir, "index.json");
|
|
4039
|
+
if (!existsSync11(indexPath)) {
|
|
3877
4040
|
console.error(`[ManifestLoader] No index.json found at ${indexPath}, skipping manifest loading`);
|
|
3878
4041
|
return 0;
|
|
3879
4042
|
}
|
|
3880
4043
|
let index;
|
|
3881
4044
|
try {
|
|
3882
|
-
index = JSON.parse(
|
|
4045
|
+
index = JSON.parse(readFileSync11(indexPath, "utf-8"));
|
|
3883
4046
|
} catch (err) {
|
|
3884
4047
|
console.error(`[ManifestLoader] Failed to parse ${indexPath}:`, err);
|
|
3885
4048
|
return 0;
|
|
3886
4049
|
}
|
|
3887
4050
|
let loaded = 0;
|
|
3888
4051
|
for (const [id, relativePath] of Object.entries(index.integrations)) {
|
|
3889
|
-
const manifestPath =
|
|
4052
|
+
const manifestPath = join11(dir, relativePath);
|
|
3890
4053
|
try {
|
|
3891
|
-
const manifest = JSON.parse(
|
|
4054
|
+
const manifest = JSON.parse(readFileSync11(manifestPath, "utf-8"));
|
|
3892
4055
|
manifests.set(id, manifest);
|
|
3893
4056
|
loaded++;
|
|
3894
4057
|
} catch (err) {
|
|
@@ -3907,6 +4070,90 @@ function getAllManifests() {
|
|
|
3907
4070
|
|
|
3908
4071
|
// bin/http.ts
|
|
3909
4072
|
import { decodeAttestationBlob as decodeAttestationBlob3 } from "@hap/core";
|
|
4073
|
+
|
|
4074
|
+
// src/lib/committed-executor.ts
|
|
4075
|
+
import { existsSync as existsSync12, mkdirSync as mkdirSync8, readFileSync as readFileSync12, unlinkSync as unlinkSync5, writeFileSync as writeFileSync8 } from "fs";
|
|
4076
|
+
import { dirname as dirname7, join as join12 } from "path";
|
|
4077
|
+
var CommittedExecutor = class {
|
|
4078
|
+
constructor(run) {
|
|
4079
|
+
this.run = run;
|
|
4080
|
+
}
|
|
4081
|
+
inFlight = /* @__PURE__ */ new Map();
|
|
4082
|
+
chain = Promise.resolve();
|
|
4083
|
+
/** Execute `proposal` once, however many callers ask while it is running. */
|
|
4084
|
+
execute(proposal) {
|
|
4085
|
+
const existing = this.inFlight.get(proposal.id);
|
|
4086
|
+
if (existing) return existing;
|
|
4087
|
+
const p = this.chain.then(() => this.run(proposal)).finally(() => this.inFlight.delete(proposal.id));
|
|
4088
|
+
this.chain = p.then(
|
|
4089
|
+
() => void 0,
|
|
4090
|
+
() => void 0
|
|
4091
|
+
);
|
|
4092
|
+
this.inFlight.set(proposal.id, p);
|
|
4093
|
+
return p;
|
|
4094
|
+
}
|
|
4095
|
+
/** Proposal ids currently queued or running. Diagnostic. */
|
|
4096
|
+
get pending() {
|
|
4097
|
+
return [...this.inFlight.keys()];
|
|
4098
|
+
}
|
|
4099
|
+
};
|
|
4100
|
+
var ExecutorLock = class {
|
|
4101
|
+
path;
|
|
4102
|
+
held = false;
|
|
4103
|
+
constructor(dataDir) {
|
|
4104
|
+
this.path = join12(dataDir, "executor.lock");
|
|
4105
|
+
}
|
|
4106
|
+
/** Try to take the lock. Returns the holder's pid when someone else has it. */
|
|
4107
|
+
acquire() {
|
|
4108
|
+
const current = this.read();
|
|
4109
|
+
if (current && current.pid !== process.pid && isAlive(current.pid)) {
|
|
4110
|
+
return { held: false, holderPid: current.pid };
|
|
4111
|
+
}
|
|
4112
|
+
mkdirSync8(dirname7(this.path), { recursive: true, mode: 448 });
|
|
4113
|
+
const lock = { pid: process.pid, startedAt: Math.floor(Date.now() / 1e3) };
|
|
4114
|
+
writeFileSync8(this.path, JSON.stringify(lock), { mode: 384 });
|
|
4115
|
+
this.held = true;
|
|
4116
|
+
return { held: true };
|
|
4117
|
+
}
|
|
4118
|
+
/** True if this process currently holds the lock (re-checks the file). */
|
|
4119
|
+
isHeld() {
|
|
4120
|
+
if (!this.held) return false;
|
|
4121
|
+
const current = this.read();
|
|
4122
|
+
return current?.pid === process.pid;
|
|
4123
|
+
}
|
|
4124
|
+
release() {
|
|
4125
|
+
if (!this.held) return;
|
|
4126
|
+
this.held = false;
|
|
4127
|
+
const current = this.read();
|
|
4128
|
+
if (current?.pid === process.pid) {
|
|
4129
|
+
try {
|
|
4130
|
+
unlinkSync5(this.path);
|
|
4131
|
+
} catch {
|
|
4132
|
+
}
|
|
4133
|
+
}
|
|
4134
|
+
}
|
|
4135
|
+
read() {
|
|
4136
|
+
if (!existsSync12(this.path)) return null;
|
|
4137
|
+
try {
|
|
4138
|
+
const parsed = JSON.parse(readFileSync12(this.path, "utf8"));
|
|
4139
|
+
return typeof parsed?.pid === "number" ? parsed : null;
|
|
4140
|
+
} catch {
|
|
4141
|
+
return null;
|
|
4142
|
+
}
|
|
4143
|
+
}
|
|
4144
|
+
};
|
|
4145
|
+
function isAlive(pid) {
|
|
4146
|
+
try {
|
|
4147
|
+
process.kill(pid, 0);
|
|
4148
|
+
return true;
|
|
4149
|
+
} catch (err) {
|
|
4150
|
+
return err.code === "EPERM";
|
|
4151
|
+
}
|
|
4152
|
+
}
|
|
4153
|
+
|
|
4154
|
+
// bin/http.ts
|
|
4155
|
+
import { homedir as homedir9 } from "os";
|
|
4156
|
+
import { join as join13 } from "path";
|
|
3910
4157
|
var spUrl = process.env.SUVEREN_AS_URL ?? "https://www.suveren.ai";
|
|
3911
4158
|
var port = parseInt(process.env.SUVEREN_MCP_PORT ?? "3430", 10);
|
|
3912
4159
|
var state = new SharedState(spUrl);
|
|
@@ -4397,12 +4644,31 @@ app.listen(port, "0.0.0.0", () => {
|
|
|
4397
4644
|
}
|
|
4398
4645
|
});
|
|
4399
4646
|
const PROPOSAL_POLL_INTERVAL = 5e3;
|
|
4647
|
+
const committedExecutor = new CommittedExecutor(
|
|
4648
|
+
(proposal) => executeCommitted(proposal, state, integrationManager)
|
|
4649
|
+
);
|
|
4650
|
+
installCommittedExecutor(committedExecutor);
|
|
4651
|
+
const dataDir = process.env.SUVEREN_DATA_DIR ?? join13(homedir9(), ".suveren");
|
|
4652
|
+
const executorLock = new ExecutorLock(dataDir);
|
|
4653
|
+
const lock = executorLock.acquire();
|
|
4654
|
+
if (!lock.held) {
|
|
4655
|
+
console.error(
|
|
4656
|
+
`[Suveren MCP] Committed-proposal execution is DISABLED in this process: pid ${lock.holderPid} holds ${dataDir}/executor.lock. Approved proposals will run there. Stop that gateway if this one should execute.`
|
|
4657
|
+
);
|
|
4658
|
+
}
|
|
4659
|
+
for (const sig of ["SIGINT", "SIGTERM", "exit"]) {
|
|
4660
|
+
process.once(sig, () => executorLock.release());
|
|
4661
|
+
}
|
|
4662
|
+
let draining = false;
|
|
4400
4663
|
async function executeCommittedProposals() {
|
|
4664
|
+
if (!executorLock.isHeld()) return;
|
|
4665
|
+
if (draining) return;
|
|
4666
|
+
draining = true;
|
|
4401
4667
|
try {
|
|
4402
4668
|
const committed = await state.spClient.getCommittedProposals();
|
|
4403
4669
|
for (const proposal of committed) {
|
|
4404
4670
|
try {
|
|
4405
|
-
const { text, isError } = await
|
|
4671
|
+
const { text, isError } = await committedExecutor.execute(proposal);
|
|
4406
4672
|
console.error(
|
|
4407
4673
|
isError ? `[Suveren MCP] Auto-exec proposal ${proposal.id}: ${text}` : `[Suveren MCP] Auto-executed proposal ${proposal.id}: ${proposal.tool}`
|
|
4408
4674
|
);
|
|
@@ -4411,6 +4677,8 @@ app.listen(port, "0.0.0.0", () => {
|
|
|
4411
4677
|
}
|
|
4412
4678
|
}
|
|
4413
4679
|
} catch {
|
|
4680
|
+
} finally {
|
|
4681
|
+
draining = false;
|
|
4414
4682
|
}
|
|
4415
4683
|
}
|
|
4416
4684
|
setInterval(executeCommittedProposals, PROPOSAL_POLL_INTERVAL);
|
|
@@ -157,24 +157,24 @@ const hc = (baseUrl, options) => createProxy(function proxyCallback(opts) {
|
|
|
157
157
|
const path = parts.join("/");
|
|
158
158
|
const url = (0, import_utils.mergePath)(baseUrl, path);
|
|
159
159
|
if (method === "url" || method === "path") {
|
|
160
|
-
let result = url;
|
|
160
|
+
let result = (0, import_utils.removeIndexString)(url);
|
|
161
161
|
if (opts.args[0]) {
|
|
162
162
|
if (opts.args[0].param) {
|
|
163
|
-
result = (0, import_utils.replaceUrlParam)(
|
|
163
|
+
result = (0, import_utils.replaceUrlParam)(result, opts.args[0].param);
|
|
164
164
|
}
|
|
165
165
|
if (opts.args[0].query) {
|
|
166
166
|
result = appendQueryParams(result, buildSearchParamsOption(opts.args[0].query));
|
|
167
167
|
}
|
|
168
168
|
}
|
|
169
|
-
result = (0, import_utils.removeIndexString)(result);
|
|
170
169
|
if (method === "url") {
|
|
171
170
|
return new URL(result);
|
|
172
171
|
}
|
|
173
172
|
return result.slice(baseUrl.replace(/\/+$/, "").length).replace(/^\/?/, "/");
|
|
174
173
|
}
|
|
175
174
|
if (method === "ws") {
|
|
175
|
+
const normalizedUrl = (0, import_utils.removeIndexString)(url);
|
|
176
176
|
const webSocketUrl = (0, import_utils.replaceUrlProtocol)(
|
|
177
|
-
opts.args[0]?.param ? (0, import_utils.replaceUrlParam)(
|
|
177
|
+
opts.args[0]?.param ? (0, import_utils.replaceUrlParam)(normalizedUrl, opts.args[0].param) : normalizedUrl,
|
|
178
178
|
"ws"
|
|
179
179
|
);
|
|
180
180
|
const targetUrl = new URL(webSocketUrl);
|