@suveren/gateway 0.27.1 → 0.28.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.
@@ -54,7 +54,17 @@
54
54
  "staticExecution": {
55
55
  "action_type": "write"
56
56
  },
57
- "actionLabel": "Contact created"
57
+ "actionLabel": "Contact created",
58
+ "preview": [
59
+ { "tool": "find_contacts", "args": { "query": "email" } }
60
+ ],
61
+ "display": {
62
+ "order": ["name", "email", "company", "role", "type", "stage", "tags", "notes"],
63
+ "labels": {
64
+ "name": "Name", "email": "Email", "company": "Company", "role": "Role",
65
+ "type": "Type", "stage": "Stage", "tags": "Tags", "notes": "Notes"
66
+ }
67
+ }
58
68
  },
59
69
  "update_contact": {
60
70
  "executionMapping": {
@@ -95,7 +95,29 @@
95
95
  "staticExecution": {
96
96
  "action_type": "quote"
97
97
  },
98
- "actionLabel": "Quote drafted"
98
+ "actionLabel": "Quote drafted",
99
+ "preview": [
100
+ { "tool": "get_customer", "args": { "id": "customer_id" }, "attach": "customer_id" },
101
+ { "tool": "get_item", "forEach": "lines", "args": { "id": "item_id" }, "attach": "item" }
102
+ ],
103
+ "display": {
104
+ "order": ["customer_id", "lines", "discount_pct", "value", "valid_until", "notes"],
105
+ "money": ["value", "customer_id.credit_limit", "customer_id.open_balance", "customer_id.available_credit", "lines[].item.list_price"],
106
+ "percent": ["discount_pct"],
107
+ "currencyField": "currency",
108
+ "labels": {
109
+ "customer_id": "Customer",
110
+ "discount_pct": "Discount",
111
+ "value": "Total (net)",
112
+ "customer_id.credit_limit": "Credit limit",
113
+ "customer_id.open_balance": "Open balance",
114
+ "customer_id.available_credit": "Available credit",
115
+ "customer_id.payment_terms": "Payment terms",
116
+ "lines[].item": "Item",
117
+ "lines[].item.list_price": "List price",
118
+ "lines[].item.stock": "In stock"
119
+ }
120
+ }
99
121
  },
100
122
  "update_quote": {
101
123
  "executionMapping": {
@@ -106,7 +128,23 @@
106
128
  "staticExecution": {
107
129
  "action_type": "quote"
108
130
  },
109
- "actionLabel": "Quote updated"
131
+ "actionLabel": "Quote updated",
132
+ "preview": [
133
+ { "tool": "get_item", "forEach": "lines", "args": { "id": "item_id" }, "attach": "item" }
134
+ ],
135
+ "display": {
136
+ "order": ["lines", "discount_pct", "value", "valid_until", "notes"],
137
+ "money": ["value", "lines[].item.list_price"],
138
+ "percent": ["discount_pct"],
139
+ "currencyField": "currency",
140
+ "labels": {
141
+ "discount_pct": "Discount",
142
+ "value": "Total (net)",
143
+ "lines[].item": "Item",
144
+ "lines[].item.list_price": "List price",
145
+ "lines[].item.stock": "In stock"
146
+ }
147
+ }
110
148
  },
111
149
  "send_quote": {
112
150
  "executionMapping": {
@@ -3469,6 +3469,12 @@ import { computeContentHash as computeContentHash2 } from "@hap/core";
3469
3469
  // src/lib/manifest-loader.ts
3470
3470
  import { readFileSync as readFileSync15, existsSync as existsSync14 } from "fs";
3471
3471
  import { join as join14, resolve } from "path";
3472
+
3473
+ // src/lib/tool-gating-types.ts
3474
+ var MAX_PREVIEW_READS = 10;
3475
+ var MAX_FOREACH_ELEMENTS = 50;
3476
+
3477
+ // src/lib/manifest-loader.ts
3472
3478
  var EXACT_SEMVER_RE = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/;
3473
3479
  function isExactSemver(value) {
3474
3480
  return typeof value === "string" && EXACT_SEMVER_RE.test(value);
@@ -3483,44 +3489,101 @@ function invalidNpmPinReason(manifest) {
3483
3489
  }
3484
3490
  return null;
3485
3491
  }
3486
- function invalidPreviewReason(toolName, entry) {
3487
- if (entry === null || typeof entry !== "object") return null;
3488
- const preview = entry.preview;
3489
- if (preview === void 0) return null;
3490
- if (preview === null || typeof preview !== "object" || Array.isArray(preview)) {
3491
- return `override "${toolName}".preview must be an object`;
3492
+ function invalidReadConfigReason(toolName, idx, entry) {
3493
+ const label = idx === null ? `override "${toolName}".preview` : `override "${toolName}".preview[${idx}]`;
3494
+ if (entry === null || typeof entry !== "object" || Array.isArray(entry)) {
3495
+ return `${label} must be an object`;
3492
3496
  }
3493
- const p = preview;
3497
+ const p = entry;
3494
3498
  if (typeof p.tool !== "string" || p.tool.length === 0) {
3495
- return `override "${toolName}".preview.tool must be a non-empty string`;
3499
+ return `${label}.tool must be a non-empty string`;
3496
3500
  }
3497
3501
  if (p.args === void 0 || p.args === null || typeof p.args !== "object" || Array.isArray(p.args)) {
3498
- return `override "${toolName}".preview.args must be an object mapping preview-tool arg names to action arg names`;
3502
+ return `${label}.args must be an object mapping preview-tool arg names to action arg names`;
3499
3503
  }
3500
3504
  for (const [argName, source] of Object.entries(p.args)) {
3501
3505
  if (typeof source !== "string" || source.length === 0) {
3502
- return `override "${toolName}".preview.args.${argName} must name a non-empty source argument (string)`;
3506
+ return `${label}.args.${argName} must name a non-empty source argument (string)`;
3503
3507
  }
3504
3508
  }
3509
+ if (p.forEach !== void 0 && (typeof p.forEach !== "string" || p.forEach.length === 0)) {
3510
+ return `${label}.forEach must be a non-empty string naming an action argument`;
3511
+ }
3512
+ if (p.attach !== void 0 && (typeof p.attach !== "string" || p.attach.length === 0)) {
3513
+ return `${label}.attach must be a non-empty string`;
3514
+ }
3505
3515
  if (p.version !== void 0) {
3506
3516
  if (p.version === null || typeof p.version !== "object" || Array.isArray(p.version)) {
3507
- return `override "${toolName}".preview.version must be an object`;
3517
+ return `${label}.version must be an object`;
3508
3518
  }
3509
3519
  const v = p.version;
3510
3520
  if (typeof v.arg !== "string" || v.arg.length === 0) {
3511
- return `override "${toolName}".preview.version.arg must be a non-empty string`;
3521
+ return `${label}.version.arg must be a non-empty string`;
3512
3522
  }
3513
3523
  if (typeof v.field !== "string" || v.field.length === 0) {
3514
- return `override "${toolName}".preview.version.field must be a non-empty string`;
3524
+ return `${label}.version.field must be a non-empty string`;
3515
3525
  }
3516
3526
  }
3517
3527
  if (p.fields !== void 0) {
3518
3528
  if (!Array.isArray(p.fields)) {
3519
- return `override "${toolName}".preview.fields must be an array of field names`;
3529
+ return `${label}.fields must be an array of field names`;
3520
3530
  }
3521
3531
  for (const [i, f] of p.fields.entries()) {
3522
3532
  if (typeof f !== "string" || f.length === 0) {
3523
- return `override "${toolName}".preview.fields[${i}] must be a non-empty string`;
3533
+ return `${label}.fields[${i}] must be a non-empty string`;
3534
+ }
3535
+ }
3536
+ }
3537
+ return null;
3538
+ }
3539
+ function invalidPreviewReason(toolName, entry) {
3540
+ if (entry === null || typeof entry !== "object") return null;
3541
+ const preview = entry.preview;
3542
+ if (preview === void 0) return null;
3543
+ if (Array.isArray(preview)) {
3544
+ if (preview.length === 0) {
3545
+ return `override "${toolName}".preview must not be an empty list \u2014 omit it, or use the single-object form`;
3546
+ }
3547
+ if (preview.length > MAX_PREVIEW_READS) {
3548
+ return `override "${toolName}".preview declares ${preview.length} reads \u2014 max ${MAX_PREVIEW_READS}`;
3549
+ }
3550
+ for (const [i, read] of preview.entries()) {
3551
+ const reason = invalidReadConfigReason(toolName, i, read);
3552
+ if (reason) return reason;
3553
+ }
3554
+ return null;
3555
+ }
3556
+ return invalidReadConfigReason(toolName, null, preview);
3557
+ }
3558
+ function invalidDisplayHintsReason(toolName, entry) {
3559
+ if (entry === null || typeof entry !== "object") return null;
3560
+ const display = entry.display;
3561
+ if (display === void 0) return null;
3562
+ if (display === null || typeof display !== "object" || Array.isArray(display)) {
3563
+ return `override "${toolName}".display must be an object`;
3564
+ }
3565
+ const d = display;
3566
+ for (const key of ["order", "money", "percent", "hidden"]) {
3567
+ if (d[key] === void 0) continue;
3568
+ if (!Array.isArray(d[key])) {
3569
+ return `override "${toolName}".display.${key} must be an array of field paths`;
3570
+ }
3571
+ for (const [i, v] of d[key].entries()) {
3572
+ if (typeof v !== "string" || v.length === 0) {
3573
+ return `override "${toolName}".display.${key}[${i}] must be a non-empty string`;
3574
+ }
3575
+ }
3576
+ }
3577
+ if (d.currencyField !== void 0 && (typeof d.currencyField !== "string" || d.currencyField.length === 0)) {
3578
+ return `override "${toolName}".display.currencyField must be a non-empty string`;
3579
+ }
3580
+ if (d.labels !== void 0) {
3581
+ if (d.labels === null || typeof d.labels !== "object" || Array.isArray(d.labels)) {
3582
+ return `override "${toolName}".display.labels must be an object mapping field paths to labels`;
3583
+ }
3584
+ for (const [path, label] of Object.entries(d.labels)) {
3585
+ if (typeof label !== "string" || label.length === 0) {
3586
+ return `override "${toolName}".display.labels.${path} must be a non-empty string`;
3524
3587
  }
3525
3588
  }
3526
3589
  }
@@ -3529,7 +3592,7 @@ function invalidPreviewReason(toolName, entry) {
3529
3592
  function invalidManifestPreviewReason(manifest) {
3530
3593
  const overrides = manifest.toolGating?.overrides ?? {};
3531
3594
  for (const [toolName, entry] of Object.entries(overrides)) {
3532
- const reason = invalidPreviewReason(toolName, entry);
3595
+ const reason = invalidPreviewReason(toolName, entry) ?? invalidDisplayHintsReason(toolName, entry);
3533
3596
  if (reason) return reason;
3534
3597
  }
3535
3598
  return null;
@@ -3598,6 +3661,13 @@ function getToolPreviewConfig(integrationManager2, integrationId, toolName) {
3598
3661
  const entry = overrides[toolName];
3599
3662
  return entry?.preview;
3600
3663
  }
3664
+ function getToolDisplayHints(integrationManager2, integrationId, toolName) {
3665
+ if (integrationManager2.isBuiltin(integrationId)) return void 0;
3666
+ const manifest = getManifest(integrationId);
3667
+ const overrides = manifest?.toolGating?.overrides ?? {};
3668
+ const entry = overrides[toolName];
3669
+ return entry?.display;
3670
+ }
3601
3671
  function mapPreviewArgs(preview, actionArgs) {
3602
3672
  const mapped = {};
3603
3673
  for (const [targetArg, sourceArg] of Object.entries(preview.args)) {
@@ -3656,10 +3726,105 @@ function hashPreviewBody(body) {
3656
3726
  }
3657
3727
  return computeContentHash2({ kind: "text", version: "1" }, body.text ?? "");
3658
3728
  }
3729
+ function isPlainRecord(v) {
3730
+ return typeof v === "object" && v !== null && !Array.isArray(v);
3731
+ }
3732
+ function planPreviewReads(declaration, actionArgs) {
3733
+ const reads = Array.isArray(declaration) ? declaration : [declaration];
3734
+ return reads.map((config) => {
3735
+ const base = {
3736
+ tool: config.tool,
3737
+ attach: config.attach,
3738
+ fields: config.fields,
3739
+ version: config.version,
3740
+ argsMapping: config.args
3741
+ };
3742
+ if (config.forEach) {
3743
+ const source = actionArgs[config.forEach];
3744
+ const list = Array.isArray(source) ? source : [];
3745
+ const capped = list.slice(0, MAX_FOREACH_ELEMENTS);
3746
+ const elements = capped.filter(isPlainRecord).map((el) => ({ args: mapPreviewArgs(config, el) }));
3747
+ return {
3748
+ ...base,
3749
+ forEach: config.forEach,
3750
+ elements,
3751
+ ...list.length > MAX_FOREACH_ELEMENTS ? { overflow: { shown: MAX_FOREACH_ELEMENTS, total: list.length } } : {}
3752
+ };
3753
+ }
3754
+ const mapped = mapPreviewArgs(config, actionArgs);
3755
+ return { ...base, args: mapped, rawSource: actionArgs, noTarget: previewHasNoTarget(config, mapped) };
3756
+ });
3757
+ }
3758
+ async function runPreviewReadPlan(integrationManager2, integrationId, plan) {
3759
+ const sections = [];
3760
+ for (const item of plan) {
3761
+ const sourceArgNames = Object.values(item.argsMapping);
3762
+ const base = {
3763
+ tool: item.tool,
3764
+ ...item.attach ? { attach: item.attach } : {},
3765
+ ...sourceArgNames.length === 1 ? { sourceArg: sourceArgNames[0] } : {},
3766
+ ...item.fields ? { fields: item.fields } : {}
3767
+ };
3768
+ if (item.elements !== void 0) {
3769
+ const elements = [];
3770
+ for (const el of item.elements) {
3771
+ elements.push(await readPreview(integrationManager2, integrationId, item.tool, el.args));
3772
+ }
3773
+ sections.push({
3774
+ ...base,
3775
+ forEach: item.forEach,
3776
+ elements,
3777
+ ...item.overflow ? { overflow: item.overflow } : {}
3778
+ });
3779
+ continue;
3780
+ }
3781
+ if (item.noTarget) {
3782
+ sections.push({ ...base, result: { status: "none", reason: "no_target" } });
3783
+ continue;
3784
+ }
3785
+ const read1 = await readPreview(integrationManager2, integrationId, item.tool, item.args);
3786
+ if (read1.status !== "ok" || !item.version) {
3787
+ sections.push({ ...base, result: read1 });
3788
+ continue;
3789
+ }
3790
+ const approved = item.rawSource?.[item.version.arg];
3791
+ if (typeof approved !== "string" && typeof approved !== "number") {
3792
+ sections.push({ ...base, result: read1 });
3793
+ continue;
3794
+ }
3795
+ const { [item.version.arg]: _omit, ...withoutVersion } = item.rawSource;
3796
+ const mappedCurrentArgs = mapPreviewArgs({ args: item.argsMapping }, withoutVersion);
3797
+ const read2 = await readPreview(integrationManager2, integrationId, item.tool, mappedCurrentArgs);
3798
+ if (read2.status !== "ok" || read2.body.structured === void 0) {
3799
+ sections.push({ ...base, result: read1 });
3800
+ continue;
3801
+ }
3802
+ const current = read2.body.structured[item.version.field];
3803
+ if (typeof current !== "string" && typeof current !== "number") {
3804
+ sections.push({ ...base, result: read1 });
3805
+ continue;
3806
+ }
3807
+ const stale = String(current) !== String(approved);
3808
+ sections.push({
3809
+ ...base,
3810
+ result: read1,
3811
+ version: {
3812
+ field: item.version.field,
3813
+ approved,
3814
+ current,
3815
+ stale,
3816
+ ...stale ? { currentBody: read2.body } : {}
3817
+ }
3818
+ });
3819
+ }
3820
+ return sections;
3821
+ }
3659
3822
  async function computeSubmissionPreviewHash(integrationManager2, integrationId, toolName, actionArgs) {
3660
3823
  if (integrationManager2.isBuiltin(integrationId)) return void 0;
3661
3824
  const preview = getToolPreviewConfig(integrationManager2, integrationId, toolName);
3662
- if (!preview || preview.version) return void 0;
3825
+ if (!preview) return void 0;
3826
+ if (Array.isArray(preview)) return combinedPreviewReadsHash(integrationManager2, integrationId, preview, actionArgs);
3827
+ if (preview.version) return void 0;
3663
3828
  const mapped = mapPreviewArgs(preview, actionArgs);
3664
3829
  if (previewHasNoTarget(preview, mapped)) return void 0;
3665
3830
  const read = await readPreview(integrationManager2, integrationId, preview.tool, mapped);
@@ -3669,14 +3834,44 @@ async function computeSubmissionPreviewHash(integrationManager2, integrationId,
3669
3834
  async function previewChangedSinceSubmission(integrationManager2, integrationId, toolName, actionArgs, previousHash) {
3670
3835
  const preview = getToolPreviewConfig(integrationManager2, integrationId, toolName);
3671
3836
  if (!preview) return true;
3837
+ if (Array.isArray(preview)) {
3838
+ const hash = await combinedPreviewReadsHash(integrationManager2, integrationId, preview, actionArgs);
3839
+ if (hash === void 0) return true;
3840
+ return hash !== previousHash;
3841
+ }
3672
3842
  const mapped = mapPreviewArgs(preview, actionArgs);
3673
3843
  const read = await readPreview(integrationManager2, integrationId, preview.tool, mapped);
3674
3844
  if (read.status !== "ok") return true;
3675
3845
  return hashPreviewBody(read.body) !== previousHash;
3676
3846
  }
3847
+ async function combinedPreviewReadsHash(integrationManager2, integrationId, reads, actionArgs) {
3848
+ const plan = planPreviewReads(reads, actionArgs).filter((item) => !item.version);
3849
+ const hashes = [];
3850
+ for (const item of plan) {
3851
+ if (item.elements !== void 0) {
3852
+ for (const el of item.elements) {
3853
+ const read2 = await readPreview(integrationManager2, integrationId, item.tool, el.args);
3854
+ if (read2.status !== "ok") return void 0;
3855
+ hashes.push(hashPreviewBody(read2.body));
3856
+ }
3857
+ continue;
3858
+ }
3859
+ if (item.noTarget) continue;
3860
+ const read = await readPreview(integrationManager2, integrationId, item.tool, item.args);
3861
+ if (read.status !== "ok") return void 0;
3862
+ hashes.push(hashPreviewBody(read.body));
3863
+ }
3864
+ if (hashes.length === 0) return void 0;
3865
+ return computeContentHash2({ kind: "jcs", version: "1" }, { reads: hashes });
3866
+ }
3677
3867
  async function buildInternalPreview(integrationManager2, integrationId, toolName, actionArgs) {
3678
3868
  const preview = getToolPreviewConfig(integrationManager2, integrationId, toolName);
3679
3869
  if (!preview) return { status: "none" };
3870
+ if (Array.isArray(preview)) {
3871
+ const reads = await runPreviewReadPlan(integrationManager2, integrationId, planPreviewReads(preview, actionArgs));
3872
+ const display = getToolDisplayHints(integrationManager2, integrationId, toolName);
3873
+ return { status: "ok", integration: integrationId, reads, ...display ? { display } : {} };
3874
+ }
3680
3875
  const mappedArgs = mapPreviewArgs(preview, actionArgs);
3681
3876
  if (previewHasNoTarget(preview, mappedArgs)) return { status: "none", reason: "no_target" };
3682
3877
  const read1 = await readPreview(integrationManager2, integrationId, preview.tool, mappedArgs);
@@ -5616,14 +5811,16 @@ async function executeCommitted(proposal, state2, integrationManager2) {
5616
5811
  }
5617
5812
  let receiptId;
5618
5813
  let replayed = false;
5814
+ let binding;
5815
+ let receipt;
5619
5816
  try {
5620
- const binding = computeContentBinding(
5817
+ binding = computeContentBinding(
5621
5818
  proposal.profileId,
5622
5819
  discovered,
5623
5820
  toolArgs,
5624
5821
  proposalActionType
5625
5822
  );
5626
- const { receipt, idempotent } = await state2.spClient.postReceipt({
5823
+ const { receipt: r, idempotent } = await state2.spClient.postReceipt({
5627
5824
  authorizationId: proposal.authorizationId,
5628
5825
  // Optional cross-check — the AS fails closed on a mismatch. Parity
5629
5826
  // with the automatic path (tool-proxy.ts).
@@ -5652,48 +5849,10 @@ async function executeCommitted(proposal, state2, integrationManager2) {
5652
5849
  // this machine — see the archive call below.
5653
5850
  ...binding ? { contentHash: binding.contentHash, contentBinding: binding.contentBinding } : {}
5654
5851
  });
5852
+ receipt = r;
5655
5853
  receiptId = typeof receipt?.id === "string" ? receipt.id : void 0;
5656
5854
  replayed = idempotent;
5657
- await verifyTicket(state2.cache, receipt, {
5658
- action: proposal.tool,
5659
- executionContext: proposal.executionContext,
5660
- authorizationId: proposal.authorizationId,
5661
- profileId: proposal.profileId,
5662
- proposalId: proposal.id,
5663
- contentHash: binding?.contentHash,
5664
- contentBinding: binding?.contentBinding
5665
- });
5666
- await state2.archiveReceipt(receipt, {
5667
- authorizationId: proposal.authorizationId,
5668
- profileId: proposal.profileId,
5669
- boundsHash: cachedAuth?.boundsHash,
5670
- contextHash: cachedAuth?.contextHash,
5671
- bounds: cachedAuth?.bounds ?? cachedAuth?.frame,
5672
- context: cachedAuth?.context,
5673
- attestations: cachedAuth?.attestations,
5674
- // What the human actually approved, kept next to the receipt that cites
5675
- // it — otherwise `proposalId` is a pointer to a server we may not have.
5676
- // PE1: the archived copy carries the RESOLVED (decrypted on an
5677
- // encrypted row) toolArgs, never the sealed `content` — this archive
5678
- // is this gateway's own local evidence and has always stored it
5679
- // readable, same as every legacy row.
5680
- proposal: { ...proposal, toolArgs },
5681
- boundContent: binding?.boundContent
5682
- });
5683
5855
  } catch (err) {
5684
- if (err instanceof AsKeyMismatchError || err instanceof TicketBindingMismatchError) {
5685
- void notifyControlPlane("as-key-mismatch");
5686
- return {
5687
- text: `Proposal ${proposal.id}: blocked \u2014 ${err.message}`,
5688
- isError: true
5689
- };
5690
- }
5691
- if (err instanceof ArchiveWriteError) {
5692
- return {
5693
- text: `Proposal ${proposal.id}: blocked \u2014 the ticket could not be archived locally \u2014 ${err.message}. The action was not executed.`,
5694
- isError: true
5695
- };
5696
- }
5697
5856
  if (err instanceof ContentBindingError2) {
5698
5857
  return {
5699
5858
  text: `Proposal ${proposal.id}: blocked \u2014 the approved content cannot be bound to the receipt. ${err.message}`,
@@ -5709,6 +5868,12 @@ async function executeCommitted(proposal, state2, integrationManager2) {
5709
5868
  text: `Proposal ${proposal.id} has already been executed by another request.`
5710
5869
  };
5711
5870
  }
5871
+ if (code === "PROPOSAL_REFUSED") {
5872
+ return {
5873
+ text: `Proposal ${proposal.id}: the Authority Server refused this proposal's ticket earlier \u2014 ${err.message} Nothing ran. Do not retry the same values; ask the person (e.g. for a higher limit).`,
5874
+ isError: true
5875
+ };
5876
+ }
5712
5877
  if (err.statusCode === 409 && code === "VERSION_UNSUPPORTED") {
5713
5878
  state2.cache.markNeedsReapproval(proposal.authorizationId);
5714
5879
  return {
@@ -5716,8 +5881,20 @@ async function executeCommitted(proposal, state2, integrationManager2) {
5716
5881
  isError: true
5717
5882
  };
5718
5883
  }
5884
+ if (proposal.id) {
5885
+ const asRefusedTicketId = `as_refused:${proposal.id}`;
5886
+ const begun2 = state2.executionJournal.begin({
5887
+ ticketId: asRefusedTicketId,
5888
+ proposalId: proposal.id,
5889
+ tool: proposal.tool,
5890
+ argsHash: hashToolArgs(toolArgs)
5891
+ });
5892
+ if (begun2.ok) {
5893
+ state2.executionJournal.complete(asRefusedTicketId, "failed", "as_refused", `${code ?? "TICKET_REFUSED"}: ${err.message}`);
5894
+ }
5895
+ }
5719
5896
  return {
5720
- text: `Proposal ${proposal.id}: SP rejected receipt \u2014 ${err.message}`,
5897
+ text: `Proposal ${proposal.id}: the Authority Server refused its ticket \u2014 ${err.message} Nothing ran. Do not retry the same values; ask the person (e.g. for a higher limit).`,
5721
5898
  isError: true
5722
5899
  };
5723
5900
  }
@@ -5751,6 +5928,54 @@ async function executeCommitted(proposal, state2, integrationManager2) {
5751
5928
  isError: true
5752
5929
  };
5753
5930
  }
5931
+ try {
5932
+ await verifyTicket(state2.cache, receipt, {
5933
+ action: proposal.tool,
5934
+ executionContext: proposal.executionContext,
5935
+ authorizationId: proposal.authorizationId,
5936
+ profileId: proposal.profileId,
5937
+ proposalId: proposal.id,
5938
+ contentHash: binding?.contentHash,
5939
+ contentBinding: binding?.contentBinding
5940
+ });
5941
+ await state2.archiveReceipt(receipt, {
5942
+ authorizationId: proposal.authorizationId,
5943
+ profileId: proposal.profileId,
5944
+ boundsHash: cachedAuth?.boundsHash,
5945
+ contextHash: cachedAuth?.contextHash,
5946
+ bounds: cachedAuth?.bounds ?? cachedAuth?.frame,
5947
+ context: cachedAuth?.context,
5948
+ attestations: cachedAuth?.attestations,
5949
+ // What the human actually approved, kept next to the receipt that cites
5950
+ // it — otherwise `proposalId` is a pointer to a server we may not have.
5951
+ // PE1: the archived copy carries the RESOLVED (decrypted on an
5952
+ // encrypted row) toolArgs, never the sealed `content` — this archive
5953
+ // is this gateway's own local evidence and has always stored it
5954
+ // readable, same as every legacy row.
5955
+ proposal: { ...proposal, toolArgs },
5956
+ boundContent: binding?.boundContent
5957
+ });
5958
+ } catch (err) {
5959
+ const detail = err instanceof Error ? err.message : String(err);
5960
+ state2.executionJournal.complete(receiptId, "failed", void 0, detail);
5961
+ if (err instanceof AsKeyMismatchError || err instanceof TicketBindingMismatchError) {
5962
+ void notifyControlPlane("as-key-mismatch");
5963
+ return {
5964
+ text: `Proposal ${proposal.id}: blocked \u2014 ${err.message}`,
5965
+ isError: true
5966
+ };
5967
+ }
5968
+ if (err instanceof ArchiveWriteError) {
5969
+ return {
5970
+ text: `Proposal ${proposal.id}: blocked \u2014 the ticket could not be archived locally \u2014 ${err.message}. The action was not executed.`,
5971
+ isError: true
5972
+ };
5973
+ }
5974
+ return {
5975
+ text: `Proposal ${proposal.id}: blocked \u2014 ${detail}`,
5976
+ isError: true
5977
+ };
5978
+ }
5754
5979
  if (replayed) {
5755
5980
  console.error(
5756
5981
  `[Suveren MCP] Proposal ${proposal.id}: ticket ${receiptId} was replayed by the AS with no local execution record \u2014 executing once (lost-response recovery).`
@@ -5765,7 +5990,7 @@ async function executeCommitted(proposal, state2, integrationManager2) {
5765
5990
  submitted.previewHash
5766
5991
  );
5767
5992
  if (changed) {
5768
- state2.executionJournal.complete(receiptId, "failed", "changed");
5993
+ state2.executionJournal.complete(receiptId, "failed", "changed", `The record changed in ${integrationId} since it was requested.`);
5769
5994
  return {
5770
5995
  text: `Proposal ${proposal.id} was approved, but the record changed in ${integrationId} since it was requested \u2014 nothing was done. A new attempt needs a new request and approval.`,
5771
5996
  isError: true
@@ -5819,8 +6044,10 @@ function localOutcomeOfExecuted(proposal, state2) {
5819
6044
  if (!entry || entry.state === "done") return null;
5820
6045
  const integration = proposal.tool.split("__")[0];
5821
6046
  if (entry.state === "failed") {
6047
+ const reasonSuffix = entry.detail ? `
6048
+ Reason: ${entry.detail}` : "";
5822
6049
  return {
5823
- text: entry.outcome === "refused" ? `Proposal ${proposal.id} was approved, but ${integration} refused to run it \u2014 nothing was done. Do not call the tool again with the same values; a new attempt needs a new request and approval.` : entry.outcome === "changed" ? `Proposal ${proposal.id} was approved, but the record changed in ${integration} since it was requested \u2014 nothing was done. A new attempt needs a new request and approval.` : `Proposal ${proposal.id} was approved, but running it failed \u2014 whether anything happened in ${integration} is unknown. Check ${integration} before trying again; a new attempt needs a new request and approval.`,
6050
+ text: entry.outcome === "as_refused" ? `Proposal ${proposal.id} was approved, but the Authority Server refused its ticket \u2014 nothing ran. Do not retry the same values; ask the person (e.g. for a higher limit).${reasonSuffix}` : entry.outcome === "refused" ? `Proposal ${proposal.id} was approved, but ${integration} refused to run it \u2014 nothing was done. Do not call the tool again with the same values; a new attempt needs a new request and approval.${reasonSuffix}` : entry.outcome === "changed" ? `Proposal ${proposal.id} was approved, but the record changed in ${integration} since it was requested \u2014 nothing was done. A new attempt needs a new request and approval.${reasonSuffix}` : `Proposal ${proposal.id} was approved, but running it failed \u2014 whether anything happened in ${integration} is unknown. Check ${integration} before trying again; a new attempt needs a new request and approval.${reasonSuffix}`,
5824
6051
  isError: true
5825
6052
  };
5826
6053
  }
@@ -5869,6 +6096,16 @@ Result: ${JSON.stringify(match.executionResult, null, 2)}` : " The action ran; t
5869
6096
  }]
5870
6097
  };
5871
6098
  }
6099
+ if (match.status === "refused") {
6100
+ const detail = match.refusal ? match.refusal.message : "a bound was exceeded";
6101
+ return {
6102
+ content: [{
6103
+ type: "text",
6104
+ text: `Proposal ${match.id} was approved, but the Authority Server refused its ticket \u2014 ${detail} Nothing ran. Do not retry the same values; ask the person (e.g. for a higher limit).`
6105
+ }],
6106
+ isError: true
6107
+ };
6108
+ }
5872
6109
  if (match.status === "rejected") {
5873
6110
  const by = match.approverRejectedBy ?? match.rejectedBy;
5874
6111
  const outcome = by?.outcome ?? "rejected";