@usesigil/kit 0.17.0 → 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.
- package/dist/agent-errors.d.ts +1 -1
- package/dist/agent-errors.d.ts.map +1 -1
- package/dist/agent-errors.js +14 -2
- package/dist/agent-errors.js.map +1 -1
- package/dist/cosign-helper.d.ts +26 -6
- package/dist/cosign-helper.d.ts.map +1 -1
- package/dist/cosign-helper.js +29 -9
- package/dist/cosign-helper.js.map +1 -1
- package/dist/create-vault.d.ts.map +1 -1
- package/dist/create-vault.js +10 -0
- package/dist/create-vault.js.map +1 -1
- package/dist/dashboard/index.d.ts +21 -0
- package/dist/dashboard/index.d.ts.map +1 -1
- package/dist/dashboard/index.js +53 -0
- package/dist/dashboard/index.js.map +1 -1
- package/dist/dashboard/mutations.d.ts +82 -5
- package/dist/dashboard/mutations.d.ts.map +1 -1
- package/dist/dashboard/mutations.js +326 -82
- package/dist/dashboard/mutations.js.map +1 -1
- package/dist/dashboard/types.d.ts +24 -2
- package/dist/dashboard/types.d.ts.map +1 -1
- package/dist/errors/agent-errors.generated.d.ts +2 -2
- package/dist/errors/agent-errors.generated.d.ts.map +1 -1
- package/dist/errors/agent-errors.generated.js +5 -4
- package/dist/errors/agent-errors.generated.js.map +1 -1
- package/dist/events.d.ts.map +1 -1
- package/dist/events.js +2 -1
- package/dist/events.js.map +1 -1
- package/dist/generated/accounts/pendingPolicyUpdate.d.ts +52 -0
- package/dist/generated/accounts/pendingPolicyUpdate.d.ts.map +1 -1
- package/dist/generated/accounts/pendingPolicyUpdate.js +6 -0
- package/dist/generated/accounts/pendingPolicyUpdate.js.map +1 -1
- package/dist/generated/errors/sigil.d.ts +3 -1
- package/dist/generated/errors/sigil.d.ts.map +1 -1
- package/dist/generated/errors/sigil.js +3 -0
- package/dist/generated/errors/sigil.js.map +1 -1
- package/dist/generated/event-discriminators.d.ts.map +1 -1
- package/dist/generated/event-discriminators.js +2 -1
- package/dist/generated/event-discriminators.js.map +1 -1
- package/dist/generated/instructions/acceptOwnershipTransferMultisig.d.ts +12 -69
- package/dist/generated/instructions/acceptOwnershipTransferMultisig.d.ts.map +1 -1
- package/dist/generated/instructions/acceptOwnershipTransferMultisig.js.map +1 -1
- package/dist/generated/instructions/agentTransfer.d.ts +33 -7
- package/dist/generated/instructions/agentTransfer.d.ts.map +1 -1
- package/dist/generated/instructions/agentTransfer.js +37 -2
- package/dist/generated/instructions/agentTransfer.js.map +1 -1
- package/dist/generated/instructions/approvePendingPolicy.d.ts +58 -0
- package/dist/generated/instructions/approvePendingPolicy.d.ts.map +1 -0
- package/dist/generated/instructions/approvePendingPolicy.js +122 -0
- package/dist/generated/instructions/approvePendingPolicy.js.map +1 -0
- package/dist/generated/instructions/cancelAgentPermissionsUpdate.d.ts +38 -5
- package/dist/generated/instructions/cancelAgentPermissionsUpdate.d.ts.map +1 -1
- package/dist/generated/instructions/cancelAgentPermissionsUpdate.js +43 -4
- package/dist/generated/instructions/cancelAgentPermissionsUpdate.js.map +1 -1
- package/dist/generated/instructions/finalizeSession.d.ts +24 -3
- package/dist/generated/instructions/finalizeSession.d.ts.map +1 -1
- package/dist/generated/instructions/finalizeSession.js.map +1 -1
- package/dist/generated/instructions/index.d.ts +1 -0
- package/dist/generated/instructions/index.d.ts.map +1 -1
- package/dist/generated/instructions/index.js +1 -0
- package/dist/generated/instructions/index.js.map +1 -1
- package/dist/generated/programs/sigil.d.ts +31 -27
- package/dist/generated/programs/sigil.d.ts.map +1 -1
- package/dist/generated/programs/sigil.js +39 -27
- package/dist/generated/programs/sigil.js.map +1 -1
- package/dist/generated/types/auditEntry.d.ts +2 -0
- package/dist/generated/types/auditEntry.d.ts.map +1 -1
- package/dist/generated/types/auditEntry.js.map +1 -1
- package/dist/generated/types/index.d.ts +1 -0
- package/dist/generated/types/index.d.ts.map +1 -1
- package/dist/generated/types/index.js +1 -0
- package/dist/generated/types/index.js.map +1 -1
- package/dist/generated/types/policyCosignApproved.d.ts +27 -0
- package/dist/generated/types/policyCosignApproved.d.ts.map +1 -0
- package/dist/generated/types/policyCosignApproved.js +26 -0
- package/dist/generated/types/policyCosignApproved.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/kit-adapter.d.ts +1 -1
- package/dist/kit-adapter.d.ts.map +1 -1
- package/dist/kit-adapter.js +1 -1
- package/dist/kit-adapter.js.map +1 -1
- package/dist/policy/compute-agent-perms-cosign-digest.d.ts +51 -0
- package/dist/policy/compute-agent-perms-cosign-digest.d.ts.map +1 -0
- package/dist/policy/compute-agent-perms-cosign-digest.js +55 -0
- package/dist/policy/compute-agent-perms-cosign-digest.js.map +1 -0
- package/dist/policy/compute-cosign-digest.d.ts +9 -7
- package/dist/policy/compute-cosign-digest.d.ts.map +1 -1
- package/dist/policy/compute-cosign-digest.js +9 -7
- package/dist/policy/compute-cosign-digest.js.map +1 -1
- package/dist/policy/compute-policy-preview-digest.d.ts +48 -27
- package/dist/policy/compute-policy-preview-digest.d.ts.map +1 -1
- package/dist/policy/compute-policy-preview-digest.js +49 -27
- package/dist/policy/compute-policy-preview-digest.js.map +1 -1
- package/dist/testing/errors/names.generated.d.ts +1 -0
- package/dist/testing/errors/names.generated.d.ts.map +1 -1
- package/dist/testing/errors/names.generated.js +2 -1
- package/dist/testing/errors/names.generated.js.map +1 -1
- package/package.json +1 -1
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import { getProgramDerivedAddress, getAddressEncoder } from "../kit-adapter.js";
|
|
8
8
|
import { getSigilModuleLogger } from "../logger.js";
|
|
9
|
-
import { pipe, createTransactionMessage, setTransactionMessageFeePayer, setTransactionMessageLifetimeUsingBlockhash, appendTransactionMessageInstructions, addSignersToTransactionMessage, signTransactionMessageWithSigners, getBase64EncodedWireTransaction, } from "../kit-adapter.js";
|
|
9
|
+
import { pipe, createTransactionMessage, setTransactionMessageFeePayer, setTransactionMessageLifetimeUsingBlockhash, appendTransactionMessageInstructions, addSignersToTransactionMessage, signTransactionMessageWithSigners, partiallySignTransactionMessageWithSigners, getBase64EncodedWireTransaction, } from "../kit-adapter.js";
|
|
10
10
|
import { getSetComputeUnitLimitInstruction, getSetComputeUnitPriceInstruction, } from "@solana-program/compute-budget";
|
|
11
11
|
import { sendAndConfirmTransaction, getBlockhashCache, } from "../rpc-helpers.js";
|
|
12
12
|
import { AccountRole } from "../kit-adapter.js";
|
|
@@ -16,7 +16,9 @@ import { redactCause } from "../network-errors.js";
|
|
|
16
16
|
import { SIGIL_PROGRAM_ADDRESS, MAX_ALLOWED_PROTOCOLS } from "../types.js";
|
|
17
17
|
import { fetchAgentVault } from "../generated/accounts/agentVault.js";
|
|
18
18
|
import { fetchPolicyConfig } from "../generated/accounts/policyConfig.js";
|
|
19
|
-
import { computePolicyPreviewDigest } from "../policy/compute-policy-preview-digest.js";
|
|
19
|
+
import { computePolicyPreviewDigest, computeAgentSetHash, } from "../policy/compute-policy-preview-digest.js";
|
|
20
|
+
import { computeAgentPermsCosignDigest } from "../policy/compute-agent-perms-cosign-digest.js";
|
|
21
|
+
import { computeCosignDigest } from "../policy/compute-cosign-digest.js";
|
|
20
22
|
// Phase 3: Simple mutations
|
|
21
23
|
import { getFreezeVaultInstructionAsync } from "../generated/instructions/freezeVault.js";
|
|
22
24
|
import { getReactivateVaultInstructionAsync } from "../generated/instructions/reactivateVault.js";
|
|
@@ -38,7 +40,7 @@ import { getApplyPendingPolicyInstructionAsync } from "../generated/instructions
|
|
|
38
40
|
import { getCancelPendingPolicyInstructionAsync } from "../generated/instructions/cancelPendingPolicy.js";
|
|
39
41
|
import { getQueueAgentPermissionsUpdateInstructionAsync } from "../generated/instructions/queueAgentPermissionsUpdate.js";
|
|
40
42
|
import { getApplyAgentPermissionsUpdateInstructionAsync } from "../generated/instructions/applyAgentPermissionsUpdate.js";
|
|
41
|
-
import {
|
|
43
|
+
import { getCancelAgentPermissionsUpdateInstructionAsync } from "../generated/instructions/cancelAgentPermissionsUpdate.js";
|
|
42
44
|
import { getCreatePostAssertionsInstructionAsync } from "../generated/instructions/createPostAssertions.js";
|
|
43
45
|
import { getClosePostAssertionsInstructionAsync } from "../generated/instructions/closePostAssertions.js";
|
|
44
46
|
// M-2 (pre-redeploy audit 2026-05-21): Phase 8 ownership-transfer ix builders.
|
|
@@ -147,12 +149,38 @@ async function siblingHandlerExpectedDigest(rpc, vault, override) {
|
|
|
147
149
|
// this via `queue_policy_update` only.
|
|
148
150
|
cosignRequired: livePolicy.data.cosignRequired,
|
|
149
151
|
// D-5 (Bucket 2 audit 2026-05-21, F-RP3-1): pass-through from live
|
|
150
|
-
// policy. Position
|
|
152
|
+
// policy. Position 21 of the canonical TA-19 digest. Sibling handlers
|
|
151
153
|
// never mutate this — owner sets via queue_policy_update only.
|
|
152
154
|
cosignSessionPubkey: livePolicy.data.cosignSessionPubkey,
|
|
155
|
+
// M-1 (audit 2026-06-11): per-protocol caps (positions 23-24). Sibling
|
|
156
|
+
// handlers never mutate the caps — pass-through from live policy so the
|
|
157
|
+
// re-bind digest matches the on-chain recompute (create_post_assertions
|
|
158
|
+
// .rs:138-139 / close_post_assertions.rs read policy.has_protocol_caps +
|
|
159
|
+
// policy.protocol_caps).
|
|
160
|
+
hasProtocolCaps: livePolicy.data.hasProtocolCaps,
|
|
161
|
+
protocolCaps: livePolicy.data.protocolCaps,
|
|
162
|
+
// HIGH (audit 2026-06-11 follow-up): create_post_assertions.rs:129 and
|
|
163
|
+
// close_post_assertions.rs recompute agent_set_hash from the LIVE vault
|
|
164
|
+
// agents, and :136 reads operator_grant_delay_seconds from live policy.
|
|
165
|
+
// Omitting them here defaulted the digest to EMPTY_AGENT_SET_HASH / 0n,
|
|
166
|
+
// mismatching the on-chain recompute (PolicyPreviewMismatch) for ANY vault
|
|
167
|
+
// with >=1 agent or a non-zero operator-grant delay — i.e. every real vault.
|
|
168
|
+
// vault.agents is the active-agent Vec (register pushes; owner-revoke
|
|
169
|
+
// removes the entry, auto-revoke zeroes its capability in place — either
|
|
170
|
+
// way membership matches the on-chain Vec), mapped 1:1 by
|
|
171
|
+
// computeAgentSetHash (mirrors compute_agent_set_hash).
|
|
172
|
+
agentSetHash: computeAgentSetHash(liveVault.data.agents),
|
|
173
|
+
operatorGrantDelaySeconds: livePolicy.data.operatorGrantDelaySeconds,
|
|
153
174
|
});
|
|
154
175
|
}
|
|
155
|
-
async function run(rpc, owner, network, instructions, opts = {}
|
|
176
|
+
async function run(rpc, owner, network, instructions, opts = {},
|
|
177
|
+
// Elevated-cosign surface (audit 2026-06-12): additional signers beyond the
|
|
178
|
+
// owner (e.g. a cosign-session signer for an elevated queue mutation). The
|
|
179
|
+
// cosigner must ALSO be present in the instruction's account metas as a
|
|
180
|
+
// readonly-signer (the elevated wrappers append it); attaching it here makes
|
|
181
|
+
// its signature land in the wire tx. Default [] preserves the owner-only path
|
|
182
|
+
// for every existing non-elevated caller.
|
|
183
|
+
cosigners = []) {
|
|
156
184
|
try {
|
|
157
185
|
const cu = opts.computeUnits ?? CU_OWNER_ACTION;
|
|
158
186
|
const allIx = [
|
|
@@ -171,7 +199,7 @@ async function run(rpc, owner, network, instructions, opts = {}) {
|
|
|
171
199
|
const cache = getBlockhashCache(rpc);
|
|
172
200
|
const blockhash = await cache.get(rpc);
|
|
173
201
|
const txMessage = pipe(createTransactionMessage({ version: 0 }), (tx) => setTransactionMessageFeePayer(owner.address, tx), (tx) => setTransactionMessageLifetimeUsingBlockhash(blockhash, tx), (tx) => appendTransactionMessageInstructions(allIx, tx));
|
|
174
|
-
const txWithSigners = addSignersToTransactionMessage([owner], txMessage);
|
|
202
|
+
const txWithSigners = addSignersToTransactionMessage([owner, ...cosigners], txMessage);
|
|
175
203
|
const signedTx = await signTransactionMessageWithSigners(txWithSigners);
|
|
176
204
|
const wire = getBase64EncodedWireTransaction(signedTx);
|
|
177
205
|
const signature = await sendAndConfirmTransaction(rpc, wire);
|
|
@@ -326,7 +354,7 @@ export async function cancelAgentGrant(rpc, vault, owner, network, opts) {
|
|
|
326
354
|
* created/destroyed between the read and TX execution, the on-chain program
|
|
327
355
|
* will reject the TX. This is a known race window — retry on failure.
|
|
328
356
|
*/
|
|
329
|
-
|
|
357
|
+
async function buildCloseVaultFinalIx(rpc, vault, owner, network) {
|
|
330
358
|
const net = network === "mainnet" ? "mainnet-beta" : "devnet";
|
|
331
359
|
// Resolve vault state to determine which remaining_accounts are needed
|
|
332
360
|
const state = await resolveVaultStateForOwner(rpc, vault, undefined, net);
|
|
@@ -420,6 +448,16 @@ export async function closeVault(rpc, vault, owner, network, opts) {
|
|
|
420
448
|
],
|
|
421
449
|
}
|
|
422
450
|
: ix;
|
|
451
|
+
return finalIx;
|
|
452
|
+
}
|
|
453
|
+
/**
|
|
454
|
+
* Close a vault (owner-only path — for vaults WITHOUT cosign). On a
|
|
455
|
+
* cosign-required vault the on-chain `close_vault` rejects an owner-only close
|
|
456
|
+
* with `ErrCosignRequired`; use {@link buildCloseVaultElevated} (partial-sign,
|
|
457
|
+
* bound cosigner co-signs) instead.
|
|
458
|
+
*/
|
|
459
|
+
export async function closeVault(rpc, vault, owner, network, opts) {
|
|
460
|
+
const finalIx = await buildCloseVaultFinalIx(rpc, vault, owner, network);
|
|
423
461
|
return run(rpc, owner, network, [finalIx], {
|
|
424
462
|
computeUnits: opts?.computeUnits ?? 400_000,
|
|
425
463
|
priorityFeeMicroLamports: opts?.priorityFeeMicroLamports,
|
|
@@ -525,6 +563,37 @@ export async function withdraw(rpc, vault, owner, network, mint, amount, opts) {
|
|
|
525
563
|
* - `sessionExpirySeconds` range (5..=90 when > 0; audit F5-H1)
|
|
526
564
|
*/
|
|
527
565
|
export async function queuePolicyUpdate(rpc, vault, owner, network, changes, opts) {
|
|
566
|
+
// Non-elevated path: cosign_session = Pubkey::default(), no cosigner in
|
|
567
|
+
// remaining_accounts, owner-only signature. Shares buildPolicyUpdateIx (the
|
|
568
|
+
// merged-effective projection + TA-19 digest) with queuePolicyElevated — the
|
|
569
|
+
// single source of truth that prevents digest drift between the two surfaces.
|
|
570
|
+
// An elevated change submitted here (e.g. raising a cap on a cosign_required
|
|
571
|
+
// vault) fails closed on-chain with ErrCosignRequired; use queuePolicyElevated.
|
|
572
|
+
const ix = await buildPolicyUpdateIx(rpc, owner, vault, changes, DEFAULT_COSIGN_SESSION);
|
|
573
|
+
return run(rpc, owner, network, [ix], opts);
|
|
574
|
+
}
|
|
575
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
576
|
+
// Elevated-cosign surface (audit 2026-06-12) — policy path.
|
|
577
|
+
//
|
|
578
|
+
// queuePolicyElevated / buildQueuePolicyElevated mirror the agent-perms pair for
|
|
579
|
+
// policy changes. They share buildPolicyUpdateIx with queuePolicyUpdate (DRY —
|
|
580
|
+
// the single source of truth for the merged-effective projection + TA-19 digest;
|
|
581
|
+
// duplicating it is the exact digest-drift failure mode the 2026-06-11 audit
|
|
582
|
+
// fixed). The only difference between non-elevated and elevated is the
|
|
583
|
+
// cosign_session arg (default vs a real cosigner pubkey) + the elevated-only
|
|
584
|
+
// change fields, all plumbed through `eff = changes.X ?? live`.
|
|
585
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
586
|
+
/** Pubkey::default() (System Program) — the non-elevated cosign_session arg. */
|
|
587
|
+
const DEFAULT_COSIGN_SESSION = "11111111111111111111111111111111";
|
|
588
|
+
/**
|
|
589
|
+
* Shared queue_policy_update instruction builder. Validates `changes`, fetches
|
|
590
|
+
* live policy+vault, projects the merged-effective policy (`eff = changes.X ??
|
|
591
|
+
* live.X` for EVERY field, so omitted fields fall through to live — the
|
|
592
|
+
* non-elevated path is byte-identical to the prior inline impl), computes the
|
|
593
|
+
* TA-19 digest over it, and builds the ix with the supplied `cosignSession`
|
|
594
|
+
* (DEFAULT_COSIGN_SESSION = non-elevated; a real cosigner pubkey = elevated).
|
|
595
|
+
*/
|
|
596
|
+
async function buildPolicyUpdateIx(rpc, owner, vault, changes, cosignSession) {
|
|
528
597
|
if (Object.keys(changes).length === 0) {
|
|
529
598
|
throw toDxError(new Error("At least one policy change is required"));
|
|
530
599
|
}
|
|
@@ -542,10 +611,6 @@ export async function queuePolicyUpdate(rpc, vault, owner, network, changes, opt
|
|
|
542
611
|
changes.approvedApps.length > MAX_ALLOWED_PROTOCOLS) {
|
|
543
612
|
throw toDxError(new Error(`approvedApps length exceeds on-chain MAX_ALLOWED_PROTOCOLS (${MAX_ALLOWED_PROTOCOLS}). Got ${changes.approvedApps.length}. On-chain rejects TooManyAllowedProtocols.`));
|
|
544
613
|
}
|
|
545
|
-
// Phase 2 TA-19: fetch live policy + vault state to compute the digest of
|
|
546
|
-
// the merged-effective policy that WILL result if this update is applied.
|
|
547
|
-
// The on-chain handler re-asserts the same digest at queue time, so any
|
|
548
|
-
// owner blind-sign that diverges from the SDK-projected update is rejected.
|
|
549
614
|
const [policyPda] = await getPolicyPDA(vault);
|
|
550
615
|
const livePolicy = await fetchPolicyConfig(rpc, policyPda);
|
|
551
616
|
const liveVault = await fetchAgentVault(rpc, vault);
|
|
@@ -559,13 +624,21 @@ export async function queuePolicyUpdate(rpc, vault, owner, network, changes, opt
|
|
|
559
624
|
const effDaily = changes.dailyCap ?? livePolicy.data.dailySpendingCapUsd;
|
|
560
625
|
const effMaxTx = changes.maxPerTrade ?? livePolicy.data.maxTransactionSizeUsd;
|
|
561
626
|
const effMaxSlip = changes.maxSlippageBps ?? livePolicy.data.maxSlippageBps;
|
|
562
|
-
// PEN-CROSS-6: developer_fee_rate is now part of the digest. Project the
|
|
563
|
-
// merged-effective value the same way as other Option<…> fields.
|
|
564
627
|
const effDeveloperFeeRate = changes.developerFeeRate ?? livePolicy.data.developerFeeRate;
|
|
565
628
|
const effTimelock = changes.timelock != null
|
|
566
629
|
? BigInt(changes.timelock)
|
|
567
630
|
: livePolicy.data.timelockDuration;
|
|
568
631
|
const effSessionExpiry = changes.sessionExpirySeconds ?? livePolicy.data.sessionExpirySeconds;
|
|
632
|
+
const effHasProtocolCaps = changes.hasProtocolCaps ?? livePolicy.data.hasProtocolCaps;
|
|
633
|
+
const effProtocolCaps = changes.protocolCaps ?? livePolicy.data.protocolCaps;
|
|
634
|
+
// Elevated-only fields (audit 2026-06-12): same merged-effective projection.
|
|
635
|
+
// Undefined ⇒ live pass-through, so queuePolicyUpdate's digest is unchanged.
|
|
636
|
+
const effStableFloor = changes.stableBalanceFloor ?? livePolicy.data.stableBalanceFloor;
|
|
637
|
+
const effPerRecip = changes.perRecipientDailyCapUsd ?? livePolicy.data.perRecipientDailyCapUsd;
|
|
638
|
+
const effCosignRequired = changes.cosignRequired ?? livePolicy.data.cosignRequired;
|
|
639
|
+
const effCosignSessionPubkey = changes.cosignSessionPubkey ?? livePolicy.data.cosignSessionPubkey;
|
|
640
|
+
const effOperatorDelay = changes.operatorGrantDelaySeconds ??
|
|
641
|
+
livePolicy.data.operatorGrantDelaySeconds;
|
|
569
642
|
const newPolicyPreviewDigest = computePolicyPreviewDigest({
|
|
570
643
|
dailySpendingCapUsd: effDaily,
|
|
571
644
|
maxTransactionSizeUsd: effMaxTx,
|
|
@@ -579,31 +652,18 @@ export async function queuePolicyUpdate(rpc, vault, owner, network, changes, opt
|
|
|
579
652
|
sessionExpirySeconds: effSessionExpiry,
|
|
580
653
|
observeOnly: liveVault.data.observeOnly,
|
|
581
654
|
hasPostAssertions: livePolicy.data.hasPostAssertions,
|
|
582
|
-
// PEN-CROSS-2: created_at_slot is immutable post-init — read from live.
|
|
583
655
|
createdAtSlot: livePolicy.data.createdAtSlot,
|
|
584
|
-
// TA-05 (Phase 3): operating_hours is policy-owned and bound by TA-19.
|
|
585
|
-
// queueAgentPermissions does not currently mutate it through the
|
|
586
|
-
// dashboard mutation surface — read from live policy.
|
|
587
656
|
operatingHours: livePolicy.data.operatingHours,
|
|
588
|
-
// TA-07/17 (Phase 3): same — not mutated by this dashboard surface.
|
|
589
657
|
autoPromoteGrays: livePolicy.data.autoPromoteGrays,
|
|
590
658
|
autoRevokeThreshold: livePolicy.data.autoRevokeThreshold,
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
// be done non-elevated by passing the override directly through the
|
|
600
|
-
// ix arg below — but this dashboard helper keeps the policy stable
|
|
601
|
-
// for the default path).
|
|
602
|
-
cosignRequired: livePolicy.data.cosignRequired,
|
|
603
|
-
// F-Q6 (2026-06-02): operator_grant_delay not mutated by this dashboard
|
|
604
|
-
// surface — pass-through from live policy so the digest matches the
|
|
605
|
-
// on-chain merged (eff) value at canonical position 22.
|
|
606
|
-
operatorGrantDelaySeconds: livePolicy.data.operatorGrantDelaySeconds,
|
|
659
|
+
stableBalanceFloor: effStableFloor,
|
|
660
|
+
perRecipientDailyCapUsd: effPerRecip,
|
|
661
|
+
cosignRequired: effCosignRequired,
|
|
662
|
+
operatorGrantDelaySeconds: effOperatorDelay,
|
|
663
|
+
hasProtocolCaps: effHasProtocolCaps,
|
|
664
|
+
protocolCaps: effProtocolCaps,
|
|
665
|
+
agentSetHash: computeAgentSetHash(liveVault.data.agents),
|
|
666
|
+
cosignSessionPubkey: effCosignSessionPubkey,
|
|
607
667
|
});
|
|
608
668
|
const ix = await getQueuePolicyUpdateInstructionAsync({
|
|
609
669
|
owner,
|
|
@@ -620,55 +680,55 @@ export async function queuePolicyUpdate(rpc, vault, owner, network, changes, opt
|
|
|
620
680
|
hasProtocolCaps: changes.hasProtocolCaps ?? null,
|
|
621
681
|
protocolCaps: changes.protocolCaps ?? null,
|
|
622
682
|
destinationMode: changes.destinationMode ?? null,
|
|
623
|
-
// TA-05 (Phase 3): operating_hours is not mutated by this mutation
|
|
624
|
-
// surface — pass null to fall through to live policy at on-chain merge.
|
|
625
683
|
operatingHours: null,
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
// G6 (audit 2026-05-18 cosign opt-in): not mutated by this non-
|
|
633
|
-
// elevated surface — pass null to fall through to live policy.
|
|
634
|
-
// Toggling cosign on/off goes through a dedicated path that is
|
|
635
|
-
// aware of the one-way-ratchet semantics (true→false requires
|
|
636
|
-
// cosign; false→true does not).
|
|
637
|
-
cosignRequired: null,
|
|
638
|
-
// D-5 (Bucket 2 audit 2026-05-21, F-RP3-1): not mutated by this
|
|
639
|
-
// non-elevated surface — pass null to keep live policy value. Owner
|
|
640
|
-
// sets cosign_session_pubkey via a dedicated elevated helper that
|
|
641
|
-
// verifies the new pubkey isn't a Sigil-protected PDA at queue time.
|
|
642
|
-
cosignSessionPubkey: null,
|
|
643
|
-
// F-Q6 (2026-06-02): not mutated by this dashboard surface — pass null
|
|
644
|
-
// (falls through to live policy at on-chain merge). Configurability is
|
|
645
|
-
// available via the raw codama builder + owner paths.
|
|
646
|
-
operatorGrantDelaySeconds: null,
|
|
647
|
-
// TA-09 (Phase 3): non-elevated path by default — pass the
|
|
648
|
-
// System Program / zero-pubkey ("11111111111111111111111111111111").
|
|
649
|
-
// Elevated mutations through this dashboard surface require a
|
|
650
|
-
// follow-on `queuePolicyElevated()` helper (cosign-helper.ts, G4).
|
|
651
|
-
//
|
|
652
|
-
// CANONICAL `cosign_session` ARG CONTRACT (Round 2 §RP-2 B4 F-3,
|
|
653
|
-
// 2026-05-19) — for non-Codama callers reading this file as a
|
|
654
|
-
// reference impl:
|
|
655
|
-
// - Non-elevated queue (this branch): pass `Pubkey::default()`
|
|
656
|
-
// and OMIT any cosigner from `remaining_accounts`.
|
|
657
|
-
// - Elevated queue (raising daily_cap, expanding destinations /
|
|
658
|
-
// protocols, lowering stable_balance_floor, raising
|
|
659
|
-
// per_recipient_daily_cap_usd, disabling protocol_caps, mutating
|
|
660
|
-
// protocol_caps entries, or disabling cosign): pass a REAL session
|
|
661
|
-
// pubkey + include it in `remaining_accounts` with
|
|
662
|
-
// `is_signer == true`. Build the bundle via
|
|
663
|
-
// `buildCosignBundle()` in `sdk/kit/src/cosign-helper.ts`.
|
|
664
|
-
// - Reject path: a non-default `cosign_session` on a non-elevated
|
|
665
|
-
// queue surfaces `InvalidPermissions` (6088). INTENTIONAL — the
|
|
666
|
-
// on-chain handler refuses to silently downgrade a caller's
|
|
667
|
-
// declared intent (Option A behaviour).
|
|
668
|
-
cosignSession: "11111111111111111111111111111111",
|
|
684
|
+
stableBalanceFloor: changes.stableBalanceFloor ?? null,
|
|
685
|
+
perRecipientDailyCapUsd: changes.perRecipientDailyCapUsd ?? null,
|
|
686
|
+
cosignRequired: changes.cosignRequired ?? null,
|
|
687
|
+
cosignSessionPubkey: changes.cosignSessionPubkey ?? null,
|
|
688
|
+
operatorGrantDelaySeconds: changes.operatorGrantDelaySeconds ?? null,
|
|
689
|
+
cosignSession,
|
|
669
690
|
newPolicyPreviewDigest,
|
|
670
691
|
});
|
|
671
|
-
return
|
|
692
|
+
return ix;
|
|
693
|
+
}
|
|
694
|
+
/**
|
|
695
|
+
* Elevated policy queue — single-builder dual-sign. Caller holds the cosigner
|
|
696
|
+
* key; signs [owner, cosigner] + sends. For true 2-party async use
|
|
697
|
+
* buildQueuePolicyElevated.
|
|
698
|
+
*/
|
|
699
|
+
export async function queuePolicyElevated(rpc, vault, owner, network, changes, cosigner, opts) {
|
|
700
|
+
requireValidAddress(cosigner.address, "Cosigner address");
|
|
701
|
+
if (cosigner.address === owner.address) {
|
|
702
|
+
throw toDxError(new Error("Cosigner must be distinct from the owner (on-chain ErrCosignRequired)"));
|
|
703
|
+
}
|
|
704
|
+
const ix = await buildPolicyUpdateIx(rpc, owner, vault, changes, cosigner.address);
|
|
705
|
+
return run(rpc, owner, network, [withCosignerSigner(ix, cosigner.address)], opts, [cosigner]);
|
|
706
|
+
}
|
|
707
|
+
/**
|
|
708
|
+
* Elevated policy queue — partial-sign handoff. Owner-signs + returns the
|
|
709
|
+
* partial tx + the policy cosign digest for the cosigner to complete + send.
|
|
710
|
+
* The cosign digest binds the RAW queued args (mirrors compute_cosign_digest).
|
|
711
|
+
*/
|
|
712
|
+
export async function buildQueuePolicyElevated(rpc, vault, owner, changes, cosignSession, opts) {
|
|
713
|
+
requireValidAddress(cosignSession, "Cosigner address");
|
|
714
|
+
if (cosignSession === owner.address) {
|
|
715
|
+
throw toDxError(new Error("Cosigner must be distinct from the owner (on-chain ErrCosignRequired)"));
|
|
716
|
+
}
|
|
717
|
+
const ix = await buildPolicyUpdateIx(rpc, owner, vault, changes, cosignSession);
|
|
718
|
+
const partialTransactionBase64 = await buildOwnerPartialSignedTx(rpc, owner, [withCosignerSigner(ix, cosignSession)], opts);
|
|
719
|
+
const cosignDigest = computeCosignDigest({
|
|
720
|
+
cosignSession,
|
|
721
|
+
dailySpendingCapUsd: changes.dailyCap ?? null,
|
|
722
|
+
maxTransactionAmountUsd: changes.maxPerTrade ?? null,
|
|
723
|
+
allowedDestinations: changes.allowedDestinations ?? null,
|
|
724
|
+
protocols: changes.approvedApps ?? null,
|
|
725
|
+
stableBalanceFloor: changes.stableBalanceFloor ?? null,
|
|
726
|
+
perRecipientDailyCapUsd: changes.perRecipientDailyCapUsd ?? null,
|
|
727
|
+
hasProtocolCaps: changes.hasProtocolCaps ?? null,
|
|
728
|
+
protocolCaps: changes.protocolCaps ?? null,
|
|
729
|
+
cosignRequired: changes.cosignRequired ?? null,
|
|
730
|
+
});
|
|
731
|
+
return { partialTransactionBase64, cosignSession, cosignDigest };
|
|
672
732
|
}
|
|
673
733
|
export async function applyPendingPolicy(rpc, vault, owner, network, opts) {
|
|
674
734
|
const ix = await getApplyPendingPolicyInstructionAsync({ owner, vault });
|
|
@@ -710,13 +770,118 @@ cooldownSeconds = 0n) {
|
|
|
710
770
|
// pass a REAL session pubkey + include it as a signer in
|
|
711
771
|
// `remaining_accounts`.
|
|
712
772
|
// - Reject path: passing a non-default `cosign_session` on a
|
|
713
|
-
// non-elevated queue surfaces `InvalidPermissions` (
|
|
773
|
+
// non-elevated queue surfaces `InvalidPermissions` (6036).
|
|
714
774
|
// INTENTIONAL — the on-chain handler refuses to silently
|
|
715
775
|
// downgrade a caller's declared intent (Option A behaviour).
|
|
716
776
|
cosignSession: "11111111111111111111111111111111",
|
|
717
777
|
});
|
|
718
778
|
return run(rpc, owner, network, [ix], opts);
|
|
719
779
|
}
|
|
780
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
781
|
+
// Elevated-cosign surface (audit 2026-06-12).
|
|
782
|
+
//
|
|
783
|
+
// On a `cosign_required` vault, raising an agent's capability or spending limit,
|
|
784
|
+
// or setting a non-zero cooldown, is an ELEVATED mutation: the on-chain
|
|
785
|
+
// queue_agent_permissions_update handler requires a non-default `cosign_session`
|
|
786
|
+
// that is (a) distinct from the owner and (b) present as a signer in
|
|
787
|
+
// remaining_accounts. Two caller models:
|
|
788
|
+
// - queueAgentPermissionsElevated(...) single-builder dual-sign: caller
|
|
789
|
+
// supplies the cosigner as a TransactionSigner; we sign [owner, cosigner].
|
|
790
|
+
// - buildQueueAgentPermissionsElevated(...) partial-sign handoff: caller
|
|
791
|
+
// supplies only the cosigner PUBKEY; we owner-partial-sign and return the
|
|
792
|
+
// base64 partial tx + the cosign digest for the cosigner to complete + send.
|
|
793
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
794
|
+
/** Append a cosign-session signer to a generated instruction's account metas. */
|
|
795
|
+
function withCosignerSigner(ix, cosignSession) {
|
|
796
|
+
return {
|
|
797
|
+
...ix,
|
|
798
|
+
accounts: [
|
|
799
|
+
...(ix.accounts ?? []),
|
|
800
|
+
{ address: cosignSession, role: AccountRole.READONLY_SIGNER },
|
|
801
|
+
],
|
|
802
|
+
};
|
|
803
|
+
}
|
|
804
|
+
/**
|
|
805
|
+
* Assemble the compute-budget-prefixed message and OWNER-partial-sign it (the
|
|
806
|
+
* cosigner — a required signer via the appended account meta — signs later).
|
|
807
|
+
* Returns the base64 wire transaction for handoff. Mirrors run()'s message
|
|
808
|
+
* assembly but does NOT send.
|
|
809
|
+
*/
|
|
810
|
+
async function buildOwnerPartialSignedTx(rpc, owner, instructions, opts = {}) {
|
|
811
|
+
const cu = opts.computeUnits ?? CU_OWNER_ACTION;
|
|
812
|
+
const allIx = [
|
|
813
|
+
getSetComputeUnitLimitInstruction({
|
|
814
|
+
units: cu,
|
|
815
|
+
}),
|
|
816
|
+
...(opts.priorityFeeMicroLamports
|
|
817
|
+
? [
|
|
818
|
+
getSetComputeUnitPriceInstruction({
|
|
819
|
+
microLamports: BigInt(opts.priorityFeeMicroLamports),
|
|
820
|
+
}),
|
|
821
|
+
]
|
|
822
|
+
: []),
|
|
823
|
+
...instructions,
|
|
824
|
+
];
|
|
825
|
+
const blockhash = await getBlockhashCache(rpc).get(rpc);
|
|
826
|
+
const txMessage = pipe(createTransactionMessage({ version: 0 }), (tx) => setTransactionMessageFeePayer(owner.address, tx), (tx) => setTransactionMessageLifetimeUsingBlockhash(blockhash, tx), (tx) => appendTransactionMessageInstructions(allIx, tx));
|
|
827
|
+
const withOwner = addSignersToTransactionMessage([owner], txMessage);
|
|
828
|
+
const partial = await partiallySignTransactionMessageWithSigners(withOwner);
|
|
829
|
+
return getBase64EncodedWireTransaction(partial);
|
|
830
|
+
}
|
|
831
|
+
/**
|
|
832
|
+
* Elevated agent-permissions queue — single-builder dual-sign. The caller holds
|
|
833
|
+
* the cosigner key (server-side / single-operator). Signs [owner, cosigner] and
|
|
834
|
+
* sends. For a true 2-party async flow use `buildQueueAgentPermissionsElevated`.
|
|
835
|
+
*/
|
|
836
|
+
export async function queueAgentPermissionsElevated(rpc, vault, owner, network, agent, permissions, spendingLimit, cooldownSeconds, cosigner, opts) {
|
|
837
|
+
requireValidAddress(agent, "Agent address");
|
|
838
|
+
requireValidPermissions(permissions);
|
|
839
|
+
requireValidAddress(cosigner.address, "Cosigner address");
|
|
840
|
+
if (cosigner.address === owner.address) {
|
|
841
|
+
throw toDxError(new Error("Cosigner must be distinct from the owner (on-chain ErrCosignRequired)"));
|
|
842
|
+
}
|
|
843
|
+
const ix = await getQueueAgentPermissionsUpdateInstructionAsync({
|
|
844
|
+
owner,
|
|
845
|
+
vault,
|
|
846
|
+
agent,
|
|
847
|
+
newCapability: Number(permissions),
|
|
848
|
+
spendingLimitUsd: spendingLimit,
|
|
849
|
+
cooldownSeconds,
|
|
850
|
+
cosignSession: cosigner.address,
|
|
851
|
+
});
|
|
852
|
+
return run(rpc, owner, network, [withCosignerSigner(ix, cosigner.address)], opts, [cosigner]);
|
|
853
|
+
}
|
|
854
|
+
/**
|
|
855
|
+
* Elevated agent-permissions queue — partial-sign handoff. Owner-signs and
|
|
856
|
+
* returns the partial transaction + cosign digest; the cosigner signs and sends
|
|
857
|
+
* out-of-band (true 2-of-2). Validation mirrors the dual-sign path.
|
|
858
|
+
*/
|
|
859
|
+
export async function buildQueueAgentPermissionsElevated(rpc, vault, owner, agent, permissions, spendingLimit, cooldownSeconds, cosignSession, opts) {
|
|
860
|
+
requireValidAddress(agent, "Agent address");
|
|
861
|
+
requireValidPermissions(permissions);
|
|
862
|
+
requireValidAddress(cosignSession, "Cosigner address");
|
|
863
|
+
if (cosignSession === owner.address) {
|
|
864
|
+
throw toDxError(new Error("Cosigner must be distinct from the owner (on-chain ErrCosignRequired)"));
|
|
865
|
+
}
|
|
866
|
+
const ix = await getQueueAgentPermissionsUpdateInstructionAsync({
|
|
867
|
+
owner,
|
|
868
|
+
vault,
|
|
869
|
+
agent,
|
|
870
|
+
newCapability: Number(permissions),
|
|
871
|
+
spendingLimitUsd: spendingLimit,
|
|
872
|
+
cooldownSeconds,
|
|
873
|
+
cosignSession,
|
|
874
|
+
});
|
|
875
|
+
const partialTransactionBase64 = await buildOwnerPartialSignedTx(rpc, owner, [withCosignerSigner(ix, cosignSession)], opts);
|
|
876
|
+
const cosignDigest = computeAgentPermsCosignDigest({
|
|
877
|
+
cosignSession,
|
|
878
|
+
agent,
|
|
879
|
+
newCapability: Number(permissions),
|
|
880
|
+
spendingLimitUsd: spendingLimit,
|
|
881
|
+
cooldownSeconds,
|
|
882
|
+
});
|
|
883
|
+
return { partialTransactionBase64, cosignSession, cosignDigest };
|
|
884
|
+
}
|
|
720
885
|
export async function applyAgentPermissions(rpc, vault, owner, network, agent, opts) {
|
|
721
886
|
requireValidAddress(agent, "Agent address");
|
|
722
887
|
const [overlayPda] = await getAgentOverlayPDA(vault, 0);
|
|
@@ -732,13 +897,92 @@ export async function applyAgentPermissions(rpc, vault, owner, network, agent, o
|
|
|
732
897
|
export async function cancelAgentPermissions(rpc, vault, owner, network, agent, opts) {
|
|
733
898
|
requireValidAddress(agent, "Agent address");
|
|
734
899
|
const pendingPda = await derivePendingAgentPermsPDA(vault, agent);
|
|
735
|
-
const ix =
|
|
900
|
+
const ix = await getCancelAgentPermissionsUpdateInstructionAsync({
|
|
736
901
|
owner,
|
|
737
902
|
vault,
|
|
738
903
|
pendingAgentPerms: pendingPda,
|
|
739
904
|
});
|
|
740
905
|
return run(rpc, owner, network, [ix], opts);
|
|
741
906
|
}
|
|
907
|
+
function assertDistinctCosigner(owner, cosignSession) {
|
|
908
|
+
requireValidAddress(cosignSession, "Cosigner address");
|
|
909
|
+
if (cosignSession === owner.address) {
|
|
910
|
+
throw toDxError(new Error("Cosigner must be distinct from the owner (on-chain ErrCosignRequired)"));
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
/** Partial-sign: cancel a pending AGENT GRANT on a cosign-required vault. */
|
|
914
|
+
export async function buildCancelAgentGrantElevated(rpc, vault, owner, cosignSession, opts) {
|
|
915
|
+
assertDistinctCosigner(owner, cosignSession);
|
|
916
|
+
const ix = await getCancelAgentGrantInstructionAsync({ owner, vault });
|
|
917
|
+
const partialTransactionBase64 = await buildOwnerPartialSignedTx(rpc, owner, [withCosignerSigner(ix, cosignSession)], opts);
|
|
918
|
+
return { partialTransactionBase64, cosignSession };
|
|
919
|
+
}
|
|
920
|
+
/** Partial-sign: cancel a pending AGENT-PERMISSIONS update (M2b gate). */
|
|
921
|
+
export async function buildCancelAgentPermissionsElevated(rpc, vault, owner, agent, cosignSession, opts) {
|
|
922
|
+
assertDistinctCosigner(owner, cosignSession);
|
|
923
|
+
requireValidAddress(agent, "Agent address");
|
|
924
|
+
const pendingPda = await derivePendingAgentPermsPDA(vault, agent);
|
|
925
|
+
const ix = await getCancelAgentPermissionsUpdateInstructionAsync({
|
|
926
|
+
owner,
|
|
927
|
+
vault,
|
|
928
|
+
pendingAgentPerms: pendingPda,
|
|
929
|
+
});
|
|
930
|
+
const partialTransactionBase64 = await buildOwnerPartialSignedTx(rpc, owner, [withCosignerSigner(ix, cosignSession)], opts);
|
|
931
|
+
return { partialTransactionBase64, cosignSession };
|
|
932
|
+
}
|
|
933
|
+
/** Partial-sign: cancel a pending POLICY update (M2a gate). */
|
|
934
|
+
export async function buildCancelPendingPolicyElevated(rpc, vault, owner, cosignSession, opts) {
|
|
935
|
+
assertDistinctCosigner(owner, cosignSession);
|
|
936
|
+
const ix = await getCancelPendingPolicyInstructionAsync({ owner, vault });
|
|
937
|
+
const partialTransactionBase64 = await buildOwnerPartialSignedTx(rpc, owner, [withCosignerSigner(ix, cosignSession)], opts);
|
|
938
|
+
return { partialTransactionBase64, cosignSession };
|
|
939
|
+
}
|
|
940
|
+
/**
|
|
941
|
+
* Partial-sign: APPLY a queued agent-permissions update on a cosign-required
|
|
942
|
+
* vault. The on-chain H-1 gate requires the LIVE bound cosigner to sign at apply
|
|
943
|
+
* (defends the pre-signed-apply-survives-rotation class).
|
|
944
|
+
*/
|
|
945
|
+
export async function buildApplyAgentPermissionsElevated(rpc, vault, owner, agent, cosignSession, opts) {
|
|
946
|
+
assertDistinctCosigner(owner, cosignSession);
|
|
947
|
+
requireValidAddress(agent, "Agent address");
|
|
948
|
+
const [overlayPda] = await getAgentOverlayPDA(vault, 0);
|
|
949
|
+
const pendingPda = await derivePendingAgentPermsPDA(vault, agent);
|
|
950
|
+
const ix = await getApplyAgentPermissionsUpdateInstructionAsync({
|
|
951
|
+
owner,
|
|
952
|
+
vault,
|
|
953
|
+
agentSpendOverlay: overlayPda,
|
|
954
|
+
pendingAgentPerms: pendingPda,
|
|
955
|
+
});
|
|
956
|
+
const partialTransactionBase64 = await buildOwnerPartialSignedTx(rpc, owner, [withCosignerSigner(ix, cosignSession)], opts);
|
|
957
|
+
return { partialTransactionBase64, cosignSession };
|
|
958
|
+
}
|
|
959
|
+
/**
|
|
960
|
+
* Partial-sign: APPLY a pending policy update that DISABLES cosign on a
|
|
961
|
+
* cosign-required vault (the on-chain disable gate requires the bound cosigner).
|
|
962
|
+
* Non-disable applies do NOT need the cosigner — use owner-only
|
|
963
|
+
* {@link applyPendingPolicy}.
|
|
964
|
+
*/
|
|
965
|
+
export async function buildApplyPendingPolicyElevated(rpc, vault, owner, cosignSession, opts) {
|
|
966
|
+
assertDistinctCosigner(owner, cosignSession);
|
|
967
|
+
const ix = await getApplyPendingPolicyInstructionAsync({ owner, vault });
|
|
968
|
+
const partialTransactionBase64 = await buildOwnerPartialSignedTx(rpc, owner, [withCosignerSigner(ix, cosignSession)], opts);
|
|
969
|
+
return { partialTransactionBase64, cosignSession };
|
|
970
|
+
}
|
|
971
|
+
/**
|
|
972
|
+
* Partial-sign: CLOSE a cosign-required vault. `close_vault` is now cosign-gated
|
|
973
|
+
* (a leaked owner key alone cannot close → cannot start the close->reinit
|
|
974
|
+
* drain). Reuses the owner-only close's pending-PDA cleanup enumeration, then
|
|
975
|
+
* binds the cosigner.
|
|
976
|
+
*/
|
|
977
|
+
export async function buildCloseVaultElevated(rpc, vault, owner, network, cosignSession, opts) {
|
|
978
|
+
assertDistinctCosigner(owner, cosignSession);
|
|
979
|
+
const finalIx = await buildCloseVaultFinalIx(rpc, vault, owner, network);
|
|
980
|
+
const partialTransactionBase64 = await buildOwnerPartialSignedTx(rpc, owner, [withCosignerSigner(finalIx, cosignSession)], {
|
|
981
|
+
computeUnits: opts?.computeUnits ?? 400_000,
|
|
982
|
+
priorityFeeMicroLamports: opts?.priorityFeeMicroLamports,
|
|
983
|
+
});
|
|
984
|
+
return { partialTransactionBase64, cosignSession };
|
|
985
|
+
}
|
|
742
986
|
// ─── Post-execution assertions (Phase 2) ─────────────────────────────────────
|
|
743
987
|
// Composes with pre-execution InstructionConstraints — NOT a replacement.
|
|
744
988
|
//
|