Policy Authoring Guide
Policies define the governance rules that control how specialist agent signals (evaluations and objections) are aggregated into final decisions during MACP coordination runs. This guide explains how the examples-service loads policies, how they flow to spawned agents, and how to author new ones for demo scenarios.
Canonical rule schema, voting algorithms, veto/confidence/ABSTAIN mechanics, and commitment-authority semantics live in the runtime docs — not here. See
runtime/docs/policy.mdfor the full schema and behavioural reference. This guide covers only the examples-service-specific plumbing (loading, registration, hints mapping, scenario wiring).
Policy JSON Schema (at a glance)
Every policy under policies/*.json follows the runtime's
PolicyDescriptor shape:
{
"policy_id": "policy.<domain>.<variant>",
"mode": "macp.mode.decision.v1",
"schema_version": 1,
"description": "Human-readable description of this policy",
"rules": { "voting": { ... }, "objection_handling": { ... },
"evaluation": { ... }, "commitment": { ... } }
}
For the full field reference (voting algorithms, thresholds, quorum shapes, objection handling, evaluation confidence, commitment authority, rule-level validation errors) see the canonical runtime doc linked above. The examples-service does not re-document or alter any of those semantics — it just registers whatever descriptors live on disk.
Included Policies
These are the policies shipped in policies/ for the demo scenarios:
| Policy ID | Algorithm | Threshold | Quorum | Veto | Min Confidence |
|---|---|---|---|---|---|
policy.default | none | - | 0 | No | 0.0 |
policy.fraud.majority-veto | majority | 50% | 2 count | Yes (1) | 0.0 |
policy.fraud.supermajority | supermajority | 67% | 2 count | No | 0.0 |
policy.fraud.unanimous | unanimous | - | 100% | Yes (1) | 0.7 |
policy.lending.conservative | supermajority | 67% | 3 count | Yes (1) | 0.6 |
policy.claims.majority | majority | 50% | 2 count | No | 0.0 |
Connecting Policies to Scenarios
Policies are referenced in scenario templates via the policyVersion field:
# In a scenario template (e.g., templates/unanimous.yaml)
spec:
overrides:
launch:
policyVersion: policy.fraud.unanimous
policyHints:
type: unanimous
threshold: 1.0
vetoEnabled: true
minimumConfidence: 0.7
designatedRoles: []
The default template uses policy.default which requires no registration.
Policy Hints
policyHints are an examples-service-specific denormalized projection
of the policy's rules that agents consume at bootstrap time. They are
never sent to the runtime or the control-plane — only to the in-tree
PolicyStrategy used by the Risk coordinator. The runtime evaluates
governance against the registered policy_id directly.
| Hint Field | Maps From (canonical) |
|---|---|
type | rules.voting.algorithm |
threshold | rules.voting.threshold |
vetoEnabled | rules.objection_handling.critical_severity_vetoes |
vetoThreshold | rules.objection_handling.veto_threshold |
minimumConfidence | rules.evaluation.minimum_confidence |
designatedRoles | rules.commitment.designated_roles |
How Policies Are Loaded
PolicyLoaderService reads all *.json files from the policies/
directory at startup:
- Parses each file and extracts
policy_id - Validates structure (non-blocking warnings)
- Caches policies in memory
- Excludes
policy.defaultfrom the registrable set (auto-resolved by the runtime)
How Policies Are Registered
When REGISTER_POLICIES_ON_LAUNCH=true (the default) and
MACP_RUNTIME_ADDRESS is set, PolicyRegistrarService.onApplicationBootstrap()
registers every non-default policy with the runtime once per process
start. Registration happens at service boot — not per-run — so that
/examples/run requests never hit an UNKNOWN_POLICY_VERSION from the
runtime.
Flow (src/policy/policy-registrar.service.ts):
PolicyLoaderService.listRegistrablePolicies()returns every policy whosepolicy_idis notpolicy.default.AuthTokenMinterService.mintToken("examples-service", { can_manage_mode_registry: true, is_observer: false, allowed_modes: ["*"] })mints an admin JWT from the auth-service.- The registrar opens a short-lived gRPC channel to the runtime using that
JWT and calls
MacpClient.registerPolicy(descriptor)for each policy. (For the wire contract ofRegisterPolicy, seeruntime/docs/API.md.) - Errors whose message contains
"already"are treated as idempotent success (the runtime signals a duplicate). - On completion, logs
policy_registration_complete registered=<n> already=<n> failed=<n> total=<n>.
Registration is skipped (with a warning, not an error) when:
REGISTER_POLICIES_ON_LAUNCH=false— explicit opt-out.MACP_RUNTIME_ADDRESSis unset — typically CI/test.
If the admin JWT mint fails (e.g. auth-service unreachable), the
registrar aborts the entire registration pass and logs an ERROR.
The service still starts, but downstream /examples/run requests will
fail at the runtime with UNKNOWN_POLICY_VERSION. See
"Troubleshooting" below.
Creating a Custom Policy
- Create the JSON file in
policies/— the shape is the runtime'sPolicyDescriptor. Example:
{
"policy_id": "policy.myteam.custom",
"mode": "macp.mode.decision.v1",
"schema_version": 1,
"description": "Custom policy for my team's use case",
"rules": {
"voting": {
"algorithm": "supermajority",
"threshold": 0.75,
"quorum": { "type": "count", "value": 3 }
},
"objection_handling": { "critical_severity_vetoes": true, "veto_threshold": 1 },
"evaluation": { "minimum_confidence": 0.6, "required_before_voting": true },
"commitment": {
"authority": "designated_roles",
"require_vote_quorum": true,
"designated_roles": ["risk", "compliance"]
}
}
}
Refer to runtime/docs/policy.md
for the legal values of each rule field.
- Reference it in a scenario template:
spec:
overrides:
launch:
policyVersion: policy.myteam.custom
policyHints:
type: supermajority
threshold: 0.75
vetoEnabled: true
vetoThreshold: 1
minimumConfidence: 0.6
designatedRoles: ["risk", "compliance"]
- Restart the service —
PolicyLoaderServicewill discover the new file on next load, andPolicyRegistrarServicewill register it with the runtime duringonApplicationBootstrap.
Troubleshooting
UNKNOWN_POLICY_VERSION at run time
Symptom: /examples/run compiles successfully but the runtime rejects
the session with UNKNOWN_POLICY_VERSION.
Checklist:
- Startup logs. Look for
policy_registration_completeon the most recent examples-service boot. If you seepolicy registration aborted: failed to mint admin JWT, fix the auth-service connection (MACP_AUTH_SERVICE_URL, network reachability, JWKS on the runtime). - Scope. The admin mint uses
can_manage_mode_registry. If the runtime's auth config does not accept this scope,registerPolicyreturns an error and the policy stays unregistered. - Runtime trust chain. The runtime must have
MACP_AUTH_JWKS_URL=<auth-service>/.well-known/jwks.jsonand the matchingMACP_AUTH_ISSUER/MACP_AUTH_AUDIENCE. A mismatch rejects the admin JWT at the runtime boundary, which logs aspolicy_register_exception. Seeruntime/docs/getting-started.md§ Authentication for the full JWT setup. - Manual re-register. Restart the examples-service once the
auth-service is healthy — registration is idempotent, so any
already-registered policies come back as
alreadyin the log summary.
AUTH_MINT_FAILED on /examples/run
The spawn-time JWT mint hit the auth-service and got a non-2xx response
(or timed out). Check MACP_AUTH_SERVICE_URL, MACP_AUTH_SERVICE_TIMEOUT_MS,
and the auth-service logs.
Local validation warnings
PolicyLoaderService runs a light structural check on load and warns
(non-blocking) for missing policy_id, out-of-range values, or
obviously invalid combinations. The authoritative schema validation
happens at the runtime during RegisterPolicy — if a descriptor passes
local load but fails at the runtime, the registrar logs
policy_register_exception with the runtime's INVALID_POLICY_DEFINITION
reason. See
runtime/docs/policy.md § Registering a policy
for the validation rules the runtime enforces.