@suveren/gateway 0.26.0 → 0.27.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.
@@ -717,6 +717,42 @@ function describeFetchError(err) {
717
717
  return String(err);
718
718
  }
719
719
 
720
+ // src/lib/cp-content-client.ts
721
+ var CP_PORT2 = process.env.SUVEREN_CP_PORT ?? "3402";
722
+ var CP_BASE2 = process.env.SUVEREN_CP_INTERNAL_URL ?? `http://127.0.0.1:${CP_PORT2}`;
723
+ var TIMEOUT_MS2 = 1e4;
724
+ async function postInternal(path, body) {
725
+ const secret = process.env.SUVEREN_INTERNAL_SECRET ?? "";
726
+ if (!secret) {
727
+ throw new Error("No internal secret configured between the MCP server and the control plane.");
728
+ }
729
+ const controller = new AbortController();
730
+ const timer = setTimeout(() => controller.abort(), TIMEOUT_MS2);
731
+ try {
732
+ const res = await fetch(`${CP_BASE2}${path}`, {
733
+ method: "POST",
734
+ headers: { "Content-Type": "application/json", "X-Internal-Secret": secret },
735
+ body: JSON.stringify(body),
736
+ signal: controller.signal
737
+ });
738
+ const data = await res.json().catch(() => ({}));
739
+ if (!res.ok) {
740
+ throw new Error(data.error ?? `${path} failed: ${res.status}`);
741
+ }
742
+ return data;
743
+ } finally {
744
+ clearTimeout(timer);
745
+ }
746
+ }
747
+ async function cpEncryptContent(toolArgs, recipientUserIds) {
748
+ const data = await postInternal("/internal/encrypt-content", { toolArgs, recipientUserIds });
749
+ return data;
750
+ }
751
+ async function cpDecryptContent(wire) {
752
+ const data = await postInternal("/internal/decrypt-content", { ciphertext: wire.ciphertext, keys: wire.keys });
753
+ return data.plaintext;
754
+ }
755
+
720
756
  // src/lib/sp-client.ts
721
757
  var SPReceiptError = class extends Error {
722
758
  constructor(message, statusCode, body) {
@@ -944,7 +980,11 @@ var SPClient = class {
944
980
  * Errors throw SPReceiptError with the structured `errors` array in `body`.
945
981
  */
946
982
  async postReceipt(data) {
947
- const body = JSON.stringify(data);
983
+ const { proposalContentHash, ...rest } = data;
984
+ const body = JSON.stringify({
985
+ ...rest,
986
+ ...proposalContentHash ? { proposal_content_hash: proposalContentHash } : {}
987
+ });
948
988
  const maxAttempts = data.idempotencyKey || data.proposalId ? this.receiptRetry.maxAttempts : 1;
949
989
  let lastError;
950
990
  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
@@ -984,8 +1024,20 @@ var SPClient = class {
984
1024
  /**
985
1025
  * Submit a proposal for deferred commitment review.
986
1026
  * Phase 6: accepts optional pendingApprovers and createdBy for above-cap routing.
1027
+ *
1028
+ * PE1: toolArgs is encrypted (via the control plane's vault + the AS's
1029
+ * registered public keys — cp-content-client.ts) before it ever reaches
1030
+ * this method's request body; only the sealed `content` is sent, never
1031
+ * `tool_args`. `recipientUserIds` are who besides the submitter (added by
1032
+ * the control plane itself) must be able to read it back: the mandate
1033
+ * owner, every pending approver (above-cap path), or every active member
1034
+ * holding a pending domain (deferred-domain path) — decision 3 of the
1035
+ * PE1 brief. `unreadableFor` is G4: anyone in that set with no registered
1036
+ * end-to-end key — the proposal is still created for everyone who CAN
1037
+ * read it; the caller reports who cannot.
987
1038
  */
988
1039
  async submitProposal(data) {
1040
+ const { content, unreadableFor } = await cpEncryptContent(data.toolArgs, data.recipientUserIds);
989
1041
  const res = await this.fetch("/api/proposals", {
990
1042
  method: "POST",
991
1043
  body: JSON.stringify({
@@ -994,7 +1046,7 @@ var SPClient = class {
994
1046
  path: data.path,
995
1047
  pending_domains: data.pendingDomains,
996
1048
  tool: data.tool,
997
- tool_args: data.toolArgs,
1049
+ content,
998
1050
  execution_context: data.executionContext,
999
1051
  // Phase 6
1000
1052
  ...data.pendingApprovers && data.pendingApprovers.length > 0 ? { pending_approvers: data.pendingApprovers } : {},
@@ -1002,11 +1054,12 @@ var SPClient = class {
1002
1054
  })
1003
1055
  });
1004
1056
  if (!res.ok) {
1005
- const body = await res.json().catch(() => ({}));
1006
- throw new Error(body.error ?? `SP proposal submission failed: ${res.status}`);
1057
+ const body2 = await res.json().catch(() => ({}));
1058
+ throw new Error(body2.error ?? `SP proposal submission failed: ${res.status}`);
1007
1059
  }
1008
1060
  void notifyControlPlane("proposal-added");
1009
- return res.json();
1061
+ const body = await res.json();
1062
+ return { proposal: body.proposal, unreadableFor };
1010
1063
  }
1011
1064
  /**
1012
1065
  * Phase 6: Fetch the authorization summary by its per-ceremony id.
@@ -3747,6 +3800,20 @@ async function verifyTicket(cache, receipt, expected) {
3747
3800
  }
3748
3801
  }
3749
3802
 
3803
+ // src/lib/who-label.ts
3804
+ var UNKNOWN_DECIDER = "the person";
3805
+ async function resolveWhoLabel(archive, domainKey, userId) {
3806
+ if (!archive) return UNKNOWN_DECIDER;
3807
+ try {
3808
+ const dir = await getIdentityDirectory(archive);
3809
+ const did = dir.domainDids.get(domainKey) ?? dir.domainDids.get(userId);
3810
+ const name = did ? dir.names.get(did) : void 0;
3811
+ return name ?? UNKNOWN_DECIDER;
3812
+ } catch {
3813
+ return UNKNOWN_DECIDER;
3814
+ }
3815
+ }
3816
+
3750
3817
  // src/lib/arg-encoding.ts
3751
3818
  function rfc2047Encode(value) {
3752
3819
  if (!/[^\x00-\x7F]/.test(value)) return value;
@@ -4066,6 +4133,14 @@ async function attachImagePreview(args) {
4066
4133
  return args;
4067
4134
  }
4068
4135
  }
4136
+ async function unreadableForNote(unreadableFor, state2) {
4137
+ if (unreadableFor.length === 0) return "";
4138
+ const labels = await Promise.all(
4139
+ unreadableFor.map((id) => resolveWhoLabel(state2.receiptArchive, id, id))
4140
+ );
4141
+ return `
4142
+ Note: ${labels.join(", ")} cannot read this proposal's content yet \u2014 no device of theirs has registered an end-to-end key.`;
4143
+ }
4069
4144
  function applyMapping(m, value, execution) {
4070
4145
  if (m.divisor) {
4071
4146
  const numValue = typeof value === "number" ? value : Number(value);
@@ -4640,14 +4715,22 @@ function createGatedToolHandlerInner(tool, integrationManager2, state2) {
4640
4715
  try {
4641
4716
  const enrichedArgs = await attachImagePreview(args);
4642
4717
  const wireExecCtx = wireExecutionContext(auth.profileId, execution);
4643
- const { proposal } = await state2.spClient.submitProposal({
4718
+ let recipientUserIds = [...auth.deferredCommitmentDomains];
4719
+ try {
4720
+ const summary = await state2.spClient.getAuthorizationSummary(authzId);
4721
+ if (summary?.created_by) recipientUserIds = [summary.created_by, ...recipientUserIds];
4722
+ } catch {
4723
+ }
4724
+ recipientUserIds = [...new Set(recipientUserIds)];
4725
+ const { proposal, unreadableFor } = await state2.spClient.submitProposal({
4644
4726
  authorizationId: authzId,
4645
4727
  profileId: auth.profileId,
4646
4728
  path: auth.path,
4647
4729
  pendingDomains: auth.deferredCommitmentDomains,
4648
4730
  tool: tool.namespacedName,
4649
4731
  toolArgs: enrichedArgs,
4650
- executionContext: wireExecCtx
4732
+ executionContext: wireExecCtx,
4733
+ recipientUserIds
4651
4734
  });
4652
4735
  const previewHash = await computeSubmissionPreviewHash(
4653
4736
  integrationManager2,
@@ -4668,7 +4751,7 @@ function createGatedToolHandlerInner(tool, integrationManager2, state2) {
4668
4751
  content: [{
4669
4752
  type: "text",
4670
4753
  text: `Awaiting commitment from domain${auth.deferredCommitmentDomains.length > 1 ? "s" : ""} "${auth.deferredCommitmentDomains.join('", "')}" for tool ${tool.originalName}.
4671
- Proposal ID: ${proposal.id}. Check status with check-pending-commitments(proposal_id: "${proposal.id}").`
4754
+ Proposal ID: ${proposal.id}. Check status with check-pending-commitments(proposal_id: "${proposal.id}").` + await unreadableForNote(unreadableFor ?? [], state2)
4672
4755
  }]
4673
4756
  };
4674
4757
  } catch (err) {
@@ -4818,7 +4901,7 @@ Proposal ID: ${proposal.id}. Check status with check-pending-commitments(proposa
4818
4901
  try {
4819
4902
  const enrichedArgs = await attachImagePreview(args);
4820
4903
  const wireExecCtx = wireExecutionContext(auth.profileId, execution);
4821
- const { proposal } = await state2.spClient.submitProposal({
4904
+ const { proposal, unreadableFor } = await state2.spClient.submitProposal({
4822
4905
  authorizationId: authzId,
4823
4906
  profileId: auth.profileId,
4824
4907
  path: auth.path,
@@ -4826,7 +4909,8 @@ Proposal ID: ${proposal.id}. Check status with check-pending-commitments(proposa
4826
4909
  tool: tool.namespacedName,
4827
4910
  toolArgs: enrichedArgs,
4828
4911
  executionContext: wireExecCtx,
4829
- pendingApprovers: uniqueApprovers
4912
+ pendingApprovers: uniqueApprovers,
4913
+ recipientUserIds: uniqueApprovers
4830
4914
  });
4831
4915
  const previewHash = await computeSubmissionPreviewHash(
4832
4916
  integrationManager2,
@@ -4847,7 +4931,7 @@ Proposal ID: ${proposal.id}. Check status with check-pending-commitments(proposa
4847
4931
  content: [{
4848
4932
  type: "text",
4849
4933
  text: `Action exceeds team cap. Approval required from ${uniqueApprovers.length} reviewer(s).
4850
- Proposal ID: ${proposal.id}. Use check-pending-commitments to track status.`
4934
+ Proposal ID: ${proposal.id}. Use check-pending-commitments to track status.` + await unreadableForNote(unreadableFor ?? [], state2)
4851
4935
  }]
4852
4936
  };
4853
4937
  } catch (proposalErr) {
@@ -5414,17 +5498,54 @@ function listIntegrationsHandler(state2, integrationManager2) {
5414
5498
  // src/tools/commitments.ts
5415
5499
  import { ContentBindingError as ContentBindingError2 } from "@hap/core";
5416
5500
 
5417
- // src/lib/who-label.ts
5418
- var UNKNOWN_DECIDER = "the person";
5419
- async function resolveWhoLabel(archive, domainKey, userId) {
5420
- if (!archive) return UNKNOWN_DECIDER;
5501
+ // src/lib/proposal-content.ts
5502
+ import { createHash as createHash3 } from "crypto";
5503
+ import { canonicalize } from "@hap/core";
5504
+ var ProposalContentError = class extends Error {
5505
+ };
5506
+ function computeProposalContentHash(toolArgs) {
5507
+ return `sha256:${createHash3("sha256").update(canonicalize(toolArgs)).digest("hex")}`;
5508
+ }
5509
+ async function resolveProposalToolArgs(proposal) {
5510
+ if (proposal.toolArgs) return proposal.toolArgs;
5511
+ if (!proposal.content) {
5512
+ throw new ProposalContentError(`Proposal ${proposal.id} carries neither tool_args nor content \u2014 cannot execute.`);
5513
+ }
5514
+ let plaintext;
5421
5515
  try {
5422
- const dir = await getIdentityDirectory(archive);
5423
- const did = dir.domainDids.get(domainKey) ?? dir.domainDids.get(userId);
5424
- const name = did ? dir.names.get(did) : void 0;
5425
- return name ?? UNKNOWN_DECIDER;
5516
+ plaintext = await cpDecryptContent(proposal.content);
5517
+ } catch (err) {
5518
+ throw new ProposalContentError(`Could not decrypt the approved content on this device: ${err instanceof Error ? err.message : String(err)}`);
5519
+ }
5520
+ let parsed;
5521
+ try {
5522
+ parsed = JSON.parse(plaintext);
5426
5523
  } catch {
5427
- return UNKNOWN_DECIDER;
5524
+ throw new ProposalContentError(`Proposal ${proposal.id}: decrypted content is not valid JSON.`);
5525
+ }
5526
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
5527
+ throw new ProposalContentError(`Proposal ${proposal.id}: decrypted content is not a JSON object.`);
5528
+ }
5529
+ const toolArgs = parsed;
5530
+ if (proposal.content.hash) {
5531
+ const actual = computeProposalContentHash(toolArgs);
5532
+ if (actual !== proposal.content.hash) {
5533
+ throw new ProposalContentError(
5534
+ `Proposal ${proposal.id}: decrypted content does not match its own fingerprint \u2014 refusing to execute.`
5535
+ );
5536
+ }
5537
+ }
5538
+ return toolArgs;
5539
+ }
5540
+ async function decryptRejectionNote(by) {
5541
+ if (!by) return void 0;
5542
+ if (typeof by.note === "string") return by.note;
5543
+ if (!by.noteEncrypted) return void 0;
5544
+ try {
5545
+ return await cpDecryptContent(by.noteEncrypted);
5546
+ } catch (err) {
5547
+ console.error("[Suveren MCP] Failed to decrypt a change-needed note:", err);
5548
+ return "(could not decrypt this note on this device)";
5428
5549
  }
5429
5550
  }
5430
5551
 
@@ -5436,6 +5557,7 @@ var installedExecutor = null;
5436
5557
  function installCommittedExecutor(executor) {
5437
5558
  installedExecutor = executor;
5438
5559
  }
5560
+ var warnedSkippedProposalIds = /* @__PURE__ */ new Set();
5439
5561
  function runCommitted(proposal, state2, integrationManager2) {
5440
5562
  return installedExecutor ? installedExecutor.execute(proposal) : executeCommitted(proposal, state2, integrationManager2);
5441
5563
  }
@@ -5459,15 +5581,28 @@ async function executeCommitted(proposal, state2, integrationManager2) {
5459
5581
  const cachedAuth = state2.cache.getAllAuthorizations().find((a) => a.authorizationId === proposal.authorizationId);
5460
5582
  const submitted = state2.proposalSubmissions.get(proposal.id);
5461
5583
  if (!submitted) {
5462
- console.error(
5463
- `[Suveren MCP] Proposal ${proposal.id} is committed but has no local submission record on this gateway \u2014 leaving it for whichever gateway submitted it (this is normal for a different device of the same operator, or the AS lists a proposal we never submitted).`
5464
- );
5465
- return { text: buildSkippedProposalNote(proposal.id) };
5584
+ if (!warnedSkippedProposalIds.has(proposal.id)) {
5585
+ warnedSkippedProposalIds.add(proposal.id);
5586
+ console.error(
5587
+ `[Suveren MCP] Proposal ${proposal.id} is committed but has no local submission record on this gateway \u2014 leaving it for whichever gateway submitted it (this is normal for a different device of the same operator, or the AS lists a proposal we never submitted).`
5588
+ );
5589
+ }
5590
+ return { text: buildSkippedProposalNote(proposal.id), skipped: true };
5591
+ }
5592
+ let toolArgs;
5593
+ try {
5594
+ toolArgs = await resolveProposalToolArgs(proposal);
5595
+ } catch (err) {
5596
+ const msg = err instanceof ProposalContentError ? err.message : `Could not resolve the approved content on this device: ${err instanceof Error ? err.message : String(err)}`;
5597
+ return {
5598
+ text: `Proposal ${proposal.id}: blocked \u2014 ${msg} The action was not executed.`,
5599
+ isError: true
5600
+ };
5466
5601
  }
5467
5602
  {
5468
5603
  const mismatches = [];
5469
5604
  if (submitted.tool !== proposal.tool) mismatches.push("tool");
5470
- if (submitted.toolArgsHash !== hashToolArgs(proposal.toolArgs)) mismatches.push("arguments");
5605
+ if (submitted.toolArgsHash !== hashToolArgs(toolArgs)) mismatches.push("arguments");
5471
5606
  if (submitted.executionContextHash !== hashToolArgs(proposal.executionContext)) mismatches.push("executionContext");
5472
5607
  if (submitted.authorizationId !== proposal.authorizationId) mismatches.push("authorizationId");
5473
5608
  if (submitted.profileId !== proposal.profileId) mismatches.push("profileId");
@@ -5485,7 +5620,7 @@ async function executeCommitted(proposal, state2, integrationManager2) {
5485
5620
  const binding = computeContentBinding(
5486
5621
  proposal.profileId,
5487
5622
  discovered,
5488
- proposal.toolArgs,
5623
+ toolArgs,
5489
5624
  proposalActionType
5490
5625
  );
5491
5626
  const { receipt, idempotent } = await state2.spClient.postReceipt({
@@ -5499,7 +5634,12 @@ async function executeCommitted(proposal, state2, integrationManager2) {
5499
5634
  executionContext: proposal.executionContext,
5500
5635
  amount: typeof proposal.executionContext.amount === "number" ? proposal.executionContext.amount : void 0,
5501
5636
  proposalId: proposal.id,
5502
- toolArgs: proposal.toolArgs,
5637
+ toolArgs,
5638
+ // PE1 — the fingerprint of what THIS gateway resolved (decrypted, or
5639
+ // read plain on a legacy row) and is about to execute. The AS compares
5640
+ // it against its own stored content_hash and refuses without echoing
5641
+ // content on a mismatch.
5642
+ proposalContentHash: computeProposalContentHash(toolArgs),
5503
5643
  // Deliberately NO idempotencyKey: the spec has review-mode commits carry
5504
5644
  // a proposalId INSTEAD of a key — the proposal's committed→executed
5505
5645
  // transition is their replay protection, and the AS replays the ORIGINAL
@@ -5533,7 +5673,11 @@ async function executeCommitted(proposal, state2, integrationManager2) {
5533
5673
  attestations: cachedAuth?.attestations,
5534
5674
  // What the human actually approved, kept next to the receipt that cites
5535
5675
  // it — otherwise `proposalId` is a pointer to a server we may not have.
5536
- proposal,
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 },
5537
5681
  boundContent: binding?.boundContent
5538
5682
  });
5539
5683
  } catch (err) {
@@ -5592,7 +5736,7 @@ async function executeCommitted(proposal, state2, integrationManager2) {
5592
5736
  ticketId: receiptId,
5593
5737
  proposalId: proposal.id,
5594
5738
  tool: proposal.tool,
5595
- argsHash: hashToolArgs(proposal.toolArgs)
5739
+ argsHash: hashToolArgs(toolArgs)
5596
5740
  });
5597
5741
  if (!begun.ok) {
5598
5742
  const prior = begun.existing;
@@ -5617,7 +5761,7 @@ async function executeCommitted(proposal, state2, integrationManager2) {
5617
5761
  integrationManager2,
5618
5762
  integrationId,
5619
5763
  toolName,
5620
- proposal.toolArgs,
5764
+ toolArgs,
5621
5765
  submitted.previewHash
5622
5766
  );
5623
5767
  if (changed) {
@@ -5629,7 +5773,7 @@ async function executeCommitted(proposal, state2, integrationManager2) {
5629
5773
  }
5630
5774
  }
5631
5775
  try {
5632
- let outgoingArgs = proposal.toolArgs;
5776
+ let outgoingArgs = toolArgs;
5633
5777
  if (discovered && receiptId) {
5634
5778
  if (shouldAttachFooter()) {
5635
5779
  outgoingArgs = appendVerificationFooter(discovered, outgoingArgs, receiptId, cachedAuth?.subjects?.[0]);
@@ -5735,10 +5879,11 @@ Result: ${JSON.stringify(match.executionResult, null, 2)}` : " The action ran; t
5735
5879
  by.userId
5736
5880
  ) : "a person";
5737
5881
  if (outcome === "change_needed") {
5882
+ const note = await decryptRejectionNote(by);
5738
5883
  return {
5739
5884
  content: [{
5740
5885
  type: "text",
5741
- text: `Proposal ${match.id} was SENT BACK FOR A CHANGE by ${who}: "${by?.note ?? ""}". The action did not run. You may adjust it and propose again \u2014 a new proposal the person approves again. Do not resend the same values.`
5886
+ text: `Proposal ${match.id} was SENT BACK FOR A CHANGE by ${who}: "${note ?? ""}". The action did not run. You may adjust it and propose again \u2014 a new proposal the person approves again. Do not resend the same values.`
5742
5887
  }]
5743
5888
  };
5744
5889
  }
@@ -7823,13 +7968,13 @@ var reportBuiltin = (deps) => ({
7823
7968
  });
7824
7969
 
7825
7970
  // src/lib/cp-mandate.ts
7826
- var CP_PORT2 = process.env.SUVEREN_CP_PORT ?? "3402";
7827
- var CP_BASE2 = process.env.SUVEREN_CP_INTERNAL_URL ?? `http://127.0.0.1:${CP_PORT2}`;
7971
+ var CP_PORT3 = process.env.SUVEREN_CP_PORT ?? "3402";
7972
+ var CP_BASE3 = process.env.SUVEREN_CP_INTERNAL_URL ?? `http://127.0.0.1:${CP_PORT3}`;
7828
7973
  async function controlPlaneMandate(dryRun, request) {
7829
7974
  const secret = process.env.SUVEREN_INTERNAL_SECRET ?? "";
7830
7975
  if (!secret) return { ok: false, message: "the gateway is not fully started (no control plane connection)." };
7831
7976
  try {
7832
- const res = await fetch(`${CP_BASE2}/internal/mandate`, {
7977
+ const res = await fetch(`${CP_BASE3}/internal/mandate`, {
7833
7978
  method: "POST",
7834
7979
  headers: { "Content-Type": "application/json", "X-Internal-Secret": secret },
7835
7980
  body: JSON.stringify({ dryRun, request }),
@@ -8425,6 +8570,10 @@ import { decodeMandateBlob as decodeMandateBlob3 } from "@hap/core";
8425
8570
  // src/lib/committed-executor.ts
8426
8571
  import { existsSync as existsSync20, mkdirSync as mkdirSync15, readFileSync as readFileSync21, unlinkSync as unlinkSync7, writeFileSync as writeFileSync15 } from "fs";
8427
8572
  import { dirname as dirname15, join as join20 } from "path";
8573
+ function describeAutoExecutionLog(proposalId, tool, result) {
8574
+ if (result.skipped) return null;
8575
+ return result.isError ? `[Suveren MCP] Auto-exec proposal ${proposalId}: ${result.text}` : `[Suveren MCP] Auto-executed proposal ${proposalId}: ${tool}`;
8576
+ }
8428
8577
  var CommittedExecutor = class {
8429
8578
  constructor(run) {
8430
8579
  this.run = run;
@@ -9281,7 +9430,13 @@ app.listen(port, bindHost, () => {
9281
9430
  "domain" in by ? by.domain : by.userId,
9282
9431
  by.userId
9283
9432
  );
9284
- const entry = outcomeEntryFor(proposal, submittedHere, who, Date.now());
9433
+ const note = await decryptRejectionNote(by);
9434
+ const proposalWithDecryptedNote = {
9435
+ ...proposal,
9436
+ rejectedBy: proposal.rejectedBy ? { ...proposal.rejectedBy, note } : null,
9437
+ approverRejectedBy: proposal.approverRejectedBy ? { ...proposal.approverRejectedBy, note } : void 0
9438
+ };
9439
+ const entry = outcomeEntryFor(proposalWithDecryptedNote, submittedHere, who, Date.now());
9285
9440
  if (entry) state.outcomeOutbox.recordIfNew(entry);
9286
9441
  }
9287
9442
  } catch {
@@ -9297,10 +9452,9 @@ app.listen(port, bindHost, () => {
9297
9452
  const committed = await state.spClient.getCommittedProposals();
9298
9453
  for (const proposal of committed) {
9299
9454
  try {
9300
- const { text, isError } = await committedExecutor.execute(proposal);
9301
- console.error(
9302
- isError ? `[Suveren MCP] Auto-exec proposal ${proposal.id}: ${text}` : `[Suveren MCP] Auto-executed proposal ${proposal.id}: ${proposal.tool}`
9303
- );
9455
+ const result = await committedExecutor.execute(proposal);
9456
+ const line = describeAutoExecutionLog(proposal.id, proposal.tool, result);
9457
+ if (line) console.error(line);
9304
9458
  } catch (err) {
9305
9459
  console.error(`[Suveren MCP] Failed to execute proposal ${proposal.id}:`, err);
9306
9460
  }