Spaces:
Running
Running
File size: 4,532 Bytes
1b87b65 b86ec3d 1b87b65 7aa8d9d 1b87b65 7aa8d9d 1b87b65 7aa8d9d 1b87b65 7aa8d9d 1b87b65 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 | /**
* ActionExecutionPolicy β pure rules for `canExecute`.
*
* Order: consent β idempotency β capability β rate-limit.
* Failure of any step returns `ActionExecutionRefused` with a typed reason.
* This is the invariant core of the Action spine (CLAUDE.md #2, #3, #5, #7).
*/
import type {
Action,
AdapterCapability,
ActionExecutionRefused,
} from "@courtmitra/contracts";
import { consentHashMatches } from "./consent-hash.js";
// ---------------------------------------------------------------------------
// Refusal result
// ---------------------------------------------------------------------------
export interface ExecutionRefusal {
refused: true;
reason: string;
code: ActionExecutionRefused["code"];
}
export interface ExecutionAllowance {
refused: false;
}
export type CanExecuteResult = ExecutionRefusal | ExecutionAllowance;
// ---------------------------------------------------------------------------
// Adapter capability check
// ---------------------------------------------------------------------------
function adapterForAction(action: Action): string | null {
const t = action.actionType;
if (t === "send_email" || t === "takedown") return "resend_email";
if (t === "post_social") return "social_publisher";
// All other types (generate_packet, manual_packet, prepare_portal,
// submit_portal, inform_user, etc.) are local/user-driven β no adapter needed.
return null;
}
function capabilityIsAvailable(cap: AdapterCapability): boolean {
return cap.status === "available" || cap.status === "dev_only";
}
const executedProofStates = new Set<Action["proofState"]>([
"sent_by_email",
"sent_via_channel",
"posted_to_social",
"user_confirmed_external_submission",
"receipt_verified",
]);
// ---------------------------------------------------------------------------
// Public gate β pure function, zero side-effects
// ---------------------------------------------------------------------------
/**
* Evaluate whether an action may be executed.
*
* @param action β the Action row to execute.
* @param consentGrant β the consent grant (null if not yet granted).
* @param outgoingPreview β the ConsentPreview computed from the EXACT outgoing payload.
* @param allCapabilities β full capability registry (adapter list).
* @returns `{ refused: false }` or `{ refused: true, reason, code }`.
*/
export function canExecute(
action: Action,
consentGrant: { actionPreviewHash: string } | null,
outgoingPreview: Parameters<typeof consentHashMatches>[1],
allCapabilities: AdapterCapability[],
): CanExecuteResult {
// 1. Consent must exist
if (!consentGrant) {
return {
refused: true,
reason: "Consent has not been granted for this action.",
code: "consent_not_found",
};
}
// 2. Consent hash must match the outgoing payload (CLAUDE.md #5)
if (!consentHashMatches(consentGrant.actionPreviewHash, outgoingPreview)) {
return {
refused: true,
reason:
"The action draft has changed since consent was given β re-approval required.",
code: "consent_hash_mismatch",
};
}
// 3. Action must be in an executable status.
// A preview is not executable until consent has explicitly moved it to approved.
// "executing" is allowed for worker retries after the attempt row is recorded.
const executableStatuses = new Set(["approved", "executing"]);
if (!executableStatuses.has(action.status)) {
return {
refused: true,
reason: `Action status is "${action.status}", not "approved" or "executing".`,
code: "action_not_approved",
};
}
// 4. Idempotency key must not already have been used (caller checks DB)
if (action.status === "succeeded" || executedProofStates.has(action.proofState)) {
return {
refused: true,
reason: "This action has already been executed (idempotency key consumed).",
code: "idempotency_key_used",
};
}
// 5. Adapter capability must be available
const adapterName = adapterForAction(action);
if (adapterName) {
const matching = allCapabilities.find((c) => c.adapter === adapterName);
if (!matching || !capabilityIsAvailable(matching)) {
return {
refused: true,
reason: matching
? `Adapter "${adapterName}" is not available (status: ${matching.status}). ${matching.reason ?? ""}`
: `No adapter found for "${adapterName}".`,
code: "adapter_capability_blocked",
};
}
}
return { refused: false };
}
|