@suveren/gateway 0.31.3 → 0.33.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/dist/control-plane/index.mjs +37 -1
- package/dist/mcp-server/http.mjs +142 -12
- package/dist/ui/assets/index-Bev2H2zu.js +123 -0
- package/dist/ui/assets/index-BiGnI2Tk.css +1 -0
- package/dist/ui/index.html +2 -2
- package/package.json +1 -1
- package/profiles/customers/0.11.profile.json +186 -0
- package/profiles/delegation/0.5.profile.json +194 -0
- package/profiles/deploy/0.12.profile.json +177 -0
- package/profiles/index.json +7 -1
- package/profiles/publish/0.7.profile.json +187 -0
- package/profiles/purchase/0.8.profile.json +235 -0
- package/profiles/reporting/0.4.profile.json +98 -0
- package/dist/ui/assets/index-CC9U7Luz.css +0 -1
- package/dist/ui/assets/index-CfPjctPy.js +0 -123
|
@@ -657,6 +657,20 @@ async function getLocalEvidence() {
|
|
|
657
657
|
if (!res.ok) throw new Error(`Failed to fetch local evidence: ${res.status}`);
|
|
658
658
|
return res.json();
|
|
659
659
|
}
|
|
660
|
+
async function getTicketContent(ticketId) {
|
|
661
|
+
const res = await fetch(`${MCP_BASE}/internal/evidence/ticket/${encodeURIComponent(ticketId)}`, {
|
|
662
|
+
headers: internalHeaders()
|
|
663
|
+
});
|
|
664
|
+
if (res.status === 404) return null;
|
|
665
|
+
if (!res.ok) throw new Error(`Failed to fetch ticket content: ${res.status}`);
|
|
666
|
+
return res.json();
|
|
667
|
+
}
|
|
668
|
+
async function getTicketHeadlines() {
|
|
669
|
+
const res = await fetch(`${MCP_BASE}/internal/evidence/headlines`, { headers: internalHeaders() });
|
|
670
|
+
if (!res.ok) throw new Error(`Failed to fetch ticket headlines: ${res.status}`);
|
|
671
|
+
const data = await res.json();
|
|
672
|
+
return data.headlines ?? {};
|
|
673
|
+
}
|
|
660
674
|
var McpLockedError = class extends Error {
|
|
661
675
|
constructor(message) {
|
|
662
676
|
super(message);
|
|
@@ -4104,7 +4118,29 @@ function createEvidenceRouter() {
|
|
|
4104
4118
|
const authorizationId = entry.authorizationId;
|
|
4105
4119
|
const authorization = authorizationId ? mergeAuthorizations(evidence)[authorizationId] ?? null : null;
|
|
4106
4120
|
const signature = (await verifyAll([entry]))[id] ?? "unverifiable";
|
|
4107
|
-
|
|
4121
|
+
let content = null;
|
|
4122
|
+
try {
|
|
4123
|
+
content = await getTicketContent(id);
|
|
4124
|
+
} catch {
|
|
4125
|
+
content = null;
|
|
4126
|
+
}
|
|
4127
|
+
res.json({
|
|
4128
|
+
entry,
|
|
4129
|
+
authorization,
|
|
4130
|
+
signature,
|
|
4131
|
+
sentArgsCheck: content?.sentArgsCheck ?? "unchecked",
|
|
4132
|
+
result: content?.result ?? null
|
|
4133
|
+
});
|
|
4134
|
+
} catch (err) {
|
|
4135
|
+
res.status(503).json({
|
|
4136
|
+
error: "Local evidence unavailable",
|
|
4137
|
+
detail: err instanceof Error ? err.message : String(err)
|
|
4138
|
+
});
|
|
4139
|
+
}
|
|
4140
|
+
});
|
|
4141
|
+
router.get("/headlines", async (_req, res) => {
|
|
4142
|
+
try {
|
|
4143
|
+
res.json({ headlines: await getTicketHeadlines() });
|
|
4108
4144
|
} catch (err) {
|
|
4109
4145
|
res.status(503).json({
|
|
4110
4146
|
error: "Local evidence unavailable",
|
package/dist/mcp-server/http.mjs
CHANGED
|
@@ -1917,6 +1917,13 @@ var ArchiveWriteError = class extends Error {
|
|
|
1917
1917
|
}
|
|
1918
1918
|
};
|
|
1919
1919
|
var DEFAULT_DIR4 = process.env.SUVEREN_DATA_DIR ?? join7(homedir4(), ".suveren");
|
|
1920
|
+
var SENT_ARGS_MAX_BYTES = 64 * 1024;
|
|
1921
|
+
function sentArgsFields(args) {
|
|
1922
|
+
if (args === void 0) return {};
|
|
1923
|
+
const json = JSON.stringify(args);
|
|
1924
|
+
if (Buffer.byteLength(json, "utf8") > SENT_ARGS_MAX_BYTES) return { sentArgsOmitted: "too_large" };
|
|
1925
|
+
return { sentArgs: JSON.parse(json) };
|
|
1926
|
+
}
|
|
1920
1927
|
function stripPreviews(proposal) {
|
|
1921
1928
|
const args = proposal.toolArgs;
|
|
1922
1929
|
if (!args || typeof args !== "object") return proposal;
|
|
@@ -1977,7 +1984,8 @@ var ReceiptArchive = class {
|
|
|
1977
1984
|
asPublicKey: entry.asPublicKey,
|
|
1978
1985
|
receipt: entry.receipt,
|
|
1979
1986
|
...entry.proposal ? { proposal: stripPreviews(entry.proposal) } : {},
|
|
1980
|
-
...entry.boundContent !== void 0 ? { boundContent: entry.boundContent } : {}
|
|
1987
|
+
...entry.boundContent !== void 0 ? { boundContent: entry.boundContent } : {},
|
|
1988
|
+
...sentArgsFields(entry.sentArgs)
|
|
1981
1989
|
});
|
|
1982
1990
|
changed = true;
|
|
1983
1991
|
}
|
|
@@ -2289,12 +2297,16 @@ var ExecutionResultStore = class {
|
|
|
2289
2297
|
isLocked() {
|
|
2290
2298
|
return this.vaultKey === null && existsSync9(this.encryptedFilePath);
|
|
2291
2299
|
}
|
|
2292
|
-
/**
|
|
2300
|
+
/**
|
|
2301
|
+
* Record the outcome of one execution. Replaces any prior entry for the
|
|
2302
|
+
* same proposal — or, for an automatic execution (no proposalId), for the
|
|
2303
|
+
* same ticket.
|
|
2304
|
+
*/
|
|
2293
2305
|
record(input, now = nowSec2()) {
|
|
2294
2306
|
const capped = capResult(input.text, input.structuredContent);
|
|
2295
|
-
this.entries = this.entries.filter((e) => e.proposalId !== input.proposalId);
|
|
2307
|
+
this.entries = input.proposalId !== void 0 ? this.entries.filter((e) => e.proposalId !== input.proposalId) : this.entries.filter((e) => e.proposalId !== void 0 || e.ticketId !== input.ticketId);
|
|
2296
2308
|
this.entries.push({
|
|
2297
|
-
proposalId: input.proposalId,
|
|
2309
|
+
...input.proposalId !== void 0 ? { proposalId: input.proposalId } : {},
|
|
2298
2310
|
ticketId: input.ticketId,
|
|
2299
2311
|
ok: input.ok,
|
|
2300
2312
|
text: capped.text,
|
|
@@ -2309,6 +2321,13 @@ var ExecutionResultStore = class {
|
|
|
2309
2321
|
get(proposalId) {
|
|
2310
2322
|
return this.entries.find((e) => e.proposalId === proposalId);
|
|
2311
2323
|
}
|
|
2324
|
+
/** The retained outcome of the execution under a ticket (either path), newest first. */
|
|
2325
|
+
getByTicket(ticketId) {
|
|
2326
|
+
for (let i = this.entries.length - 1; i >= 0; i--) {
|
|
2327
|
+
if (this.entries[i].ticketId === ticketId) return this.entries[i];
|
|
2328
|
+
}
|
|
2329
|
+
return void 0;
|
|
2330
|
+
}
|
|
2312
2331
|
/** Diagnostic / tests. */
|
|
2313
2332
|
get size() {
|
|
2314
2333
|
return this.entries.length;
|
|
@@ -3334,6 +3353,7 @@ var SharedState = class {
|
|
|
3334
3353
|
asPublicKey,
|
|
3335
3354
|
proposal: opts.proposal,
|
|
3336
3355
|
boundContent: opts.boundContent,
|
|
3356
|
+
sentArgs: opts.sentArgs,
|
|
3337
3357
|
authorization: opts.profileId ? {
|
|
3338
3358
|
profileId: opts.profileId,
|
|
3339
3359
|
boundsHash: opts.boundsHash,
|
|
@@ -3417,6 +3437,9 @@ function getConsumptionState(auth, executionLog, profile) {
|
|
|
3417
3437
|
now,
|
|
3418
3438
|
boundActionTypes(fieldName, fieldDef)
|
|
3419
3439
|
);
|
|
3440
|
+
const unit = fieldDef.unit;
|
|
3441
|
+
const frameCurrency = auth.frame.currency;
|
|
3442
|
+
const currency = bt.kind !== "cumulative_sum" ? void 0 : typeof frameCurrency === "string" && /^[A-Z]{3}$/.test(frameCurrency) ? frameCurrency : typeof unit === "string" && unit.startsWith("currency:") ? unit.slice("currency:".length) : void 0;
|
|
3420
3443
|
entries.push({
|
|
3421
3444
|
// displayName FIRST: description is prose meant for a human reading the
|
|
3422
3445
|
// profile, and rendering it here produced a paragraph where a label
|
|
@@ -3427,20 +3450,23 @@ function getConsumptionState(auth, executionLog, profile) {
|
|
|
3427
3450
|
window: bt.window,
|
|
3428
3451
|
field: fieldName,
|
|
3429
3452
|
kind: bt.kind === "cumulative_sum" ? "sum" : "count",
|
|
3430
|
-
of: bt.kind === "cumulative_sum" ? bt.of : void 0
|
|
3453
|
+
of: bt.kind === "cumulative_sum" ? bt.of : void 0,
|
|
3454
|
+
...currency ? { currency } : {}
|
|
3431
3455
|
});
|
|
3432
3456
|
}
|
|
3433
3457
|
return entries;
|
|
3434
3458
|
}
|
|
3435
|
-
|
|
3459
|
+
function formatSum(value, currency) {
|
|
3460
|
+
if (currency) return `${value.toFixed(2)} ${currency}`;
|
|
3461
|
+
return String(Number(value.toFixed(6)));
|
|
3462
|
+
}
|
|
3436
3463
|
function formatConsumptionCompact(entries) {
|
|
3437
3464
|
if (entries.length === 0) return "";
|
|
3438
3465
|
const parts = [];
|
|
3439
3466
|
for (const entry of entries) {
|
|
3440
3467
|
if (entry.limit === null) continue;
|
|
3441
3468
|
if (entry.kind === "sum") {
|
|
3442
|
-
|
|
3443
|
-
parts.push(`${prefix}${entry.current}/${prefix}${entry.limit} ${entry.window}`);
|
|
3469
|
+
parts.push(`${formatSum(entry.current, entry.currency)}/${formatSum(entry.limit, entry.currency)} ${entry.window}`);
|
|
3444
3470
|
} else {
|
|
3445
3471
|
parts.push(`${entry.current}/${entry.limit} ${entry.window}`);
|
|
3446
3472
|
}
|
|
@@ -3451,9 +3477,10 @@ function formatConsumptionFull(entries) {
|
|
|
3451
3477
|
if (entries.length === 0) return "";
|
|
3452
3478
|
const lines = [];
|
|
3453
3479
|
for (const entry of entries) {
|
|
3454
|
-
const limitStr = entry.limit !== null ? String(entry.limit) : "unlimited";
|
|
3480
|
+
const limitStr = entry.limit !== null ? entry.kind === "sum" ? formatSum(entry.limit, entry.currency) : String(entry.limit) : "unlimited";
|
|
3481
|
+
const currentStr = entry.kind === "sum" ? formatSum(entry.current, entry.currency) : String(entry.current);
|
|
3455
3482
|
const label = `${entry.label}:`;
|
|
3456
|
-
lines.push(` ${label.padEnd(30)} ${
|
|
3483
|
+
lines.push(` ${label.padEnd(30)} ${currentStr} / ${limitStr}`);
|
|
3457
3484
|
}
|
|
3458
3485
|
return lines.join("\n");
|
|
3459
3486
|
}
|
|
@@ -6009,7 +6036,12 @@ Proposal ID: ${proposal.id}. Check status with check-pending-commitments(proposa
|
|
|
6009
6036
|
attestations: auth.attestations,
|
|
6010
6037
|
// Automatic path has no proposal, so without this the archive
|
|
6011
6038
|
// would hold a hash of content it cannot reproduce.
|
|
6012
|
-
boundContent: binding?.boundContent
|
|
6039
|
+
boundContent: binding?.boundContent,
|
|
6040
|
+
// UI spec §14 — what this call sends, exactly the object the
|
|
6041
|
+
// journal's argsHash is computed from below (`hashToolArgs(args)`),
|
|
6042
|
+
// so the ticket view can show it and check it. Same write, so it
|
|
6043
|
+
// is as fail-closed and as encrypted as the rest of the entry.
|
|
6044
|
+
sentArgs: args
|
|
6013
6045
|
});
|
|
6014
6046
|
} catch (err) {
|
|
6015
6047
|
if (err instanceof AsKeyMismatchError || err instanceof TicketBindingMismatchError) {
|
|
@@ -6207,9 +6239,11 @@ Proposal ID: ${proposal.id}. Use check-pending-commitments to track status.` + a
|
|
|
6207
6239
|
try {
|
|
6208
6240
|
const result = await integrationManager2.callTool(tool.integrationId, tool.originalName, outgoingArgs);
|
|
6209
6241
|
state2.executionJournal.complete(receiptId, "done");
|
|
6242
|
+
recordAutomaticOutcome(state2, receiptId, result);
|
|
6210
6243
|
return result;
|
|
6211
6244
|
} catch (err) {
|
|
6212
6245
|
state2.executionJournal.complete(receiptId, "failed");
|
|
6246
|
+
recordAutomaticOutcome(state2, receiptId, void 0, err);
|
|
6213
6247
|
throw err;
|
|
6214
6248
|
}
|
|
6215
6249
|
}
|
|
@@ -6217,6 +6251,20 @@ Proposal ID: ${proposal.id}. Use check-pending-commitments to track status.` + a
|
|
|
6217
6251
|
}
|
|
6218
6252
|
});
|
|
6219
6253
|
}
|
|
6254
|
+
function recordAutomaticOutcome(state2, ticketId, result, thrown) {
|
|
6255
|
+
try {
|
|
6256
|
+
if (!result) {
|
|
6257
|
+
const msg = thrown instanceof Error ? thrown.message : String(thrown);
|
|
6258
|
+
state2.executionResults?.record({ ticketId, ok: false, text: `The tool call threw: ${msg}` });
|
|
6259
|
+
return;
|
|
6260
|
+
}
|
|
6261
|
+
const text = result.content?.[0]?.text ?? JSON.stringify(result);
|
|
6262
|
+
const structured = result.structuredContent && typeof result.structuredContent === "object" && !Array.isArray(result.structuredContent) ? result.structuredContent : void 0;
|
|
6263
|
+
state2.executionResults?.record({ ticketId, ok: result.isError !== true, text, structuredContent: structured });
|
|
6264
|
+
} catch (err) {
|
|
6265
|
+
console.error(`[Suveren MCP] Could not record the execution result for ticket ${ticketId}:`, err);
|
|
6266
|
+
}
|
|
6267
|
+
}
|
|
6220
6268
|
function noMandateRefusal(toolName, profile) {
|
|
6221
6269
|
return `Refused: no mandate covers ${toolName} (${shortProfile(profile)}). Nothing was done. Ask the person to grant a ${shortProfile(profile)} mandate in the gateway, then try again. No ticket was requested.`;
|
|
6222
6270
|
}
|
|
@@ -8267,7 +8315,14 @@ var IntegrationManager = class {
|
|
|
8267
8315
|
getToolDisplay() {
|
|
8268
8316
|
const out = {};
|
|
8269
8317
|
for (const t of this.getAllTools()) {
|
|
8270
|
-
|
|
8318
|
+
const mapping = t.gating?.executionMapping;
|
|
8319
|
+
out[t.namespacedName] = {
|
|
8320
|
+
inputSchema: t.inputSchema,
|
|
8321
|
+
...t.gating?.approvalView ? { approvalView: t.gating.approvalView } : {},
|
|
8322
|
+
// UI spec §14.1 — which sent values the signed ticket also carries
|
|
8323
|
+
// (arg → executionContext field), for the "in the ticket" tag.
|
|
8324
|
+
...mapping && Object.keys(mapping).length > 0 ? { executionMapping: mapping } : {}
|
|
8325
|
+
};
|
|
8271
8326
|
}
|
|
8272
8327
|
return out;
|
|
8273
8328
|
}
|
|
@@ -9992,6 +10047,52 @@ function isAlive(pid) {
|
|
|
9992
10047
|
}
|
|
9993
10048
|
}
|
|
9994
10049
|
|
|
10050
|
+
// src/lib/ticket-content.ts
|
|
10051
|
+
function archivedArgs(entry) {
|
|
10052
|
+
if (entry.sentArgs && typeof entry.sentArgs === "object") return { args: entry.sentArgs, source: "sent" };
|
|
10053
|
+
const toolArgs = entry.proposal?.toolArgs;
|
|
10054
|
+
if (toolArgs && typeof toolArgs === "object" && !Array.isArray(toolArgs)) {
|
|
10055
|
+
return { args: toolArgs, source: "proposal" };
|
|
10056
|
+
}
|
|
10057
|
+
return void 0;
|
|
10058
|
+
}
|
|
10059
|
+
function checkSentArgs(entry, journalRow) {
|
|
10060
|
+
const found = archivedArgs(entry);
|
|
10061
|
+
if (!found || !journalRow?.argsHash) return "unchecked";
|
|
10062
|
+
if (hashToolArgs(found.args) === journalRow.argsHash) return "match";
|
|
10063
|
+
if (found.source === "proposal" && typeof found.args.imagePath === "string") return "unchecked";
|
|
10064
|
+
return "mismatch";
|
|
10065
|
+
}
|
|
10066
|
+
function resultView(entry) {
|
|
10067
|
+
if (!entry) return null;
|
|
10068
|
+
return {
|
|
10069
|
+
ok: entry.ok,
|
|
10070
|
+
text: entry.text,
|
|
10071
|
+
...entry.structuredContent ? { structuredContent: entry.structuredContent } : {},
|
|
10072
|
+
...entry.truncated ? { truncated: true } : {}
|
|
10073
|
+
};
|
|
10074
|
+
}
|
|
10075
|
+
function trimForHeadline(value, max = 200) {
|
|
10076
|
+
if (typeof value === "string") return value.length > max ? value.slice(0, max) : value;
|
|
10077
|
+
if (Array.isArray(value)) return value.map((v) => trimForHeadline(v, max));
|
|
10078
|
+
if (value && typeof value === "object") {
|
|
10079
|
+
const out = {};
|
|
10080
|
+
for (const [k, v] of Object.entries(value)) out[k] = trimForHeadline(v, max);
|
|
10081
|
+
return out;
|
|
10082
|
+
}
|
|
10083
|
+
return value;
|
|
10084
|
+
}
|
|
10085
|
+
function headlineArgs(entries) {
|
|
10086
|
+
const out = {};
|
|
10087
|
+
for (const e of entries) {
|
|
10088
|
+
const id = e.receipt?.id;
|
|
10089
|
+
if (typeof id !== "string" || e.proposal) continue;
|
|
10090
|
+
const src = e.sentArgs ?? (e.boundContent && typeof e.boundContent === "object" ? e.boundContent : void 0);
|
|
10091
|
+
if (src) out[id] = trimForHeadline(src);
|
|
10092
|
+
}
|
|
10093
|
+
return out;
|
|
10094
|
+
}
|
|
10095
|
+
|
|
9995
10096
|
// bin/http.ts
|
|
9996
10097
|
import { homedir as homedir14 } from "os";
|
|
9997
10098
|
import { join as join23 } from "path";
|
|
@@ -10528,6 +10629,35 @@ app.get("/internal/evidence", internalOnly, (_req, res) => {
|
|
|
10528
10629
|
gates: state.gateStore.getAll()
|
|
10529
10630
|
});
|
|
10530
10631
|
});
|
|
10632
|
+
app.get("/internal/evidence/ticket/:id", internalOnly, (req, res) => {
|
|
10633
|
+
if (state.receiptArchive.isLocked() || state.executionResults.isLocked()) {
|
|
10634
|
+
res.status(503).json({ error: "Vault locked \u2014 the evidence archive is encrypted until sign-in." });
|
|
10635
|
+
return;
|
|
10636
|
+
}
|
|
10637
|
+
const id = String(req.params.id);
|
|
10638
|
+
const entry = state.receiptArchive.getReceipts().find((r) => r.receipt.id === id);
|
|
10639
|
+
if (!entry) {
|
|
10640
|
+
res.status(404).json({ error: "No local copy of this ticket" });
|
|
10641
|
+
return;
|
|
10642
|
+
}
|
|
10643
|
+
let journalRow;
|
|
10644
|
+
try {
|
|
10645
|
+
journalRow = state.executionJournal.get(id);
|
|
10646
|
+
} catch {
|
|
10647
|
+
journalRow = void 0;
|
|
10648
|
+
}
|
|
10649
|
+
res.json({
|
|
10650
|
+
sentArgsCheck: checkSentArgs(entry, journalRow),
|
|
10651
|
+
result: resultView(state.executionResults.getByTicket(id))
|
|
10652
|
+
});
|
|
10653
|
+
});
|
|
10654
|
+
app.get("/internal/evidence/headlines", internalOnly, (_req, res) => {
|
|
10655
|
+
if (state.receiptArchive.isLocked()) {
|
|
10656
|
+
res.status(503).json({ error: "Vault locked \u2014 the evidence archive is encrypted until sign-in." });
|
|
10657
|
+
return;
|
|
10658
|
+
}
|
|
10659
|
+
res.json({ headlines: headlineArgs(state.receiptArchive.getReceipts()) });
|
|
10660
|
+
});
|
|
10531
10661
|
function reportScope() {
|
|
10532
10662
|
return scopeReportSources(reportSources, {
|
|
10533
10663
|
authorizations: state.getEnrichedAuthorizations(),
|