@suveren/gateway 0.26.0 → 0.27.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/content/guides/6-risks.md +1 -1
- package/dist/control-plane/index.mjs +294 -19
- package/dist/mcp-server/http.mjs +195 -41
- package/dist/ui/assets/index-DKOr7_IY.js +120 -0
- package/dist/ui/assets/index-DeEzJCqI.css +1 -0
- package/dist/ui/index.html +2 -2
- package/package.json +1 -1
- package/dist/ui/assets/index-46UkxmPE.css +0 -1
- package/dist/ui/assets/index-BC5hwcsC.js +0 -120
package/dist/mcp-server/http.mjs
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
|
1006
|
-
throw new Error(
|
|
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
|
-
|
|
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
|
-
|
|
4718
|
+
let recipientUserIds = auth.deferredCommitmentDomains.filter((d) => d !== "owner");
|
|
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/
|
|
5418
|
-
|
|
5419
|
-
|
|
5420
|
-
|
|
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
|
-
|
|
5423
|
-
|
|
5424
|
-
|
|
5425
|
-
|
|
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
|
-
|
|
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
|
-
|
|
5463
|
-
|
|
5464
|
-
|
|
5465
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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 =
|
|
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}: "${
|
|
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
|
|
7827
|
-
var
|
|
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(`${
|
|
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 }),
|
|
@@ -8017,7 +8162,7 @@ var DELEGATION_INTENT_HINT = [
|
|
|
8017
8162
|
"",
|
|
8018
8163
|
"Goal \u2014 Follow the setup guide: interview me about the company, the team and the work; prepare test data from real cases, renamed to .example names; propose the mandates and the working instructions (agent brief) that the AI doing the work will need.",
|
|
8019
8164
|
"",
|
|
8020
|
-
"Watch out \u2014 You only propose; nothing happens until I approve it in the Suveren Gateway. Ask, don't guess: never invent a rule, a limit or a step. One proposal at a time. Steps customers would see start with my review. Never use real names or customer data
|
|
8165
|
+
"Watch out \u2014 You only propose; nothing happens until I approve it in the Suveren Gateway. Ask, don't guess: never invent a rule, a limit or a step. One proposal at a time. Steps customers would see start with my review. Never use real names or customer data in a test run \u2014 test data only. Never give the working AI a reporting mandate during a test run. Keep the setup and the working AI in separate conversations.",
|
|
8021
8166
|
"",
|
|
8022
8167
|
// DL1 — outside simulation, the same mandate also covers ongoing, real-system
|
|
8023
8168
|
// use: the Scope above (which profiles, which mode) is what actually bounds
|
|
@@ -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
|
|
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
|
|
9301
|
-
|
|
9302
|
-
|
|
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
|
}
|