@suveren/gateway 0.18.1 → 0.19.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.
@@ -1,22 +1,36 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
- METRIC_LABELS,
3
+ CASE_TIME_UNKNOWN_REASON,
4
+ REPORT_BRIEF,
5
+ REPORT_ELEMENTS,
6
+ START_TIME_METRICS,
7
+ agentApproval,
8
+ agentMandate,
9
+ agentTicket,
10
+ agentTicketRow,
11
+ approvalFields,
12
+ buildExportBundle,
13
+ buildExportDocument,
14
+ checkedValueLine,
15
+ computeMetric,
4
16
  fingerprintOf,
5
- formatActionLabel,
6
- formatCurrency,
7
17
  formatDateTime,
8
- formatDuration,
9
- formatMetricValue,
18
+ glossaryUsage,
19
+ mandateFields,
10
20
  parseCaseAttrs,
11
21
  parseElements,
12
22
  parseTimestampSeconds,
13
23
  profileShortLabel,
14
24
  readPairing,
25
+ renderReportHtml,
15
26
  resolveApprovalElement,
16
27
  resolveCaseElement,
17
28
  resolveMandateElement,
18
- resolveTicketElement
19
- } from "./chunk-JUFH4SKS.mjs";
29
+ resolveTicketElement,
30
+ sanitizeReport,
31
+ suggestedFilename,
32
+ ticketFields
33
+ } from "./chunk-ZRRZFGRL.mjs";
20
34
 
21
35
  // bin/http.ts
22
36
  import { randomUUID as randomUUID2 } from "crypto";
@@ -1677,8 +1691,8 @@ var ReceiptArchive = class {
1677
1691
  */
1678
1692
  record(entry, now = Math.floor(Date.now() / 1e3)) {
1679
1693
  let changed = false;
1680
- const receiptId2 = typeof entry.receipt?.id === "string" ? entry.receipt.id : void 0;
1681
- const alreadyArchived = receiptId2 !== void 0 && this.data.receipts.some((r) => r.receipt.id === receiptId2);
1694
+ const receiptId = typeof entry.receipt?.id === "string" ? entry.receipt.id : void 0;
1695
+ const alreadyArchived = receiptId !== void 0 && this.data.receipts.some((r) => r.receipt.id === receiptId);
1682
1696
  if (!alreadyArchived) {
1683
1697
  this.data.receipts.push({
1684
1698
  archivedAt: now,
@@ -2083,115 +2097,6 @@ import { homedir as homedir7 } from "os";
2083
2097
  import { dirname as dirname8, join as join10 } from "path";
2084
2098
  import { createCipheriv as createCipheriv5, createDecipheriv as createDecipheriv5, randomBytes as randomBytes5 } from "crypto";
2085
2099
 
2086
- // src/lib/report-brief.ts
2087
- var REPORT_ELEMENTS = [
2088
- "sv-ticket",
2089
- "sv-approval",
2090
- "sv-mandate",
2091
- "sv-record",
2092
- "sv-case",
2093
- "sv-metric"
2094
- ];
2095
- var REPORT_BRIEF = `How to write the report
2096
-
2097
- Purpose: show the company's management what the AI achieved in this test \u2014 case by case, from the request that started it to the goal that completed it \u2014 with proof for every fact that matters.
2098
-
2099
- Who reads it: managers who are not technical. Plain words, short sentences, the result first.
2100
-
2101
- What you write: one HTML page (HTML, CSS and inline SVG only \u2014 no JavaScript, no external files or links except the check links the gateway adds). The layout, headings, charts, tables and your assessment are yours.
2102
-
2103
- It must read well everywhere: on a phone, in a narrow side panel (about 360 pixels wide) and on a large screen. Use fluid widths (no fixed page widths), let columns stack on narrow screens (CSS grid/flex with wrapping, or media queries), make wide tables scroll horizontally or turn into stacked rows, give SVG charts a viewBox so they scale, and keep text at a readable size.
2104
-
2105
- What you cannot write yourself: facts that need proof. Place these elements instead; the gateway looks the data up, checks it and draws the element. You give references, never the content:
2106
-
2107
- - <sv-ticket ref="TICKET_ID"></sv-ticket> \u2014 one action you took, with its signed ticket.
2108
- - <sv-approval ticket="TICKET_ID"></sv-approval> \u2014 who approved that action, when, and how long it waited.
2109
- - <sv-mandate ticket="TICKET_ID"></sv-mandate> \u2014 the authority the action ran under: limits, mode, intent.
2110
- - <sv-record system="email|crm|erp" ref="RECORD_ID"></sv-record> \u2014 an email, CRM entry, quote or order.
2111
- - <sv-case start="email:MESSAGE_ID" goal="ticket:TICKET_ID" steps="TICKET_ID TICKET_ID"></sv-case> \u2014 one business case: the incoming email that started it, the ticket that completed it, and the tickets in between.
2112
- - <sv-metric kind="KIND" cases="all"></sv-metric> \u2014 a number the gateway computes over the cases you defined. KIND: completed, median-time, average-time, without-approval, approvals, median-approval-wait, tickets, refusals. cases is "all" or a space-separated list of case ids, e.g. cases="C1 C3".
2113
-
2114
- Rules:
2115
- 1. Define every case you worked on with sv-case \u2014 also the ones that did not reach their goal (omit goal, or say why in your text). The gateway shows management how many of the loaded cases your report covers and names the missing ones \u2014 and lists every ticket from the test period that your report does not reference, so name every step.
2116
- 2. Use sv-metric for every headline number. Numbers you compute yourself are shown as "AI analysis \u2014 not verified".
2117
- 3. A case's goal must be what actually completed it \u2014 for an email reply, the reply to the start email. The gateway checks this link.
2118
- 4. A wrong or unknown reference is shown as "not verifiable". Do not guess IDs \u2014 read them first.
2119
- 5. Never claim more than the elements prove. Say plainly what went wrong or waited for a person.
2120
-
2121
- Suggested structure:
2122
- 1. The result in one sentence, then 3\u20134 sv-metric figures (completed, median time, share without approval, approvals).
2123
- 2. All cases at a glance \u2014 a table with one row per case, linked to its sv-case below.
2124
- 3. Each case: sv-case, then the sv-ticket and sv-approval elements that explain it.
2125
- 4. What was stopped or needed a person, and why (sv-approval, refusals).
2126
- 5. Your assessment: what worked, what slowed things down, what a different mandate would change \u2014 marked as your view.
2127
-
2128
- Update the report whenever cases progress; each write replaces the previous report.`;
2129
-
2130
- // src/lib/report/sanitize.ts
2131
- import sanitizeHtmlLib from "sanitize-html";
2132
- var FORBIDDEN_TAGS = /* @__PURE__ */ new Set(["script", "iframe", "object", "embed", "link"]);
2133
- function isUrlAttribute(name) {
2134
- const n = name.toLowerCase();
2135
- return n === "href" || n === "src" || n === "xlink:href" || n.endsWith(":href");
2136
- }
2137
- function isAllowedUrl(value) {
2138
- const v = value.trim();
2139
- if (v.startsWith("#")) return true;
2140
- if (/^data:image\//i.test(v)) return true;
2141
- return false;
2142
- }
2143
- function sanitizeReportHtml(html) {
2144
- return sanitizeHtmlLib(html, {
2145
- // Allow every tag/attribute by default — see module doc comment for why
2146
- // this is deny-by-name rather than allow-by-name. `transformTags['*']`
2147
- // below is then the ONLY attribute filter (allowedAttributes: false
2148
- // skips sanitize-html's own, so there is exactly one place attributes
2149
- // are decided, not two that could disagree). Logs a harmless warning for
2150
- // "script"/"style" on every call (sanitize-html's own vulnerable-tag
2151
- // notice, fired from its internal tag list even though we pass `false`,
2152
- // not an array containing them) — `allowVulnerableTags` is the
2153
- // documented way to accept that, and <script> is still removed, by
2154
- // `exclusiveFilter` below, regardless of this flag.
2155
- allowedTags: false,
2156
- allowedAttributes: false,
2157
- allowVulnerableTags: true,
2158
- // sanitize-html's OWN scheme check (independent of transformTags, and
2159
- // applied to it/src/cite only — NOT xlink:href, which is why the
2160
- // transformTags filter above is the one actually relied on for SVG) uses
2161
- // this list; it has no "data" scheme by default, which would undo our
2162
- // own decision to keep `data:image/*` sources. Nothing wider is opened:
2163
- // transformTags has already removed every href/src that isn't a
2164
- // fragment link or a data:image/* URI by the time this check runs.
2165
- allowedSchemes: [...sanitizeHtmlLib.defaults.allowedSchemes, "data"],
2166
- // Preserve attribute name casing — inline SVG uses camelCase attributes
2167
- // (viewBox, preserveAspectRatio, …) that lowercasing would silently
2168
- // break. Safe for the `on*`/url checks below: both compare case-folded.
2169
- parser: { lowerCaseAttributeNames: false },
2170
- // Removes the named tag AND all of its children/text — the only correct
2171
- // behaviour for <script>: dropping just the tag would leave its JS as
2172
- // visible text, but the content must not survive either.
2173
- exclusiveFilter: (frame) => {
2174
- const tag = frame.tag.toLowerCase();
2175
- if (FORBIDDEN_TAGS.has(tag)) return true;
2176
- if (tag === "meta" && Object.keys(frame.attribs).some((a) => a.toLowerCase() === "http-equiv")) {
2177
- return true;
2178
- }
2179
- return false;
2180
- },
2181
- transformTags: {
2182
- "*": (tagName, attribs) => {
2183
- const kept = {};
2184
- for (const [name, value] of Object.entries(attribs)) {
2185
- if (name.toLowerCase().startsWith("on")) continue;
2186
- if (isUrlAttribute(name) && !isAllowedUrl(value)) continue;
2187
- kept[name] = value;
2188
- }
2189
- return { tagName, attribs: kept };
2190
- }
2191
- }
2192
- });
2193
- }
2194
-
2195
2100
  // src/lib/report/record-resolvers.ts
2196
2101
  function findById(rows, id) {
2197
2102
  return rows.find((r) => r.id === id);
@@ -2240,64 +2145,6 @@ function resolveRecord(system, exportData, ref) {
2240
2145
  return resolveCrmRecord(exportData, ref);
2241
2146
  }
2242
2147
 
2243
- // src/lib/report/metric-resolvers.ts
2244
- var METRIC_KINDS = [
2245
- "completed",
2246
- "median-time",
2247
- "average-time",
2248
- "without-approval",
2249
- "approvals",
2250
- "median-approval-wait",
2251
- "tickets",
2252
- "refusals"
2253
- ];
2254
- var UnknownMetricKindError = class extends Error {
2255
- constructor(kind) {
2256
- super(`Unknown sv-metric kind "${kind}" \u2014 expected one of: ${METRIC_KINDS.join(", ")}.`);
2257
- this.name = "UnknownMetricKindError";
2258
- }
2259
- };
2260
- function median(values) {
2261
- if (values.length === 0) return 0;
2262
- const sorted = [...values].sort((a, b) => a - b);
2263
- const mid = Math.floor(sorted.length / 2);
2264
- return sorted.length % 2 === 0 ? (sorted[mid - 1] + sorted[mid]) / 2 : sorted[mid];
2265
- }
2266
- function average(values) {
2267
- if (values.length === 0) return 0;
2268
- return values.reduce((a, b) => a + b, 0) / values.length;
2269
- }
2270
- function computeMetric(kind, cases, refusalTimes = []) {
2271
- switch (kind) {
2272
- case "completed":
2273
- return cases.length;
2274
- case "median-time":
2275
- return median(cases.map((c) => c.totalDurationSeconds));
2276
- case "average-time":
2277
- return average(cases.map((c) => c.totalDurationSeconds));
2278
- case "without-approval": {
2279
- if (cases.length === 0) return 0;
2280
- const withoutApproval = cases.filter((c) => c.approvals.length === 0).length;
2281
- return withoutApproval / cases.length;
2282
- }
2283
- case "approvals":
2284
- return cases.reduce((sum, c) => sum + c.approvals.length, 0);
2285
- case "median-approval-wait":
2286
- return median(cases.flatMap((c) => c.approvals.map((a) => a.waitSeconds).filter((w) => typeof w === "number")));
2287
- case "tickets": {
2288
- const ids = new Set(cases.flatMap((c) => c.ticketIds));
2289
- return ids.size;
2290
- }
2291
- case "refusals":
2292
- return cases.reduce(
2293
- (sum, c) => sum + refusalTimes.filter((t) => t >= c.startTime && t <= c.goalTime).length,
2294
- 0
2295
- );
2296
- default:
2297
- throw new UnknownMetricKindError(kind);
2298
- }
2299
- }
2300
-
2301
2148
  // src/lib/report/types.ts
2302
2149
  function isEmailExport(v) {
2303
2150
  const o = v;
@@ -2315,7 +2162,7 @@ function isCrmExport(v) {
2315
2162
  // src/lib/report/verify-report.ts
2316
2163
  var KNOWN_KINDS = new Set(REPORT_ELEMENTS);
2317
2164
  async function verifyReport(html, sources) {
2318
- const sanitized = sanitizeReportHtml(html);
2165
+ const { html: sanitized, notes: sanitizeNotes } = sanitizeReport(html);
2319
2166
  const parsed = parseElements(sanitized);
2320
2167
  const exportCache = /* @__PURE__ */ new Map();
2321
2168
  function getExport(system) {
@@ -2336,7 +2183,16 @@ async function verifyReport(html, sources) {
2336
2183
  try {
2337
2184
  switch (p.kind) {
2338
2185
  case "sv-ticket": {
2339
- const r = await resolveTicketElement(sources.archive, p.attrs.ref ?? "");
2186
+ const ref = p.attrs.ref ?? "";
2187
+ const r = await resolveTicketElement(sources.archive, ref);
2188
+ if (r.status === "verified" && (p.attrs.variant ?? "").trim().toLowerCase() === "full") {
2189
+ const m = await resolveMandateElement(sources.archive, ref);
2190
+ const a = await resolveApprovalElement(sources.archive, ref);
2191
+ const data = { ...r.data };
2192
+ if (m.status === "verified") data.mandate = m.data;
2193
+ if (a.status === "verified") data.approval = a.data;
2194
+ return { id: p.id, kind: p.kind, attrs: p.attrs, status: r.status, data };
2195
+ }
2340
2196
  return { id: p.id, kind: p.kind, attrs: p.attrs, ...r };
2341
2197
  }
2342
2198
  case "sv-approval": {
@@ -2435,9 +2291,13 @@ async function verifyReport(html, sources) {
2435
2291
  });
2436
2292
  continue;
2437
2293
  }
2294
+ if (START_TIME_METRICS.has(kind) && caseInputs.some((c) => c.totalDurationSeconds === null)) {
2295
+ elements.push({ id: p.id, kind: p.kind, attrs: p.attrs, status: "unverifiable", reason: CASE_TIME_UNKNOWN_REASON });
2296
+ continue;
2297
+ }
2438
2298
  const refusalTimes = kind === "refusals" ? await collectRefusalTimes(getExport) : [];
2439
2299
  const value = computeMetric(kind, caseInputs, refusalTimes);
2440
- const data = { kind, cases: casesAttr, value, caseCount: verifiedCount };
2300
+ const data = { kind, cases: casesAttr, value, caseCount: verifiedCount, caseIds: caseInputs.map((c) => c.caseId) };
2441
2301
  if (verifiedCount < requestedCount) {
2442
2302
  elements.push({
2443
2303
  id: p.id,
@@ -2455,8 +2315,8 @@ async function verifyReport(html, sources) {
2455
2315
  }
2456
2316
  }
2457
2317
  const proof = buildProof(elements, caseElements);
2458
- const coverage = await buildCoverage(caseElements, getExport, sources.archive, proof.ticketsReferenced);
2459
- return { html: sanitized, elements, proof, coverage };
2318
+ const coverage = await buildCoverage(caseElements, getExport, sources.archive, proof.ticketsReferenced, sources.window);
2319
+ return { html: sanitized, elements, proof, coverage, sanitizeNotes };
2460
2320
  }
2461
2321
  async function collectRefusalTimes(getExport) {
2462
2322
  const times = [];
@@ -2472,37 +2332,7 @@ async function collectRefusalTimes(getExport) {
2472
2332
  return times;
2473
2333
  }
2474
2334
  function summarize(el) {
2475
- const d = el.data ?? {};
2476
- switch (el.kind) {
2477
- case "sv-ticket": {
2478
- const actionLabel = typeof d.actionLabel === "string" ? d.actionLabel : formatActionLabel(d.action);
2479
- const timeLabel = typeof d.timeLabel === "string" ? d.timeLabel : formatDateTime(d.time);
2480
- return `${actionLabel} \u2014 ${timeLabel}`;
2481
- }
2482
- case "sv-approval": {
2483
- const who = typeof d.whoLabel === "string" ? d.whoLabel : d.who?.join(", ") ?? "unknown";
2484
- const waitLabel = typeof d.waitLabel === "string" ? d.waitLabel : typeof d.waitSeconds === "number" ? formatDuration(d.waitSeconds) : "unknown";
2485
- return `Approval by ${who} \u2014 waited ${waitLabel}`;
2486
- }
2487
- case "sv-mandate": {
2488
- const profileLabel = typeof d.profileLabel === "string" ? d.profileLabel : profileShortLabel(typeof d.profile === "string" ? d.profile : void 0);
2489
- const owners = Array.isArray(d.owners) && d.owners.length > 0 ? d.owners.join(", ") : "unknown owner";
2490
- return `Mandate: ${profileLabel} \xB7 ${owners}`;
2491
- }
2492
- case "sv-record":
2493
- return `${String(d.kind ?? "record")} record checked`;
2494
- case "sv-case": {
2495
- const steps = d.steps?.length ?? 0;
2496
- return `Case ${String(d.caseId)}: ${steps} ${steps === 1 ? "step" : "steps"}, ${formatDuration(d.totalDurationSeconds)}`;
2497
- }
2498
- case "sv-metric": {
2499
- const kind = String(d.kind ?? "");
2500
- const label = METRIC_LABELS[kind] ?? kind;
2501
- return `${label} = ${formatMetricValue(kind, d.value)}`;
2502
- }
2503
- default:
2504
- return el.id;
2505
- }
2335
+ return checkedValueLine(el);
2506
2336
  }
2507
2337
  function buildProof(elements, caseElements) {
2508
2338
  const ticketsReferenced = /* @__PURE__ */ new Set();
@@ -2543,7 +2373,7 @@ function buildProof(elements, caseElements) {
2543
2373
  verifiedValues
2544
2374
  };
2545
2375
  }
2546
- async function buildCoverage(caseElements, getExport, archive, referenced) {
2376
+ async function buildCoverage(caseElements, getExport, archive, referenced, window) {
2547
2377
  const exp = await getExport("email");
2548
2378
  const emailExportError = !exp.ok ? exp.reason : !isEmailExport(exp.data) ? "Email simulator export had an unexpected shape." : void 0;
2549
2379
  const loadedCases = exp.ok && isEmailExport(exp.data) ? [...new Set(exp.data.inbox.map((m) => m.case_id).filter((c) => !!c))] : [];
@@ -2553,7 +2383,8 @@ async function buildCoverage(caseElements, getExport, archive, referenced) {
2553
2383
  const coveredCases = loadedCases.filter((c) => covered.has(c));
2554
2384
  const missingCases = loadedCases.filter((c) => !covered.has(c));
2555
2385
  const loadedAt = exp.ok && isEmailExport(exp.data) ? exp.data.simulation_load?.loaded_at : void 0;
2556
- const periodStart = loadedAt !== void 0 ? parseTimestampSeconds(loadedAt) ?? null : null;
2386
+ const loadedAtSeconds = loadedAt !== void 0 ? parseTimestampSeconds(loadedAt) ?? null : null;
2387
+ const periodStart = window ? loadedAtSeconds !== null ? Math.max(window.start, loadedAtSeconds) : window.start : loadedAtSeconds;
2557
2388
  const inPeriod = archive.getReceipts().map((r) => ({ id: String(r.receipt.id ?? ""), time: Number(r.receipt.timestamp ?? r.archivedAt) })).filter((t) => t.id && (periodStart === null || t.time >= periodStart)).sort((a, b) => a.time - b.time).map((t) => t.id);
2558
2389
  const referencedSet = new Set(referenced);
2559
2390
  const ticketsReferenced = inPeriod.filter((id) => referencedSet.has(id));
@@ -2564,6 +2395,7 @@ async function buildCoverage(caseElements, getExport, archive, referenced) {
2564
2395
  coveredCases,
2565
2396
  missingCases,
2566
2397
  periodStart,
2398
+ ...window ? { window: { start: window.start, end: window.end, days: window.days, label: window.label } } : {},
2567
2399
  ticketsInPeriod: inPeriod,
2568
2400
  ticketsReferenced,
2569
2401
  ticketsNotReferenced
@@ -3015,8 +2847,8 @@ var FOOTER_RE = /\n*(?:—|--) (?:Sent|Published) by (?:an AI agent|[^\n]+?'s AI
3015
2847
  function shouldAttachFooter() {
3016
2848
  return true;
3017
2849
  }
3018
- function receiptUrl(receiptId2) {
3019
- return `${AS_BASE}/r/${receiptId2}`;
2850
+ function receiptUrl(receiptId) {
2851
+ return `${AS_BASE}/r/${receiptId}`;
3020
2852
  }
3021
2853
  function verbForProfile(profile) {
3022
2854
  return profile === "publish" ? "Published" : "Sent";
@@ -3024,11 +2856,11 @@ function verbForProfile(profile) {
3024
2856
  function hapLine() {
3025
2857
  return `-- ${OPERATOR_NAME} implements HAP, the open Human Agency Protocol: ${HAP_URL}`;
3026
2858
  }
3027
- function footerText(receiptId2, verb, subject) {
2859
+ function footerText(receiptId, verb, subject) {
3028
2860
  const identity = deriveIdentityLine(subject, { operatorName: OPERATOR_NAME }).replace(/^Sent\b/, verb);
3029
2861
  return `
3030
2862
 
3031
- -- ${identity}. Receipt: ${receiptUrl(receiptId2)}
2863
+ -- ${identity}. Receipt: ${receiptUrl(receiptId)}
3032
2864
 
3033
2865
  ${hapLine()}`;
3034
2866
  }
@@ -3047,13 +2879,13 @@ function detectContentField(tool) {
3047
2879
  function stripFooter(value) {
3048
2880
  return value.replace(FOOTER_RE, "").replace(/\n+$/, "");
3049
2881
  }
3050
- function appendVerificationFooter(tool, args, receiptId2, subject) {
2882
+ function appendVerificationFooter(tool, args, receiptId, subject) {
3051
2883
  const profile = tool.gating?.profile;
3052
2884
  if (!profile || !FOOTER_PROFILES.has(profile)) return args;
3053
2885
  const field = detectContentField(tool);
3054
2886
  if (!field) return args;
3055
2887
  const current = typeof args[field] === "string" ? args[field] : "";
3056
- return { ...args, [field]: stripFooter(current) + footerText(receiptId2, verbForProfile(profile), subject) };
2888
+ return { ...args, [field]: stripFooter(current) + footerText(receiptId, verbForProfile(profile), subject) };
3057
2889
  }
3058
2890
 
3059
2891
  // src/lib/content-binding.ts
@@ -3106,10 +2938,10 @@ function computeContentBinding(profileId, tool, toolArgs, actionType) {
3106
2938
  boundContent
3107
2939
  };
3108
2940
  }
3109
- function attachReceiptId(tool, args, receiptId2) {
2941
+ function attachReceiptId(tool, args, receiptId) {
3110
2942
  const schema = tool.inputSchema;
3111
2943
  if (!schema?.properties || !("receipt_id" in schema.properties)) return args;
3112
- return { ...args, receipt_id: receiptId2 };
2944
+ return { ...args, receipt_id: receiptId };
3113
2945
  }
3114
2946
 
3115
2947
  // src/lib/ticket-verify.ts
@@ -3660,6 +3492,25 @@ function createGatedToolHandlerInner(tool, integrationManager2, state2) {
3660
3492
  };
3661
3493
  }
3662
3494
  const { profile, executionMapping, staticExecution, category } = tool.gating;
3495
+ const eligibleMandates = () => {
3496
+ const refusals = [];
3497
+ const matching = state2.getEnrichedAuthorizations().filter((a) => a.complete && profileMatches(a.profileId, profile)).filter((a) => {
3498
+ const refusal = integrationManager2.mandateRefusal?.(tool, a) ?? null;
3499
+ if (refusal) refusals.push(refusal);
3500
+ return !refusal;
3501
+ });
3502
+ return { matching, refusals };
3503
+ };
3504
+ const noEligibleMandate = (refusals) => {
3505
+ const reasons = [...new Set(refusals)].join(" ");
3506
+ return {
3507
+ content: [{
3508
+ type: "text",
3509
+ text: refusals.length > 0 ? `Refused: ${reasons.charAt(0).toUpperCase() + reasons.slice(1)} No ticket was requested.` : `No active authorization matching profile "${profile}". A decision owner must grant authority via the Authority UI before this tool can be used.`
3510
+ }],
3511
+ isError: true
3512
+ };
3513
+ };
3663
3514
  const lockedGuard = (inner) => async (args) => {
3664
3515
  if (!state2.spClient.isUnlocked()) {
3665
3516
  return {
@@ -3692,19 +3543,8 @@ function createGatedToolHandlerInner(tool, integrationManager2, state2) {
3692
3543
  `${tool.originalName} declares no read governance (no static gate, no read adapter, no explicit exemption), so the gateway cannot bound what it returns. This is a manifest defect \u2014 the tool must declare how its reads are limited before it can be used.`
3693
3544
  );
3694
3545
  }
3695
- const auths = state2.getEnrichedAuthorizations();
3696
- const matchingAuths = auths.filter(
3697
- (a) => a.complete && profileMatches(a.profileId, profile)
3698
- );
3699
- if (matchingAuths.length === 0) {
3700
- return {
3701
- content: [{
3702
- type: "text",
3703
- text: `No active authorization matching profile "${profile}". A decision owner must grant authority via the Authority UI before this tool can be used.`
3704
- }],
3705
- isError: true
3706
- };
3707
- }
3546
+ const { matching: matchingAuths, refusals } = eligibleMandates();
3547
+ if (matchingAuths.length === 0) return noEligibleMandate(refusals);
3708
3548
  if (readGate.boundField) {
3709
3549
  const permitted = matchingAuths.some(
3710
3550
  (a) => boundsSatisfyReadGate(a.bounds ?? a.frame, readGate)
@@ -3886,19 +3726,8 @@ function createGatedToolHandlerInner(tool, integrationManager2, state2) {
3886
3726
  };
3887
3727
  }
3888
3728
  }
3889
- const auths = state2.getEnrichedAuthorizations();
3890
- const matchingAuths = auths.filter(
3891
- (a) => a.complete && profileMatches(a.profileId, profile)
3892
- );
3893
- if (matchingAuths.length === 0) {
3894
- return {
3895
- content: [{
3896
- type: "text",
3897
- text: `No active authorization matching profile "${profile}". A decision owner must grant authority via the Authority UI before this tool can be used.`
3898
- }],
3899
- isError: true
3900
- };
3901
- }
3729
+ const { matching: matchingAuths, refusals } = eligibleMandates();
3730
+ if (matchingAuths.length === 0) return noEligibleMandate(refusals);
3902
3731
  const errors = [];
3903
3732
  const passers = [];
3904
3733
  for (const candidate of matchingAuths) {
@@ -3999,7 +3828,7 @@ Proposal ID: ${proposal.id}. Check status with check-pending-commitments(proposa
3999
3828
  };
4000
3829
  }
4001
3830
  }
4002
- let receiptId2;
3831
+ let receiptId;
4003
3832
  try {
4004
3833
  const actionType = declaredActionType;
4005
3834
  const binding = computeContentBinding(auth.profileId, tool, args, actionType);
@@ -4021,7 +3850,7 @@ Proposal ID: ${proposal.id}. Check status with check-pending-commitments(proposa
4021
3850
  // field by field rather than spread.
4022
3851
  ...binding ? { contentHash: binding.contentHash, contentBinding: binding.contentBinding } : {}
4023
3852
  });
4024
- receiptId2 = typeof receipt?.id === "string" ? receipt.id : void 0;
3853
+ receiptId = typeof receipt?.id === "string" ? receipt.id : void 0;
4025
3854
  await verifyTicket(state2.cache, receipt, {
4026
3855
  action: tool.namespacedName,
4027
3856
  executionContext: { ...execution },
@@ -4175,12 +4004,12 @@ Proposal ID: ${proposal.id}. Use check-pending-commitments to track status.`
4175
4004
  execution: { ...execution },
4176
4005
  timestamp: Math.floor(Date.now() / 1e3)
4177
4006
  });
4178
- let outgoingArgs = shouldAttachFooter() && receiptId2 ? appendVerificationFooter(tool, args, receiptId2, auth.subjects?.[0]) : args;
4179
- if (receiptId2) outgoingArgs = attachReceiptId(tool, outgoingArgs, receiptId2);
4007
+ let outgoingArgs = shouldAttachFooter() && receiptId ? appendVerificationFooter(tool, args, receiptId, auth.subjects?.[0]) : args;
4008
+ if (receiptId) outgoingArgs = attachReceiptId(tool, outgoingArgs, receiptId);
4180
4009
  outgoingArgs = encodeOutgoingArgs(tool, outgoingArgs);
4181
- if (receiptId2) {
4010
+ if (receiptId) {
4182
4011
  const begun = state2.executionJournal.begin({
4183
- ticketId: receiptId2,
4012
+ ticketId: receiptId,
4184
4013
  tool: tool.namespacedName,
4185
4014
  argsHash: hashToolArgs(args)
4186
4015
  });
@@ -4188,17 +4017,17 @@ Proposal ID: ${proposal.id}. Use check-pending-commitments to track status.`
4188
4017
  return {
4189
4018
  content: [{
4190
4019
  type: "text",
4191
- text: `Blocked: ticket ${receiptId2} was already used to execute ${tool.namespacedName} (state: ${begun.existing.state}). Not running it again.`
4020
+ text: `Blocked: ticket ${receiptId} was already used to execute ${tool.namespacedName} (state: ${begun.existing.state}). Not running it again.`
4192
4021
  }],
4193
4022
  isError: true
4194
4023
  };
4195
4024
  }
4196
4025
  try {
4197
4026
  const result = await integrationManager2.callTool(tool.integrationId, tool.originalName, outgoingArgs);
4198
- state2.executionJournal.complete(receiptId2, "done");
4027
+ state2.executionJournal.complete(receiptId, "done");
4199
4028
  return result;
4200
4029
  } catch (err) {
4201
- state2.executionJournal.complete(receiptId2, "failed");
4030
+ state2.executionJournal.complete(receiptId, "failed");
4202
4031
  throw err;
4203
4032
  }
4204
4033
  }
@@ -4679,7 +4508,7 @@ async function executeCommitted(proposal, state2, integrationManager2) {
4679
4508
  };
4680
4509
  }
4681
4510
  }
4682
- let receiptId2;
4511
+ let receiptId;
4683
4512
  let replayed = false;
4684
4513
  try {
4685
4514
  const binding = computeContentBinding(
@@ -4712,7 +4541,7 @@ async function executeCommitted(proposal, state2, integrationManager2) {
4712
4541
  // this machine — see the archive call below.
4713
4542
  ...binding ? { contentHash: binding.contentHash, contentBinding: binding.contentBinding } : {}
4714
4543
  });
4715
- receiptId2 = typeof receipt?.id === "string" ? receipt.id : void 0;
4544
+ receiptId = typeof receipt?.id === "string" ? receipt.id : void 0;
4716
4545
  replayed = idempotent;
4717
4546
  await verifyTicket(state2.cache, receipt, {
4718
4547
  action: proposal.tool,
@@ -4767,14 +4596,14 @@ async function executeCommitted(proposal, state2, integrationManager2) {
4767
4596
  isError: true
4768
4597
  };
4769
4598
  }
4770
- if (!receiptId2) {
4599
+ if (!receiptId) {
4771
4600
  return {
4772
4601
  text: `Proposal ${proposal.id}: the ticket carries no id \u2014 refusing to execute an action that could not be recorded.`,
4773
4602
  isError: true
4774
4603
  };
4775
4604
  }
4776
4605
  const begun = state2.executionJournal.begin({
4777
- ticketId: receiptId2,
4606
+ ticketId: receiptId,
4778
4607
  proposalId: proposal.id,
4779
4608
  tool: proposal.tool,
4780
4609
  argsHash: hashToolArgs(proposal.toolArgs)
@@ -4784,36 +4613,36 @@ async function executeCommitted(proposal, state2, integrationManager2) {
4784
4613
  const when = new Date(prior.startedAt * 1e3).toISOString();
4785
4614
  if (prior.state === "done") {
4786
4615
  return {
4787
- text: `Proposal ${proposal.id} was already executed under ticket ${receiptId2} at ${when}. Not running it again.`
4616
+ text: `Proposal ${proposal.id} was already executed under ticket ${receiptId} at ${when}. Not running it again.`
4788
4617
  };
4789
4618
  }
4790
4619
  return {
4791
- text: `Proposal ${proposal.id}: an execution under ticket ${receiptId2} 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.`,
4620
+ 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.`,
4792
4621
  isError: true
4793
4622
  };
4794
4623
  }
4795
4624
  if (replayed) {
4796
4625
  console.error(
4797
- `[Suveren MCP] Proposal ${proposal.id}: ticket ${receiptId2} was replayed by the AS with no local execution record \u2014 executing once (lost-response recovery).`
4626
+ `[Suveren MCP] Proposal ${proposal.id}: ticket ${receiptId} was replayed by the AS with no local execution record \u2014 executing once (lost-response recovery).`
4798
4627
  );
4799
4628
  }
4800
4629
  try {
4801
4630
  let outgoingArgs = proposal.toolArgs;
4802
- if (discovered && receiptId2) {
4631
+ if (discovered && receiptId) {
4803
4632
  if (shouldAttachFooter()) {
4804
- outgoingArgs = appendVerificationFooter(discovered, outgoingArgs, receiptId2, cachedAuth?.subjects?.[0]);
4633
+ outgoingArgs = appendVerificationFooter(discovered, outgoingArgs, receiptId, cachedAuth?.subjects?.[0]);
4805
4634
  }
4806
- outgoingArgs = attachReceiptId(discovered, outgoingArgs, receiptId2);
4635
+ outgoingArgs = attachReceiptId(discovered, outgoingArgs, receiptId);
4807
4636
  outgoingArgs = encodeOutgoingArgs(discovered, outgoingArgs);
4808
4637
  }
4809
4638
  let result;
4810
4639
  try {
4811
4640
  result = await integrationManager2.callTool(integrationId, toolName, outgoingArgs);
4812
4641
  } catch (err) {
4813
- state2.executionJournal.complete(receiptId2, "failed");
4642
+ state2.executionJournal.complete(receiptId, "failed");
4814
4643
  throw err;
4815
4644
  }
4816
- state2.executionJournal.complete(receiptId2, "done");
4645
+ state2.executionJournal.complete(receiptId, "done");
4817
4646
  state2.executionLog.record({
4818
4647
  profileId: proposal.profileId,
4819
4648
  path: proposal.path,
@@ -5925,6 +5754,15 @@ var IntegrationManager = class {
5925
5754
  const refusal = await def?.validate?.(args);
5926
5755
  return refusal ? `Refused: ${refusal} No ticket was requested.` : null;
5927
5756
  }
5757
+ /**
5758
+ * Why this tool's integration refuses the held mandate `auth`, or null when
5759
+ * it accepts it (always null for connectors and built-ins that declare no
5760
+ * `mandateRefusal`). tool-proxy.ts drops refused mandates before selection.
5761
+ */
5762
+ mandateRefusal(tool, auth) {
5763
+ const builtin = this.builtins.get(tool.integrationId);
5764
+ return builtin?.def.mandateRefusal?.(auth) ?? null;
5765
+ }
5928
5766
  /**
5929
5767
  * Proxy a tool call to a downstream MCP server (or a built-in's handler).
5930
5768
  */
@@ -6258,238 +6096,11 @@ function createConnectorExportRunner(config) {
6258
6096
  };
6259
6097
  }
6260
6098
 
6261
- // src/lib/report/render-report.ts
6262
- import { Parser } from "htmlparser2";
6263
- function findElementSpans(html) {
6264
- const spans = [];
6265
- const seenCount = /* @__PURE__ */ new Map();
6266
- const openStack = [];
6267
- let parserRef;
6268
- const parser = new Parser(
6269
- {
6270
- onparserinit(p) {
6271
- parserRef = p;
6272
- },
6273
- onopentag(name) {
6274
- if (!name.startsWith("sv-") || !parserRef) return;
6275
- const n = seenCount.get(name) ?? 0;
6276
- seenCount.set(name, n + 1);
6277
- openStack.push({ tag: name, id: `${name}-${n}`, start: parserRef.startIndex });
6278
- },
6279
- onclosetag(name) {
6280
- if (!name.startsWith("sv-") || !parserRef) return;
6281
- const idx = [...openStack].reverse().findIndex((o) => o.tag === name);
6282
- if (idx === -1) return;
6283
- const realIdx = openStack.length - 1 - idx;
6284
- const [open] = openStack.splice(realIdx, 1);
6285
- spans.push({ id: open.id, kind: open.tag, start: open.start, end: parserRef.endIndex + 1 });
6286
- }
6287
- },
6288
- { decodeEntities: true, recognizeSelfClosing: true }
6289
- );
6290
- parser.write(html);
6291
- parser.end();
6292
- return spans.sort((a, b) => a.start - b.start);
6293
- }
6294
- function escapeHtml(value) {
6295
- return String(value ?? "").replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
6296
- }
6297
- function detailLink(id, ticketId) {
6298
- const qs = ticketId ? `?element=${encodeURIComponent(id)}&ticket=${encodeURIComponent(ticketId)}` : `?element=${encodeURIComponent(id)}`;
6299
- return `/reports${qs}`;
6300
- }
6301
- function badge(status, okLabel) {
6302
- if (status === "verified") return `<span class="sv-badge sv-badge-ok">\u2713 ${escapeHtml(okLabel)}</span>`;
6303
- if (status === "warning") return `<span class="sv-badge sv-badge-warn">\u26A0 verified with a note</span>`;
6304
- return `<span class="sv-badge sv-badge-bad">\u2717 not verifiable</span>`;
6305
- }
6306
- function unverifiableCard(el) {
6307
- return `<div class="sv-el sv-el-unverifiable" data-sv-id="${escapeHtml(el.id)}"><div class="sv-el-row"><b>Not verifiable</b>${badge("unverifiable", "")}</div><div class="sv-el-reason">${escapeHtml(el.reason ?? "This reference could not be checked.")}</div></div>`;
6308
- }
6309
- function renderTicket(el, interactive) {
6310
- if (el.status === "unverifiable" || !el.data) return unverifiableCard(el);
6311
- const d = el.data;
6312
- const href = typeof d.checkUrl === "string" ? d.checkUrl : void 0;
6313
- const actionLabel = typeof d.actionLabel === "string" ? d.actionLabel : formatActionLabel(d.action);
6314
- const timeLabel = typeof d.timeLabel === "string" ? d.timeLabel : formatDateTime(d.time);
6315
- const profileLabel = typeof d.profileLabel === "string" ? d.profileLabel : escapeHtml(d.profile ?? "");
6316
- const links = [
6317
- href ? `<a href="${escapeHtml(href)}" target="_blank" rel="noopener noreferrer">Check on suveren.ai \u2197</a>` : "",
6318
- interactive ? `<a href="${escapeHtml(detailLink(el.id))}" target="_top">Details</a>` : ""
6319
- ].filter(Boolean);
6320
- return `<div class="sv-el sv-el-${el.status}" data-sv-id="${escapeHtml(el.id)}"><div class="sv-el-row"><b>${escapeHtml(actionLabel)}</b>${badge(el.status, "signature valid")}</div><div class="sv-el-row"><span>${escapeHtml(profileLabel)} \xB7 ${escapeHtml(timeLabel)}</span></div>` + (links.length > 0 ? `<div class="sv-el-row"><span>${links.join(" \xB7 ")}</span></div>` : "") + `</div>`;
6321
- }
6322
- function renderApproval(el, interactive) {
6323
- if (el.status === "unverifiable" || !el.data) return unverifiableCard(el);
6324
- const d = el.data;
6325
- const who = typeof d.whoLabel === "string" ? d.whoLabel : Array.isArray(d.who) ? d.who.join(", ") : "unknown";
6326
- const asked = typeof d.createdAtLabel === "string" ? d.createdAtLabel : "unknown";
6327
- const approved = typeof d.decidedAtLabel === "string" ? d.decidedAtLabel : "unknown";
6328
- const waited = typeof d.waitLabel === "string" ? d.waitLabel : "unknown";
6329
- return `<div class="sv-el sv-el-${el.status}" data-sv-id="${escapeHtml(el.id)}"><div class="sv-el-row"><b>Approval</b>${badge(el.status, "from ticket archive")}</div><div class="sv-el-row"><span>asked ${escapeHtml(asked)} \xB7 approved ${escapeHtml(approved)} by ${escapeHtml(who)} (${escapeHtml(waited)})</span></div>` + (interactive ? `<div class="sv-el-row"><a href="${escapeHtml(detailLink(el.id, String(d.ticketId ?? "")))}" target="_top">Details</a></div>` : "") + `</div>`;
6330
- }
6331
- function renderMandate(el, interactive) {
6332
- if (el.status === "unverifiable" || !el.data) return unverifiableCard(el);
6333
- const d = el.data;
6334
- const profileLabel = typeof d.profileLabel === "string" ? d.profileLabel : String(d.profile ?? "");
6335
- const owners = Array.isArray(d.owners) && d.owners.length > 0 ? d.owners.join(", ") : "unknown owner";
6336
- const limits = Array.isArray(d.limits) && d.limits.length > 0 ? d.limits.join(" \xB7 ") : "";
6337
- return `<div class="sv-el sv-el-${el.status}" data-sv-id="${escapeHtml(el.id)}"><div class="sv-el-row"><b>${escapeHtml(profileLabel)} \xB7 ${escapeHtml(owners)}</b>${badge(el.status, "matches ticket")}</div>` + (limits ? `<div class="sv-el-row">${escapeHtml(limits)}${d.mode ? ` \xB7 ${escapeHtml(d.mode)}` : ""}</div>` : "") + (d.intent ? `<div class="sv-el-intent">Intent: \u201C${escapeHtml(d.intent)}\u201D</div>` : "") + (interactive ? `<div class="sv-el-row"><a href="${escapeHtml(detailLink(el.id))}" target="_top">Details</a></div>` : "") + `</div>`;
6338
- }
6339
- var RECORD_FIELD_ORDER = [
6340
- "subject",
6341
- "from_email",
6342
- "from_name",
6343
- "to_json",
6344
- "received_at",
6345
- "number",
6346
- "status",
6347
- "net_total",
6348
- "customer_id",
6349
- "causingReceiptId",
6350
- "message"
6351
- ];
6352
- var RECORD_TIME_FIELDS = /* @__PURE__ */ new Set(["received_at", "created_at", "sent_at"]);
6353
- function renderRecordField(key, d) {
6354
- if (key === "net_total") {
6355
- const currency = typeof d.currency === "string" ? d.currency : void 0;
6356
- return `<div class="sv-el-row"><span>Value</span><span>${escapeHtml(formatCurrency(d.net_total, currency))}</span></div>`;
6357
- }
6358
- if (RECORD_TIME_FIELDS.has(key)) {
6359
- const t = parseTimestampSeconds(d[key]);
6360
- const display = t !== void 0 ? formatDateTime(t) : d[key];
6361
- return `<div class="sv-el-row"><span>${escapeHtml(key)}</span><span>${escapeHtml(display)}</span></div>`;
6362
- }
6363
- return `<div class="sv-el-row"><span>${escapeHtml(key)}</span><span>${escapeHtml(d[key])}</span></div>`;
6364
- }
6365
- function renderRecord(el) {
6366
- if (el.status === "unverifiable" || !el.data) return unverifiableCard(el);
6367
- const d = el.data;
6368
- const kind = String(d.kind ?? "record");
6369
- const rows = RECORD_FIELD_ORDER.filter((k) => d[k] !== void 0 && d[k] !== null && d[k] !== "").slice(0, 5).map((k) => renderRecordField(k, d)).join("");
6370
- return `<div class="sv-el sv-el-${el.status}" data-sv-id="${escapeHtml(el.id)}"><div class="sv-el-row"><b>${escapeHtml(kind)} record</b>${badge(el.status, "in simulator database")}</div>` + rows + `</div>`;
6371
- }
6372
- function renderCaseTicketStep(caseElementId, node, isGoal, interactive) {
6373
- const cls = isGoal ? "sv-step sv-step-goal" : "sv-step";
6374
- const kind = isGoal ? "goal" : "ticket";
6375
- const inner = `<div class="sv-step-k">${kind}</div><div class="sv-step-t">${escapeHtml(formatActionLabel(node.action))}</div>${formatDateTime(node.time)}`;
6376
- if (!interactive) return `<div class="${cls}">${inner}</div>`;
6377
- return `<a class="${cls}" href="${escapeHtml(detailLink(caseElementId, node.ticketId))}" target="_top">${inner}</a>`;
6378
- }
6379
- function renderCaseApprovalStep(approval) {
6380
- const who = approval.whoLabel ?? (Array.isArray(approval.who) ? approval.who.join(", ") : "unknown");
6381
- const asked = approval.createdAtLabel ?? "unknown";
6382
- const decided = approval.decidedAtLabel ?? "unknown";
6383
- const waited = approval.waitLabel ?? "unknown";
6384
- return `<div class="sv-step sv-step-approval"><div class="sv-step-k">approval</div><div class="sv-step-t">Approval</div><div class="sv-step-sub">asked ${escapeHtml(asked)} \xB7 approved ${escapeHtml(decided)} by ${escapeHtml(who)} (${escapeHtml(waited)})</div></div>`;
6385
- }
6386
- function renderCase(el, interactive) {
6387
- if (el.status === "unverifiable" || !el.data) return unverifiableCard(el);
6388
- const d = el.data;
6389
- const steps = Array.isArray(d.steps) ? d.steps : [];
6390
- const goal = d.goal;
6391
- const approvalsByTicket = /* @__PURE__ */ new Map();
6392
- if (Array.isArray(d.approvals)) {
6393
- for (const a of d.approvals) {
6394
- approvalsByTicket.set(a.ticketId, a);
6395
- }
6396
- }
6397
- const start = d.start;
6398
- const startStep = start ? { time: start.time, html: `<div class="sv-step"><div class="sv-step-k">start</div><div class="sv-step-t">Email in</div>${formatDateTime(start.time)}</div>` } : void 0;
6399
- const ticketNodes = [
6400
- ...steps.map((s) => ({ node: s, isGoal: false })),
6401
- ...goal ? [{ node: goal, isGoal: true }] : []
6402
- ];
6403
- const items = [];
6404
- if (startStep) items.push(startStep);
6405
- for (const { node, isGoal } of ticketNodes) {
6406
- const approval = approvalsByTicket.get(node.ticketId);
6407
- if (approval) {
6408
- const approvalTime = Math.min(approval.decidedAt ?? approval.createdAt ?? node.time, node.time);
6409
- items.push({ time: approvalTime, html: renderCaseApprovalStep(approval) });
6410
- }
6411
- items.push({ time: node.time, html: renderCaseTicketStep(el.id, node, isGoal, interactive) });
6412
- }
6413
- items.sort((a, b) => a.time - b.time);
6414
- const stepHtml = items.map((i) => i.html).join("");
6415
- return `<div class="sv-el sv-el-${el.status}" data-sv-id="${escapeHtml(el.id)}"><div class="sv-el-row"><b>Case ${escapeHtml(d.caseId)} \xB7 ${formatDuration(d.totalDurationSeconds)}</b>${badge(el.status, "assembled by gateway")}</div>` + (el.status === "warning" && el.reason ? `<div class="sv-el-warn">${escapeHtml(el.reason)}</div>` : "") + `<div class="sv-tl">${stepHtml}</div>` + (interactive ? `<div class="sv-el-row"><a href="${escapeHtml(detailLink(el.id))}" target="_top">Case details</a></div>` : "") + `</div>`;
6416
- }
6417
- function renderMetric(el) {
6418
- if (el.status === "unverifiable" || !el.data) return unverifiableCard(el);
6419
- const d = el.data;
6420
- const kind = String(d.kind ?? "");
6421
- const label = METRIC_LABELS[kind] ?? kind;
6422
- const display = formatMetricValue(kind, d.value);
6423
- return `<div class="sv-el sv-el-${el.status} sv-el-metric" data-sv-id="${escapeHtml(el.id)}"><div class="sv-big">${escapeHtml(display)}</div><div class="sv-el-row"><span>${escapeHtml(label)} (${escapeHtml(d.caseCount ?? 0)} case${d.caseCount === 1 ? "" : "s"})</span>${badge(el.status, "computed by gateway")}</div>` + (el.status === "warning" && el.reason ? `<div class="sv-el-warn">${escapeHtml(el.reason)}</div>` : "") + `</div>`;
6424
- }
6425
- function renderElement(el, interactive) {
6426
- switch (el.kind) {
6427
- case "sv-ticket":
6428
- return renderTicket(el, interactive);
6429
- case "sv-approval":
6430
- return renderApproval(el, interactive);
6431
- case "sv-mandate":
6432
- return renderMandate(el, interactive);
6433
- case "sv-record":
6434
- return renderRecord(el);
6435
- case "sv-case":
6436
- return renderCase(el, interactive);
6437
- case "sv-metric":
6438
- return renderMetric(el);
6439
- default:
6440
- return unverifiableCard(el);
6441
- }
6442
- }
6443
- var DRAWN_ELEMENT_STYLES = `
6444
- .sv-el { border:1px solid #e5e5e5; border-radius:10px; padding:10px 12px; margin:10px 0; font:13px/1.5 system-ui, sans-serif; background:#fff; }
6445
- .sv-el-unverifiable { outline:2px solid #b91c1c; outline-offset:2px; }
6446
- .sv-el-verified, .sv-el-warning { outline:2px solid #15803d; outline-offset:2px; }
6447
- .sv-el-row { display:flex; justify-content:space-between; gap:10px; flex-wrap:wrap; margin:2px 0; }
6448
- .sv-el-reason, .sv-el-warn { color:#b45309; font-size:12px; margin-top:4px; }
6449
- .sv-el-intent { color:#666; font-size:12px; margin-top:4px; }
6450
- .sv-badge { display:inline-flex; gap:4px; font-size:10.5px; font-weight:600; padding:1px 7px; border-radius:999px; white-space:nowrap; }
6451
- .sv-badge-ok { color:#15803d; background:#ecfdf3; }
6452
- .sv-badge-warn { color:#b45309; background:#fff7e6; }
6453
- .sv-badge-bad { color:#b91c1c; background:#fdecea; }
6454
- .sv-big { font-size:24px; font-weight:700; }
6455
- .sv-tl { display:flex; flex-wrap:wrap; gap:10px 16px; padding:6px 0; }
6456
- .sv-step { min-width:100px; border:1px solid #e5e5e5; border-radius:8px; padding:6px 8px; font-size:11.5px; text-decoration:none; color:inherit; flex-shrink:0; }
6457
- .sv-step-k { font-size:9.5px; text-transform:uppercase; color:#666; }
6458
- .sv-step-t { font-weight:600; font-size:12px; }
6459
- .sv-step-sub { color:#666; white-space:normal; }
6460
- /* The approval step's text ("asked ... approved ... by ... (duration)") is
6461
- * longer than a ticket step's \u2014 without a width to wrap against, a
6462
- * flex item with flex-shrink:0 takes its one-line content width instead of
6463
- * wrapping, which reads as the text being cut off at the container's visible
6464
- * edge (polish 2026-10-05, second pass). */
6465
- .sv-step-approval { max-width:240px; white-space:normal; }
6466
- @media (max-width: 480px) {
6467
- .sv-tl { flex-direction:column; overflow-x:visible; }
6468
- .sv-step { min-width:0; }
6469
- .sv-step-approval { max-width:none; }
6470
- .sv-el-row { flex-direction:column; gap:2px; }
6471
- }
6472
- `;
6473
- function renderReportHtml(html, elements, interactive = true) {
6474
- const byId = new Map(elements.map((e) => [e.id, e]));
6475
- const spans = findElementSpans(html);
6476
- let out = "";
6477
- let cursor = 0;
6478
- for (const span of spans) {
6479
- out += html.slice(cursor, span.start);
6480
- const el = byId.get(span.id) ?? { id: span.id, kind: span.kind, attrs: {}, status: "unverifiable", reason: "No verification result for this element." };
6481
- out += renderElement(el, interactive);
6482
- cursor = span.end;
6483
- }
6484
- out += html.slice(cursor);
6485
- const styleTag = `<style data-sv-drawn-styles="1">${DRAWN_ELEMENT_STYLES}</style>`;
6486
- if (/<body[^>]*>/i.test(out)) {
6487
- return out.replace(/<body([^>]*)>/i, (m) => `${m}${styleTag}`);
6488
- }
6489
- return styleTag + out;
6490
- }
6491
-
6492
6099
  // src/lib/report/ticket-details.ts
6100
+ function withFields(fact, build) {
6101
+ if (fact.status !== "verified" || !fact.data) return fact;
6102
+ return { ...fact, data: { ...fact.data, fields: build(fact.data) } };
6103
+ }
6493
6104
  async function buildTicketDetails(archive, ticketIds) {
6494
6105
  const out = {};
6495
6106
  for (const id of ticketIds) {
@@ -6498,118 +6109,170 @@ async function buildTicketDetails(archive, ticketIds) {
6498
6109
  resolveApprovalElement(archive, id),
6499
6110
  resolveMandateElement(archive, id)
6500
6111
  ]);
6501
- out[id] = { ticket, approval, mandate };
6112
+ out[id] = {
6113
+ ticket: withFields(ticket, ticketFields),
6114
+ approval: withFields(approval, approvalFields),
6115
+ mandate: withFields(mandate, mandateFields)
6116
+ };
6502
6117
  }
6503
6118
  return out;
6504
6119
  }
6505
6120
 
6506
- // src/lib/report/export-report.ts
6507
- function receiptId(receipt) {
6508
- return typeof receipt.id === "string" ? receipt.id : "";
6509
- }
6510
- function buildExportBundle(params) {
6511
- const { stored, archive, authorityServer, gatewayVersion, now = Math.floor(Date.now() / 1e3) } = params;
6512
- const proof = stored.result.proof;
6513
- const coverage = stored.result.coverage;
6514
- const ticketIdSet = /* @__PURE__ */ new Set([...proof.ticketsReferenced, ...coverage.ticketsInPeriod]);
6515
- const entries = archive.getReceipts().filter((r) => ticketIdSet.has(receiptId(r.receipt)));
6516
- const tickets = entries.map((r) => r.receipt);
6517
- const authorizationIds = new Set(entries.map((r) => r.authorizationId));
6518
- const authorizations = {};
6519
- for (const a of archive.getAuthorizations()) {
6520
- if (authorizationIds.has(a.authorizationId)) authorizations[a.authorizationId] = a;
6121
+ // src/lib/report/window.ts
6122
+ import { getProfile as getProfile6 } from "@hap/core";
6123
+ var REPORT_AGE_FIELD = "read_age_days";
6124
+ var REPORTING_PROFILE = "reporting";
6125
+ var END_SKEW_SECONDS = 120;
6126
+ var OLD_PROFILE_REFUSAL = "this reporting mandate is from an older profile version \u2014 create a new reporting mandate (reporting@0.2).";
6127
+ var DAY = 86400;
6128
+ function boundsOf(a) {
6129
+ return a.bounds ?? a.frame;
6130
+ }
6131
+ function windowLabel(start, days, loadedAt) {
6132
+ const since = `since ${formatDateTime(start)}`;
6133
+ if (loadedAt !== null && loadedAt === start) return `${since} (when the test data was loaded)`;
6134
+ return `${since} (the last ${days} ${days === 1 ? "day" : "days"})`;
6135
+ }
6136
+ var defaultLookup = (id) => getProfile6(id);
6137
+ function reportingMandateRefusal(m, lookup = defaultLookup) {
6138
+ const schema = lookup(m.profileId)?.boundsSchema;
6139
+ const field = resolveAgeBoundField(schema, REPORT_AGE_FIELD);
6140
+ if (!field) return OLD_PROFILE_REFUSAL;
6141
+ if (maxReadAgeDays([boundsOf(m)], field) === null) {
6142
+ return `the reporting mandate does not set ${field} (the reporting window), so nothing can be shown \u2014 create a new reporting mandate.`;
6521
6143
  }
6144
+ return void 0;
6145
+ }
6146
+ function resolveReportWindow(input) {
6147
+ const lookup = input.lookupProfile ?? defaultLookup;
6148
+ const mandates = input.authorizations.filter((a) => a.complete && profileMatches(a.profileId, REPORTING_PROFILE));
6149
+ if (mandates.length === 0) {
6150
+ return { ok: false, reason: "No active reporting mandate \u2014 the reporting window comes from it, so no evidence can be shown." };
6151
+ }
6152
+ const candidates = [];
6153
+ const refusals = [];
6154
+ for (const m of mandates) {
6155
+ const refusal = reportingMandateRefusal(m, lookup);
6156
+ if (refusal) {
6157
+ refusals.push(refusal);
6158
+ continue;
6159
+ }
6160
+ const schema = lookup(m.profileId)?.boundsSchema;
6161
+ const field = resolveAgeBoundField(schema, REPORT_AGE_FIELD);
6162
+ const declared = maxReadAgeDays([boundsOf(m)], field);
6163
+ const def = schema?.fields?.[field] ?? {};
6164
+ const maximum = typeof def.maximum === "number" && Number.isFinite(def.maximum) ? def.maximum : null;
6165
+ const days = Math.max(0, maximum !== null ? Math.min(declared, maximum) : declared);
6166
+ let start = input.now - days * DAY;
6167
+ let clampedBy = null;
6168
+ if (input.simulation && input.loadedAt !== null && input.loadedAt > start) {
6169
+ start = input.loadedAt;
6170
+ clampedBy = input.loadedAt;
6171
+ }
6172
+ candidates.push({ start, days, loadedAt: clampedBy });
6173
+ }
6174
+ if (candidates.length === 0) {
6175
+ const reason = [...new Set(refusals)].join(" ");
6176
+ return { ok: false, reason: reason.charAt(0).toUpperCase() + reason.slice(1) };
6177
+ }
6178
+ const best = candidates.reduce((a, b) => b.start < a.start ? b : a);
6522
6179
  return {
6523
- format: "suveren-report-export",
6524
- version: 1,
6525
- exportedAt: now,
6526
- gatewayVersion,
6527
- report: { html: stored.html, savedAt: stored.savedAt, checkedAt: stored.checkedAt },
6528
- proof,
6529
- coverage,
6530
- authorityServer,
6531
- tickets,
6532
- authorizations
6180
+ ok: true,
6181
+ window: {
6182
+ start: best.start,
6183
+ end: input.now,
6184
+ days: best.days,
6185
+ loadedAt: best.loadedAt,
6186
+ label: windowLabel(best.start, best.days, best.loadedAt)
6187
+ }
6188
+ };
6189
+ }
6190
+ function isInWindow(time, window) {
6191
+ return time !== void 0 && Number.isFinite(time) && time >= window.start && time <= window.end + END_SKEW_SECONDS;
6192
+ }
6193
+ function ticketTime(entry) {
6194
+ const t = entry.receipt.timestamp;
6195
+ return typeof t === "number" && Number.isFinite(t) ? t : void 0;
6196
+ }
6197
+ var OUTSIDE_WINDOW_PREFIX = "not verifiable \u2014 outside the reporting window";
6198
+ function windowArchive(base, window) {
6199
+ const inWindow = (r) => isInWindow(ticketTime(r), window);
6200
+ return {
6201
+ getReceipts() {
6202
+ return base.getReceipts().filter(inWindow);
6203
+ },
6204
+ getAuthorizations() {
6205
+ const used = new Set(base.getReceipts().filter(inWindow).map((r) => r.authorizationId));
6206
+ return base.getAuthorizations().filter((a) => used.has(a.authorizationId));
6207
+ },
6208
+ outsideWindowReason(ticketId) {
6209
+ const entry = base.getReceipts().find((r) => r.receipt.id === ticketId);
6210
+ if (!entry || inWindow(entry)) return void 0;
6211
+ return `${OUTSIDE_WINDOW_PREFIX} (${window.label}).`;
6212
+ }
6213
+ };
6214
+ }
6215
+ var RECORD_TIME_COLUMNS = ["received_at", "created_at", "at", "date"];
6216
+ function recordTime(row, loadedAt, ticketTimes) {
6217
+ if (!row || typeof row !== "object") return void 0;
6218
+ const r = row;
6219
+ if (typeof r.receipt_id === "string" && ticketTimes.has(r.receipt_id)) return ticketTimes.get(r.receipt_id);
6220
+ for (const col of RECORD_TIME_COLUMNS) {
6221
+ const t = parseTimestampSeconds(r[col]);
6222
+ if (t !== void 0) {
6223
+ return loadedAt !== null ? Math.max(t, loadedAt) : t;
6224
+ }
6225
+ }
6226
+ return void 0;
6227
+ }
6228
+ function windowExport(exported, window, loadedAt, ticketTimes = /* @__PURE__ */ new Map()) {
6229
+ if (!exported || typeof exported !== "object" || Array.isArray(exported)) return exported;
6230
+ const out = {};
6231
+ for (const [key, value] of Object.entries(exported)) {
6232
+ out[key] = Array.isArray(value) ? value.filter((row) => isInWindow(recordTime(row, loadedAt, ticketTimes), window)) : value;
6233
+ }
6234
+ return out;
6235
+ }
6236
+ async function readLoadedAt(runExport) {
6237
+ try {
6238
+ const exp = await runExport("email");
6239
+ if (!isEmailExport(exp)) return null;
6240
+ return parseTimestampSeconds(exp.simulation_load?.loaded_at) ?? null;
6241
+ } catch {
6242
+ return null;
6243
+ }
6244
+ }
6245
+ async function scopeReportSources(base, opts) {
6246
+ const now = opts.now ?? Math.floor(Date.now() / 1e3);
6247
+ const hasReporting = opts.authorizations.some((a) => a.complete && profileMatches(a.profileId, REPORTING_PROFILE));
6248
+ const loadedAt = hasReporting && opts.simulation ? await readLoadedAt(base.runExport) : null;
6249
+ const resolved = resolveReportWindow({
6250
+ authorizations: opts.authorizations,
6251
+ simulation: opts.simulation,
6252
+ loadedAt,
6253
+ now,
6254
+ lookupProfile: opts.lookupProfile
6255
+ });
6256
+ if (!resolved.ok) return resolved;
6257
+ return { ok: true, window: resolved.window, sources: scopedSources(base, resolved.window, loadedAt) };
6258
+ }
6259
+ function scopedSources(base, window, loadedAt) {
6260
+ const runExport = async (system) => {
6261
+ const ticketTimes = /* @__PURE__ */ new Map();
6262
+ for (const r of base.archive.getReceipts()) {
6263
+ const id = r.receipt.id;
6264
+ const t = ticketTime(r);
6265
+ if (typeof id === "string" && t !== void 0) ticketTimes.set(id, t);
6266
+ }
6267
+ return windowExport(await base.runExport(system), window, loadedAt, ticketTimes);
6533
6268
  };
6269
+ return { archive: windowArchive(base.archive, window), runExport, window };
6534
6270
  }
6535
- function countLine(count, noneText = "none in this report") {
6536
- return count > 0 ? `${count} \u2713` : noneText;
6537
- }
6538
- function renderProofCoveragePanels(proof, coverage) {
6539
- const casesLine = coverage.emailExportError ? "unknown \u2014 the email simulator could not be read" : `${coverage.coveredCases.length} of ${coverage.loadedCases.length}`;
6540
- const periodNote = coverage.periodStart === null ? `<p class="sv-export-note">${coverage.emailExportError ? "Test period start unknown \u2014 the email simulator could not be read. All saved tickets were counted." : "Test period start unknown \u2014 all saved tickets were counted."}</p>` : "";
6541
- const missingParts = [];
6542
- if (coverage.missingCases.length > 0) missingParts.push(`cases ${coverage.missingCases.map(escapeHtml).join(", ")}`);
6543
- if (coverage.ticketsNotReferenced.length > 0) missingParts.push(`${coverage.ticketsNotReferenced.length} ticket${coverage.ticketsNotReferenced.length === 1 ? "" : "s"}`);
6544
- const missingLine = missingParts.length > 0 ? `<p class="sv-export-missing">Not in the report: ${missingParts.join(" \xB7 ")}</p>` : "";
6545
- const checkedValues = proof.verifiedValues.length > 0 ? `<div class="sv-export-card"><div class="sv-export-card-title">Checked values</div>${proof.verifiedValues.map((v) => `<div class="sv-export-row"><span>${escapeHtml(v.summary)}</span></div>`).join("")}</div>` : "";
6546
- return `<aside class="sv-export-side"><div class="sv-export-card"><div class="sv-export-card-title">Proof</div><div class="sv-export-row"><span>Tickets referenced</span><b>${proof.ticketsReferenced.length}</b></div><div class="sv-export-row"><span>Signatures valid</span><b>${escapeHtml(countLine(proof.signaturesValid))}</b></div><div class="sv-export-row"><span>Records checked</span><b>${escapeHtml(countLine(proof.recordsChecked))}</b></div><div class="sv-export-row"><span>Not verifiable</span><b>${proof.unverifiableCount}</b></div></div><div class="sv-export-card"><div class="sv-export-card-title">Coverage</div><div class="sv-export-row"><span>Cases</span><b>${casesLine}</b></div><div class="sv-export-row"><span>Tickets in test period</span><b>${coverage.ticketsReferenced.length} of ${coverage.ticketsInPeriod.length}</b></div>` + periodNote + missingLine + `</div>` + checkedValues + `</aside>`;
6547
- }
6548
- function ensureHtmlDocument(html) {
6549
- if (/<html[^>]*>/i.test(html)) return html;
6550
- return `<!doctype html><html><head><meta charset="utf-8"></head><body>${html}</body></html>`;
6551
- }
6552
- function injectIntoHead(html, insert) {
6553
- if (/<head[^>]*>/i.test(html)) return html.replace(/<head([^>]*)>/i, (m) => `${m}${insert}`);
6554
- if (/<html[^>]*>/i.test(html)) return html.replace(/<html([^>]*)>/i, (m) => `${m}<head>${insert}</head>`);
6555
- return `<head>${insert}</head>${html}`;
6556
- }
6557
- function injectAfterBodyOpen(html, insert) {
6558
- if (/<body[^>]*>/i.test(html)) return html.replace(/<body([^>]*)>/i, (m) => `${m}${insert}`);
6559
- return `${insert}${html}`;
6560
- }
6561
- function injectBeforeBodyClose(html, insert) {
6562
- if (/<\/body>/i.test(html)) return html.replace(/<\/body>/i, `${insert}</body>`);
6563
- return `${html}${insert}`;
6564
- }
6565
- var EXPORT_PAGE_STYLES = `
6566
- .sv-export-header { font:13px/1.5 system-ui, sans-serif; background:#f6f6f4; border-bottom:1px solid #e5e5e5; padding:14px 20px; }
6567
- .sv-export-meta { margin:0 0 6px 0; font-weight:600; }
6568
- .sv-export-howto { margin:0; color:#444; }
6569
- .sv-export-howto code { background:#eee; border-radius:4px; padding:1px 5px; font-size:12px; }
6570
- .sv-export-layout { display:flex; gap:20px; align-items:flex-start; padding:20px; font:13px/1.5 system-ui, sans-serif; }
6571
- .sv-export-main { flex:1 1 auto; min-width:0; }
6572
- .sv-export-side { flex:0 0 260px; }
6573
- .sv-export-card { border:1px solid #e5e5e5; border-radius:10px; padding:10px 12px; margin-bottom:12px; background:#fff; }
6574
- .sv-export-card-title { font-weight:700; margin-bottom:6px; }
6575
- .sv-export-row { display:flex; justify-content:space-between; gap:10px; margin:2px 0; }
6576
- .sv-export-note, .sv-export-missing { color:#b45309; font-size:12px; margin:4px 0 0 0; }
6577
- @media (max-width: 720px) {
6578
- .sv-export-layout { flex-direction:column; }
6579
- .sv-export-side { flex:1 1 auto; width:100%; }
6580
- }
6581
- `;
6582
- function formatDateTimeWithYear(value) {
6583
- const base = formatDateTime(value);
6584
- const year = new Date(value * 1e3).getFullYear();
6585
- return base.replace(",", ` ${year},`);
6586
- }
6587
- function utcOffsetLabel(value) {
6588
- const offsetMin = -new Date(value * 1e3).getTimezoneOffset();
6589
- const sign = offsetMin >= 0 ? "+" : "-";
6590
- const abs = Math.abs(offsetMin);
6591
- const hh = Math.floor(abs / 60);
6592
- const mm = abs % 60;
6593
- return `UTC${sign}${hh}${mm ? ":" + String(mm).padStart(2, "0") : ""}`;
6594
- }
6595
- function buildExportDocument(params) {
6596
- const { bundle, renderedHtml } = params;
6597
- const exportedDate = new Date(bundle.exportedAt * 1e3).toISOString().slice(0, 10);
6598
- const exportedLabel = formatDateTimeWithYear(bundle.exportedAt);
6599
- const checkedLabel = formatDateTime(bundle.report.checkedAt);
6600
- const tzLabel = utcOffsetLabel(bundle.exportedAt);
6601
- const headerHtml = `<div class="sv-export-header"><p class="sv-export-meta">Suveren Gateway ${escapeHtml(bundle.gatewayVersion)} \u2014 exported ${escapeHtml(exportedLabel)} &middot; checked ${escapeHtml(checkedLabel)} (times in ${escapeHtml(tzLabel)})</p><p class="sv-export-howto">How to check this report: each ticket below links to its public record on suveren.ai ("Check on suveren.ai \u2197"). To verify this entire file offline (including every signature), run <code>suveren-gateway verify-report ${escapeHtml(suggestedFilename(bundle))}</code> from a terminal with the Suveren gateway CLI installed.</p></div>`;
6602
- const proofScriptJson = JSON.stringify(bundle).replace(/<\//g, "<\\/");
6603
- const proofScript = `<script type="application/json" id="suveren-proof">${proofScriptJson}</script>`;
6604
- let doc = ensureHtmlDocument(renderedHtml);
6605
- doc = injectIntoHead(doc, `<meta charset="utf-8"><title>Suveren report export \u2014 ${escapeHtml(exportedDate)}</title><style>${DRAWN_ELEMENT_STYLES}${EXPORT_PAGE_STYLES}</style>`);
6606
- doc = injectAfterBodyOpen(doc, headerHtml + `<div class="sv-export-layout"><div class="sv-export-main">`);
6607
- doc = injectBeforeBodyClose(doc, `</div>${renderProofCoveragePanels(bundle.proof, bundle.coverage)}</div>${proofScript}`);
6608
- return doc;
6609
- }
6610
- function suggestedFilename(bundle) {
6611
- const date = new Date(bundle.exportedAt * 1e3).toISOString().slice(0, 10);
6612
- return `suveren-report-${date}.html`;
6271
+ async function scopeReportSourcesToStoredWindow(base, stored, opts) {
6272
+ const now = opts.now ?? Math.floor(Date.now() / 1e3);
6273
+ const loadedAt = opts.simulation ? await readLoadedAt(base.runExport) : null;
6274
+ const window = { start: stored.start, end: now, days: stored.days, loadedAt: null, label: stored.label };
6275
+ return { ok: true, window, sources: scopedSources(base, window, loadedAt) };
6613
6276
  }
6614
6277
 
6615
6278
  // src/lib/profile-loader.ts
@@ -6680,10 +6343,19 @@ var CRM_RECORD_KINDS = ["contacts", "deals", "tasks", "activities", "changes", "
6680
6343
  function errorText(text) {
6681
6344
  return { content: [{ type: "text", text }], isError: true };
6682
6345
  }
6346
+ function scope(deps) {
6347
+ return scopeReportSources(deps.reportSources, {
6348
+ authorizations: deps.state.getEnrichedAuthorizations(),
6349
+ simulation: isSimulationMode()
6350
+ });
6351
+ }
6352
+ function windowInfo(w) {
6353
+ return { since: w.label, start: w.start, end: w.end };
6354
+ }
6683
6355
  function listTicketsTool(deps) {
6684
6356
  return {
6685
6357
  name: "list_tickets",
6686
- description: "List tickets (signed executions) from the local receipt archive: id, time, action, action type, profile, authorizationId, the limits used, and whether an approval was archived for it. Optionally filter by since/until (unix seconds). Use the ids with get_ticket for full detail, and with <sv-ticket>, <sv-approval>, <sv-mandate> and <sv-case> in the report.",
6358
+ description: "List tickets (signed executions) from the local receipt archive inside your reporting window (the number of days back your reporting mandate allows): id, time, action, action type, profile, the limits used, and whether an approval was archived for it. Older tickets are not shown and cannot be used in the report. Optionally narrow further by since/until (unix seconds). Use the ids with get_ticket for full detail, and with <sv-ticket>, <sv-approval>, <sv-mandate> and <sv-case> in the report.",
6687
6359
  inputSchema: {
6688
6360
  type: "object",
6689
6361
  properties: {
@@ -6692,30 +6364,22 @@ function listTicketsTool(deps) {
6692
6364
  }
6693
6365
  },
6694
6366
  handler: async (args) => {
6367
+ const scoped = await scope(deps);
6368
+ if (!scoped.ok) return errorText(scoped.reason);
6695
6369
  const since = typeof args.since === "number" ? args.since : void 0;
6696
6370
  const until = typeof args.until === "number" ? args.until : void 0;
6697
- const tickets = deps.reportSources.archive.getReceipts().map((entry) => {
6371
+ const tickets = scoped.sources.archive.getReceipts().map((entry) => {
6698
6372
  const r = entry.receipt;
6699
- const time = typeof r.timestamp === "number" ? r.timestamp : void 0;
6700
- return {
6701
- id: typeof r.id === "string" ? r.id : String(r.id ?? ""),
6702
- time,
6703
- action: r.action,
6704
- actionType: r.actionType ?? null,
6705
- profile: r.profileId,
6706
- authorizationId: r.authorizationId,
6707
- limitsUsed: r.limits ?? r.executionContext ?? {},
6708
- hasApproval: Boolean(entry.proposal)
6709
- };
6710
- }).filter((t) => t.id !== "").filter((t) => since === void 0 || t.time !== void 0 && t.time >= since).filter((t) => until === void 0 || t.time !== void 0 && t.time <= until).sort((a, b) => (a.time ?? 0) - (b.time ?? 0));
6711
- return builtinText(JSON.stringify({ tickets }, null, 2));
6373
+ return agentTicketRow(r, Boolean(entry.proposal), profileShortLabel(typeof r.profileId === "string" ? r.profileId : void 0));
6374
+ }).filter((t) => t.id !== "").filter((t) => since === void 0 || typeof t.time === "number" && t.time >= since).filter((t) => until === void 0 || typeof t.time === "number" && t.time <= until).sort((a, b) => (a.time ?? 0) - (b.time ?? 0));
6375
+ return builtinText(JSON.stringify({ window: windowInfo(scoped.window), tickets }, null, 2));
6712
6376
  }
6713
6377
  };
6714
6378
  }
6715
6379
  function getTicketTool(deps) {
6716
6380
  return {
6717
6381
  name: "get_ticket",
6718
- description: 'Full detail for one ticket: the ticket itself (action, time, limits used, public-check link), its archived approval (who approved it, when it was requested and decided, how long it waited), and the mandate it ran under (limits, commitment mode, intent). Each part reports "unverifiable" on its own if it could not be checked \u2014 e.g. an automatic-mode ticket has no approval. Use this to decide what a ticket actually proves before referencing it in the report.',
6382
+ description: 'Full detail for one ticket: the ticket itself (action, time, limits used, public-check link), its archived approval (who approved it, when it was requested and decided, how long it waited), and the mandate it ran under (limits, commitment mode, intent). Each part reports "unverifiable" on its own if it could not be checked \u2014 e.g. an automatic-mode ticket has no approval. Only tickets inside your reporting window can be opened. Use this to decide what a ticket actually proves before referencing it in the report.',
6719
6383
  inputSchema: {
6720
6384
  type: "object",
6721
6385
  properties: { id: { type: "string", description: "The ticket id, as listed by list_tickets." } },
@@ -6724,15 +6388,17 @@ function getTicketTool(deps) {
6724
6388
  handler: async (args) => {
6725
6389
  const id = typeof args.id === "string" ? args.id.trim() : "";
6726
6390
  if (!id) return errorText("id is required.");
6727
- const details = await buildTicketDetails(deps.reportSources.archive, [id]);
6391
+ const scoped = await scope(deps);
6392
+ if (!scoped.ok) return errorText(scoped.reason);
6393
+ const details = await buildTicketDetails(scoped.sources.archive, [id]);
6728
6394
  const detail = details[id];
6729
6395
  if (!detail || detail.ticket.status !== "verified") {
6730
6396
  return errorText(detail?.ticket.reason ?? `No ticket "${id}" in the local archive.`);
6731
6397
  }
6732
6398
  return builtinText(JSON.stringify({
6733
- ticket: detail.ticket.data,
6734
- approval: detail.approval.status === "verified" ? { verified: true, ...detail.approval.data } : { verified: false, reason: detail.approval.reason },
6735
- mandate: detail.mandate.status === "verified" ? { verified: true, ...detail.mandate.data } : { verified: false, reason: detail.mandate.reason }
6399
+ ticket: agentTicket(detail.ticket.data),
6400
+ approval: detail.approval.status === "verified" ? { verified: true, ...agentApproval(detail.approval.data) } : { verified: false, reason: detail.approval.reason },
6401
+ mandate: detail.mandate.status === "verified" ? { verified: true, ...agentMandate(detail.mandate.data) } : { verified: false, reason: detail.mandate.reason }
6736
6402
  }, null, 2));
6737
6403
  }
6738
6404
  };
@@ -6740,12 +6406,14 @@ function getTicketTool(deps) {
6740
6406
  function listCasesTool(deps) {
6741
6407
  return {
6742
6408
  name: "list_cases",
6743
- description: "Loaded business cases from the connected email system, plus every sent mail \u2014 so you can define sv-case elements without guessing ids. Returns: when the test data was loaded (testDataLoadedAt); cases (caseId, the starting inbox message id, sender, subject, received time); and sent mails (id, inReplyTo, the receiptId of the ticket that sent it, subject, sentAt) so you can confirm which ticket closed which case. Never includes the team's own reference replies \u2014 those are not part of this report.",
6409
+ description: "Loaded business cases from the connected email system, plus every sent mail \u2014 so you can define sv-case elements without guessing ids. Returns: when the test data was loaded (testDataLoadedAt); cases (caseId, the starting inbox message id, sender, subject, received time); and sent mails (id, inReplyTo, the receiptId of the ticket that sent it, subject, sentAt) so you can confirm which ticket closed which case. Only records inside your reporting window. Never includes the team's own reference replies \u2014 those are not part of this report.",
6744
6410
  inputSchema: { type: "object", properties: {} },
6745
6411
  handler: async () => {
6412
+ const scoped = await scope(deps);
6413
+ if (!scoped.ok) return errorText(scoped.reason);
6746
6414
  let exported;
6747
6415
  try {
6748
- exported = await deps.reportSources.runExport("email");
6416
+ exported = await scoped.sources.runExport("email");
6749
6417
  } catch (err) {
6750
6418
  return errorText(`Could not read email records: ${err instanceof Error ? err.message : String(err)}`);
6751
6419
  }
@@ -6766,6 +6434,7 @@ function listCasesTool(deps) {
6766
6434
  sentAt: m.received_at
6767
6435
  }));
6768
6436
  return builtinText(JSON.stringify({
6437
+ window: windowInfo(scoped.window),
6769
6438
  testDataLoadedAt: exp.simulation_load?.loaded_at ?? null,
6770
6439
  cases,
6771
6440
  sent
@@ -6781,7 +6450,7 @@ function recordKindsFor(system) {
6781
6450
  function getRecordsTool(deps) {
6782
6451
  return {
6783
6452
  name: "get_records",
6784
- description: `Rows from one connected system (email, erp or crm) that you may reference with <sv-record system="..." ref="...">. Optionally narrow to one table via "kind" (email: inbox/sent/changes/refusals; erp: quotes/orders/changes/refusals; crm: contacts/deals/tasks/activities/changes/refusals) \u2014 omit it to get every table for that system. Never returns the team's own reference replies.`,
6453
+ description: `Rows from one connected system (email, erp or crm) that you may reference with <sv-record system="..." ref="...">. Optionally narrow to one table via "kind" (email: inbox/sent/changes/refusals; erp: quotes/orders/changes/refusals; crm: contacts/deals/tasks/activities/changes/refusals) \u2014 omit it to get every table for that system. Only rows inside your reporting window. Never returns the team's own reference replies.`,
6785
6454
  inputSchema: {
6786
6455
  type: "object",
6787
6456
  properties: {
@@ -6795,9 +6464,11 @@ function getRecordsTool(deps) {
6795
6464
  if (system !== "email" && system !== "erp" && system !== "crm") {
6796
6465
  return errorText('system must be one of "email", "erp", "crm".');
6797
6466
  }
6467
+ const scoped = await scope(deps);
6468
+ if (!scoped.ok) return errorText(scoped.reason);
6798
6469
  let exported;
6799
6470
  try {
6800
- exported = await deps.reportSources.runExport(system);
6471
+ exported = await scoped.sources.runExport(system);
6801
6472
  } catch (err) {
6802
6473
  return errorText(`Could not read ${system} records: ${err instanceof Error ? err.message : String(err)}`);
6803
6474
  }
@@ -6814,11 +6485,11 @@ function getRecordsTool(deps) {
6814
6485
  for (const kind of kinds) {
6815
6486
  records[kind] = exp[kind] ?? [];
6816
6487
  }
6817
- return builtinText(JSON.stringify({ system, records }, null, 2));
6488
+ return builtinText(JSON.stringify({ system, window: windowInfo(scoped.window), records }, null, 2));
6818
6489
  }
6819
6490
  };
6820
6491
  }
6821
- function summarizeWrite(result) {
6492
+ function summarizeWrite(result, rawHtml) {
6822
6493
  const verified = result.elements.filter((e) => e.status === "verified");
6823
6494
  const warnings = result.elements.filter((e) => e.status === "warning");
6824
6495
  const unverifiable = result.elements.filter((e) => e.status === "unverifiable");
@@ -6834,7 +6505,20 @@ function summarizeWrite(result) {
6834
6505
  lines.push("Not verifiable \u2014 fix these references and write again:");
6835
6506
  for (const u of unverifiable) lines.push(` - ${u.kind} (${u.id}): ${u.reason ?? "unspecified"}`);
6836
6507
  }
6508
+ const n = result.sanitizeNotes;
6509
+ if (n) {
6510
+ if (n.droppedBlocks > 0) lines.push(`dropped: ${n.droppedBlocks} block(s) outside sv-ai \u2014 put your own content inside <sv-ai>\u2026</sv-ai>; only gateway elements, sv-row and one sv-glossary may stand outside it.`);
6511
+ if (n.droppedSvInsideAi > 0) lines.push(`dropped: ${n.droppedSvInsideAi} sv-* element(s) inside sv-ai \u2014 place gateway elements outside sv-ai blocks.`);
6512
+ if (n.droppedStyles > 0) lines.push(`dropped: ${n.droppedStyles} <style> block(s) \u2014 use inline style="\u2026" attributes inside sv-ai.`);
6513
+ if (n.extraGlossaries > 0) lines.push(`dropped: ${n.extraGlossaries} extra sv-glossary \u2014 one per report.`);
6514
+ }
6515
+ const g = glossaryUsage(rawHtml, result.elements);
6516
+ if (g) {
6517
+ lines.push(`Glossary: ${g.applied.length} term(s) shown as translation (when the reader switches it on).`);
6518
+ for (const r of g.rejected) lines.push(` - ignored "${r.key}": ${r.reason}`);
6519
+ }
6837
6520
  const cov = result.coverage;
6521
+ if (cov.window) lines.push(`Reporting window: ${cov.window.label}. Evidence from before it is not verifiable in this report.`);
6838
6522
  if (cov.emailExportError) {
6839
6523
  lines.push(`Coverage could not be checked: ${cov.emailExportError}`);
6840
6524
  } else {
@@ -6869,9 +6553,15 @@ function writeReportTool(deps) {
6869
6553
  },
6870
6554
  required: ["html"]
6871
6555
  },
6872
- // Empty/oversize html is refused here, BEFORE the gate requests a ticket
6873
- // (precheckBuiltin) — a refused write must not consume report_daily_max.
6874
- validate: writeReportRefusal,
6556
+ // Empty/oversize html, or no reporting window to check it against, is
6557
+ // refused here, BEFORE the gate requests a ticket (precheckBuiltin) — a
6558
+ // refused write must not consume report_daily_max.
6559
+ validate: async (args) => {
6560
+ const refusal = writeReportRefusal(args);
6561
+ if (refusal) return refusal;
6562
+ const scoped = await scope(deps);
6563
+ return scoped.ok ? void 0 : scoped.reason;
6564
+ },
6875
6565
  handler: async (args) => {
6876
6566
  const refusal = writeReportRefusal(args);
6877
6567
  if (refusal) return errorText(`Refused: ${refusal}`);
@@ -6879,9 +6569,11 @@ function writeReportTool(deps) {
6879
6569
  if (deps.state.reportStore.isLocked()) {
6880
6570
  return errorText("The report store is locked (the vault is not unlocked) \u2014 cannot save a report right now.");
6881
6571
  }
6572
+ const scoped = await scope(deps);
6573
+ if (!scoped.ok) return errorText(`Refused: ${scoped.reason}`);
6882
6574
  try {
6883
- const stored = await deps.state.reportStore.saveReport(html, deps.reportSources);
6884
- return builtinText(summarizeWrite(stored.result));
6575
+ const stored = await deps.state.reportStore.saveReport(html, scoped.sources);
6576
+ return builtinText(summarizeWrite(stored.result, html));
6885
6577
  } catch (err) {
6886
6578
  return errorText(`Could not verify and store the report: ${err instanceof Error ? err.message : String(err)}`);
6887
6579
  }
@@ -6898,6 +6590,11 @@ var reportBuiltin = (deps) => ({
6898
6590
  // read-only export CLI; writes only the local, vault-encrypted report file)
6899
6591
  // — safe during a simulation-mode test.
6900
6592
  simulation: true,
6593
+ // The same predicate the reporting window uses to refuse a mandate
6594
+ // (reporting@0.1, or no window set): the gate never selects such a mandate
6595
+ // for ANY report tool — so write_report is never ticketed under (and never
6596
+ // charges report_daily_max of) a mandate every read tool refuses (RR7).
6597
+ mandateRefusal: (auth) => reportingMandateRefusal(auth),
6901
6598
  toolGating: {
6902
6599
  overrides: {
6903
6600
  list_tickets: READ_GATE,
@@ -7733,12 +7430,36 @@ app.get("/internal/evidence", internalOnly, (_req, res) => {
7733
7430
  gates: state.gateStore.getAll()
7734
7431
  });
7735
7432
  });
7736
- async function reportResponsePayload(stored) {
7737
- const ticketDetails = await buildTicketDetails(state.receiptArchive, stored.result.proof.ticketsReferenced);
7433
+ function reportScope() {
7434
+ return scopeReportSources(reportSources, {
7435
+ authorizations: state.getEnrichedAuthorizations(),
7436
+ simulation: isSimulationMode()
7437
+ });
7438
+ }
7439
+ function storedReportScope(stored) {
7440
+ const w = stored?.result.coverage.window;
7441
+ if (w) return scopeReportSourcesToStoredWindow(reportSources, w, { simulation: isSimulationMode() });
7442
+ return reportScope();
7443
+ }
7444
+ async function reportResponsePayload(stored, archive) {
7445
+ let detailArchive = state.receiptArchive;
7446
+ if (archive) {
7447
+ detailArchive = archive;
7448
+ } else {
7449
+ const scoped = await storedReportScope(stored);
7450
+ if (scoped.ok) detailArchive = scoped.sources.archive;
7451
+ }
7452
+ const ticketDetails = await buildTicketDetails(detailArchive, stored.result.proof.ticketsReferenced);
7453
+ const glossary = glossaryUsage(stored.result.html, stored.result.elements);
7454
+ const dropped = sanitizeReport(stored.html).notes;
7455
+ const notShown = dropped.droppedBlocks + dropped.droppedSvInsideAi + dropped.droppedStyles;
7738
7456
  return {
7739
7457
  savedAt: stored.savedAt,
7740
7458
  checkedAt: stored.checkedAt,
7741
7459
  renderedHtml: renderReportHtml(stored.result.html, stored.result.elements),
7460
+ ...glossary && glossary.applied.length > 0 ? { renderedHtmlGloss: renderReportHtml(stored.result.html, stored.result.elements, { gloss: "on" }) } : {},
7461
+ ...glossary ? { glossary } : {},
7462
+ ...notShown > 0 ? { formatNotice: { blocksNotShown: notShown } } : {},
7742
7463
  proof: stored.result.proof,
7743
7464
  coverage: stored.result.coverage,
7744
7465
  elements: stored.result.elements,
@@ -7767,9 +7488,14 @@ app.post("/internal/report", internalOnly, async (req, res) => {
7767
7488
  res.status(400).json({ error: "Missing required field: html (non-empty string)" });
7768
7489
  return;
7769
7490
  }
7491
+ const scoped = await reportScope();
7492
+ if (!scoped.ok) {
7493
+ res.status(409).json({ error: scoped.reason });
7494
+ return;
7495
+ }
7770
7496
  try {
7771
- const stored = await state.reportStore.saveReport(html, reportSources);
7772
- res.json({ report: await reportResponsePayload(stored) });
7497
+ const stored = await state.reportStore.saveReport(html, scoped.sources);
7498
+ res.json({ report: await reportResponsePayload(stored, scoped.sources.archive) });
7773
7499
  } catch (err) {
7774
7500
  console.error("[Suveren MCP] /internal/report save failed:", err);
7775
7501
  res.status(500).json({ error: err instanceof Error ? err.message : "Report verification failed" });
@@ -7780,13 +7506,23 @@ app.post("/internal/report/recheck", internalOnly, async (_req, res) => {
7780
7506
  res.status(503).json({ error: "Vault locked \u2014 cannot recheck until sign-in." });
7781
7507
  return;
7782
7508
  }
7509
+ const current = state.reportStore.getReport();
7510
+ if (!current) {
7511
+ res.json({ report: null });
7512
+ return;
7513
+ }
7514
+ const scoped = await storedReportScope(current);
7515
+ if (!scoped.ok) {
7516
+ res.status(409).json({ error: scoped.reason });
7517
+ return;
7518
+ }
7783
7519
  try {
7784
- const stored = await state.reportStore.recheck(reportSources);
7520
+ const stored = await state.reportStore.recheck(scoped.sources);
7785
7521
  if (!stored) {
7786
7522
  res.json({ report: null });
7787
7523
  return;
7788
7524
  }
7789
- res.json({ report: await reportResponsePayload(stored) });
7525
+ res.json({ report: await reportResponsePayload(stored, scoped.sources.archive) });
7790
7526
  } catch (err) {
7791
7527
  console.error("[Suveren MCP] /internal/report/recheck failed:", err);
7792
7528
  res.status(500).json({ error: err instanceof Error ? err.message : "Recheck failed" });
@@ -7804,9 +7540,19 @@ app.get("/internal/report/export", internalOnly, async (req, res) => {
7804
7540
  res.status(503).json({ error: "Vault locked \u2014 the report is encrypted until sign-in." });
7805
7541
  return;
7806
7542
  }
7543
+ const current = state.reportStore.getReport();
7544
+ if (!current) {
7545
+ res.status(404).json({ error: "No report to export yet \u2014 the AI writes it with the Reporting mandate." });
7546
+ return;
7547
+ }
7548
+ const scoped = await storedReportScope(current);
7549
+ if (!scoped.ok) {
7550
+ res.status(409).json({ error: scoped.reason });
7551
+ return;
7552
+ }
7807
7553
  let stored;
7808
7554
  try {
7809
- stored = await state.reportStore.recheck(reportSources);
7555
+ stored = await state.reportStore.recheck(scoped.sources);
7810
7556
  } catch (err) {
7811
7557
  console.error("[Suveren MCP] /internal/report/export recheck failed:", err);
7812
7558
  res.status(500).json({ error: err instanceof Error ? err.message : "Recheck failed" });
@@ -7826,12 +7572,11 @@ app.get("/internal/report/export", internalOnly, async (req, res) => {
7826
7572
  const gatewayVersion = typeof req.query.gatewayVersion === "string" ? req.query.gatewayVersion : "unknown";
7827
7573
  const bundle = buildExportBundle({
7828
7574
  stored,
7829
- archive: state.receiptArchive,
7575
+ archive: scoped.sources.archive,
7830
7576
  authorityServer,
7831
7577
  gatewayVersion
7832
7578
  });
7833
- const renderedHtml = renderReportHtml(stored.result.html, stored.result.elements, false);
7834
- const document = buildExportDocument({ bundle, renderedHtml });
7579
+ const document = buildExportDocument({ bundle });
7835
7580
  res.setHeader("Content-Type", "text/html; charset=utf-8").setHeader("Content-Disposition", `attachment; filename="${suggestedFilename(bundle)}"`).setHeader("Cache-Control", "no-store").send(document);
7836
7581
  });
7837
7582
  app.get("/internal/brief", internalOnly, (_req, res) => {