@suveren/gateway 0.32.0 → 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.
@@ -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
- res.json({ entry, authorization, signature });
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",
@@ -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
- /** Record the outcome of one execution. Replaces any prior entry for the same proposal. */
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,
@@ -6016,7 +6036,12 @@ Proposal ID: ${proposal.id}. Check status with check-pending-commitments(proposa
6016
6036
  attestations: auth.attestations,
6017
6037
  // Automatic path has no proposal, so without this the archive
6018
6038
  // would hold a hash of content it cannot reproduce.
6019
- 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
6020
6045
  });
6021
6046
  } catch (err) {
6022
6047
  if (err instanceof AsKeyMismatchError || err instanceof TicketBindingMismatchError) {
@@ -6214,9 +6239,11 @@ Proposal ID: ${proposal.id}. Use check-pending-commitments to track status.` + a
6214
6239
  try {
6215
6240
  const result = await integrationManager2.callTool(tool.integrationId, tool.originalName, outgoingArgs);
6216
6241
  state2.executionJournal.complete(receiptId, "done");
6242
+ recordAutomaticOutcome(state2, receiptId, result);
6217
6243
  return result;
6218
6244
  } catch (err) {
6219
6245
  state2.executionJournal.complete(receiptId, "failed");
6246
+ recordAutomaticOutcome(state2, receiptId, void 0, err);
6220
6247
  throw err;
6221
6248
  }
6222
6249
  }
@@ -6224,6 +6251,20 @@ Proposal ID: ${proposal.id}. Use check-pending-commitments to track status.` + a
6224
6251
  }
6225
6252
  });
6226
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
+ }
6227
6268
  function noMandateRefusal(toolName, profile) {
6228
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.`;
6229
6270
  }
@@ -8274,7 +8315,14 @@ var IntegrationManager = class {
8274
8315
  getToolDisplay() {
8275
8316
  const out = {};
8276
8317
  for (const t of this.getAllTools()) {
8277
- out[t.namespacedName] = { inputSchema: t.inputSchema, ...t.gating?.approvalView ? { approvalView: t.gating.approvalView } : {} };
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
+ };
8278
8326
  }
8279
8327
  return out;
8280
8328
  }
@@ -9999,6 +10047,52 @@ function isAlive(pid) {
9999
10047
  }
10000
10048
  }
10001
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
+
10002
10096
  // bin/http.ts
10003
10097
  import { homedir as homedir14 } from "os";
10004
10098
  import { join as join23 } from "path";
@@ -10535,6 +10629,35 @@ app.get("/internal/evidence", internalOnly, (_req, res) => {
10535
10629
  gates: state.gateStore.getAll()
10536
10630
  });
10537
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
+ });
10538
10661
  function reportScope() {
10539
10662
  return scopeReportSources(reportSources, {
10540
10663
  authorizations: state.getEnrichedAuthorizations(),