Policy API Reference
PolicyDescriptor
The wire format for registered governance policies:
interface PolicyDescriptor {
policyId: string; // e.g., "policy.fraud.majority-veto"
mode: string; // Target mode or "*" for mode-agnostic
description: string;
rules: string; // JSON-encoded governance rules
schemaVersion: number; // Rule schema version: 1 for quorum/proposal/task/handoff;
// 1, 2, or 3 (default) for decision, see buildDecisionPolicy below
registeredAtUnixMs?: number; // Set by runtime
}Builder Functions
buildDecisionPolicy(policyId, description, rules, options?)
Creates a PolicyDescriptor targeting macp.mode.decision.v1. The only
builder with a runtime-selectable schema version
(RFC-MACP-0012 (Policy));
the other four modes remain schema version 1.
interface DecisionPolicyOptions {
schemaVersion?: 1 | 2 | 3; // default: 3 -- see "schemaVersion" below
}
schemaVersionselects the empty-tally semantics the runtime evaluates this policy under (RFC-MACP-0012 §8 item 4 — validated once, at admission, never re-validated on replay):
Version Empty decisive tally 1/2Fail-open: a binding algorithm (e.g. majority) passes on zero ballots unlesscommitment.requireVoteQuorumistrue(defaultfalse).3Fail-closed for every algorithm except 'none'(RFC-MACP-0012 §4.1's "vacuous participation floor", spec PR #99).The default is
3(issue #85, decided jointly withmacp-sdk-python's identicalschema_version: int = 3default — see that repo's issue #65): RFC-MACP-0012's authoring guidance is that new non-'none'policies SHOULD declareschemaVersion: 3, and the old default of2was fail-open on an empty tally for exactly the callers who opted into a binding algorithm — silently satisfying a vote that received zero ballots, which contradicts what asking for a binding algorithm means. v1/v2 semantics are preserved permanently for replay (RFC-MACP-0012 §8 item 3) and remain a supported choice, not a transitional one — pass{ schemaVersion: 1 }or{ schemaVersion: 2 }explicitly to keep fail-open empty-tally semantics. A stored policy always evaluates under its own declared version forever, so this default change carries no migration risk for existing registered policies. An out-of-range value (anything other than1,2, or3) throwsMacpSessionError.schemaVersionis descriptor metadata, not a rule — it never changes the serializedrulesJSON for the same rule input.TypeScript has no keyword arguments, so this is a fourth positional
optionsobject rather than Python'sschema_version=keyword — the two SDKs' call shapes diverge here by necessity, not oversight.
Parameters:
interface DecisionPolicyRulesInput {
voting?: {
algorithm?: 'none' | 'majority' | 'supermajority' | 'unanimous' | 'weighted' | 'plurality';
threshold?: number; // vote-share fraction, 0 < t <= 1, default: 0.5
quorum?: { type: 'count' | 'percentage'; value: number }; // percentage = integer 0–100, NOT 0–1
weights?: Record<string, number>; // participant_id → weight
};
objectionHandling?: {
criticalSeverityVetoes?: boolean; // default: false
vetoThreshold?: number; // default: 1
criticalObjectionAction?: 'deny' | 'finalize_decline' | 'hold'; // default: 'deny' (schema v2)
};
evaluation?: {
minimumConfidence?: number; // 0-1, default: 0
requiredBeforeVoting?: boolean; // default: false
};
commitment?: {
authority?: 'initiator_only' | 'any_participant' | 'designated_role'; // default: 'initiator_only'
designatedRoles?: string[]; // default: []; REQUIRED non-empty when authority is 'designated_role'
requireVoteQuorum?: boolean; // default: false
allowDeclineOverApproval?: boolean; // default: false (schema v2, decision-only)
};
}Schema-version-2 fields: criticalObjectionAction selects what happens when a
critical objection would block commitment (deny rejects it, finalize_decline
resolves the session as a negative outcome, hold leaves it open).
allowDeclineOverApproval: true lets a reject-majority resolve the session with
a committed negative outcome (outcome_positive = false) instead of denying
commitment.
buildDecisionPolicyenforces the canonicaldecision-rules.schema.jsonconstraints client-side, so a schema-invalid descriptor fails fast instead of round-tripping to a runtimeINVALID_POLICY_DEFINITION:
voting.algorithmmust be one of the six canonical values.voting.thresholdmust be0 < threshold <= 1.algorithm: 'majority'requiresthreshold >= 0.5— inclusive, deliberately, sincepolicy.std.majority(RFC-MACP-0012 §2.2) pinsthreshold: 0.5byte-identical on every runtime.algorithm: 'supermajority'requiresthreshold > 0.5— exclusive; the field's own default of0.5is a bare majority wearing the name, so an explicit threshold (e.g.0.67) is required.algorithm: 'weighted'requires a non-emptyweightsmap.weights, if supplied at all, is validated unconditionally — at every algorithm, not only'weighted': it must be non-empty, and every value must be> 0. A weight-0 participant is expressed by omission from the map, never by an explicit0— an explicit0throws. This is the weighted electorate rule: an omitted participant's vote is non-decisive (excluded from the ratio and the decisive tally) but still counts towardvoting.quorum's participation floor.
buildQuorumPolicy(policyId, description, rules)
Creates a PolicyDescriptor targeting macp.mode.quorum.v1 (RFC-MACP-0012 §4.2).
threshold.valueis the approval bar, not a participation quorum. Fortype: 'percentage'it is an integer 1–100 — the runtime computes the bar asceil(value / 100 × participants).75means "≥ 75% must approve"; a fractional value like0.75rounds to a ~1% bar and is therefore rejected (MacpSessionError). Usen_of_mfor absolute counts.
valuemust be a positive integer for everytype(exclusiveMinimum: 0in the canonicalquorum-rules.schema.json, unconditional — a zero approval bar would be trivially satisfied by any ballot set, sobuildQuorumPolicyrejects it client-side).'weighted'is reserved: it was removed from the canonical schema without ever having defined semantics (no weights vocabulary, no electorate rule) and is refused by the runtime; passing it throwsMacpSessionErrornaming the reservation.
interface QuorumPolicyRulesInput {
threshold?: {
type: 'n_of_m' | 'percentage'; // default: 'n_of_m'
value: number; // approval bar; default: 1
};
abstention?: {
countsTowardQuorum?: boolean; // default: false
interpretation?: 'neutral' | 'implicit_reject' | 'ignored'; // default: 'neutral'
};
commitment?: CommitmentRules;
}buildProposalPolicy(policyId, description, rules)
Creates a PolicyDescriptor targeting macp.mode.proposal.v1 (RFC-MACP-0012 §4.3).
interface ProposalPolicyRulesInput {
acceptance?: { criterion?: 'all_parties' | 'counterparty' | 'initiator' }; // default: 'all_parties'
counterProposal?: { maxRounds?: number }; // default: 0 (unlimited)
rejection?: { terminalOnAnyReject?: boolean }; // default: false
commitment?: CommitmentRules;
}buildTaskPolicy(policyId, description, rules)
Creates a PolicyDescriptor targeting macp.mode.task.v1 (RFC-MACP-0012 §4.4).
interface TaskPolicyRulesInput {
assignment?: { allowReassignmentOnReject?: boolean }; // default: false
completion?: { requireOutput?: boolean }; // default: false
commitment?: CommitmentRules;
}buildHandoffPolicy(policyId, description, rules)
Creates a PolicyDescriptor targeting macp.mode.handoff.v1 (RFC-MACP-0012 §4.5).
interface HandoffPolicyRulesInput {
acceptance?: { implicitAcceptTimeoutMs?: number }; // default: 0 (no implicit accept)
commitment?: CommitmentRules;
}CommitmentRules (shared by all modes)
The exported input type is named CommitmentRules:
interface CommitmentRules {
authority?: 'initiator_only' | 'any_participant' | 'designated_role'; // default: 'initiator_only'
designatedRoles?: string[]; // default: []; REQUIRED non-empty when authority is 'designated_role'
requireVoteQuorum?: boolean; // default: false; emitted only by buildDecisionPolicy, dropped elsewhere
allowDeclineOverApproval?: boolean; // default: false; emitted only by buildDecisionPolicy (schema v2), dropped elsewhere
}
authority: 'designated_role'requires a non-emptydesignatedRoles. An authority rule that names no one is unsatisfiable — no sender could ever meet it — so every one of the fivebuild*Policyfunctions throwsMacpSessionErrorifdesignatedRolesis omitted or[]whileauthorityis'designated_role'. Enforced once, in the sharedserializeCommitmenthelper all five builders funnelcommitmentthrough, mirroring the canonical rule schemas' root-level conditional (minItems: 1ondesignated_roleswhenauthority == "designated_role", spec issue #116).designatedRolesis ignored, not validated, under the other two authorities — supplying it there is still serialized into the descriptor but has no effect on who may commit.
Client Methods
client.registerPolicy(descriptor, options?)
Registers a policy with the runtime. Returns { ok: boolean; error?: string }.
client.unregisterPolicy(policyId, options?)
Removes a registered policy. Returns { ok: boolean; error?: string }.
client.getPolicy(policyId, options?)
Retrieves a policy by ID. Returns the PolicyDescriptor.
client.listPolicies(mode?, options?)
Lists registered policies, optionally filtered by mode. Returns PolicyDescriptor[].
PolicyWatcher
import { PolicyWatcher } from 'macp-sdk-typescript';
const watcher = new PolicyWatcher(client, { auth });
// Async generator
for await (const change of watcher.changes(abortSignal?)) {
// change.descriptors: PolicyDescriptor[]
// change.observedAtUnixMs: number
}
// Callback-based
await watcher.watch((change) => { ... });
// One-shot
const change = await watcher.nextChange();Constants
DEFAULT_POLICY_VERSION='policy.default'