agentplane 0.46.0

Durable, replayable agent runtime — the journal is the plan of record
Documentation
// The policy `agentplane init --serve` writes: a declared peer in, a framework
// calling the served tools, and an operator able to see the plane and stop it
// — written so the shape of a real one is visible.
//
// `agentplane serve` requires a policy file with no default, because a
// permissive engine and no engine are the same behaviour and only one of them
// looks governed.
//
// Three things are worth copying rather than the rules themselves:
//
//   * Both rules bind to what the **credential** established — `context.tenant`
//     and `context.roles`, derived by the authenticator, never by the request
//     body. A rule keyed on something a caller can claim is a rule the caller
//     writes for you.
//   * The surfaces are separated **here**, not by the network. A peer token
//     carries `peer` and reaches `a2a:*`; a framework token carries `framework`
//     and reaches `mcp:*`; the operator token carries `operator` and reaches
//     `api:*`. None can do another's work even if it reaches that socket —
//     which is why `--operator-addr` and `--mcp-addr` being separate ports is
//     defence in depth rather than the control itself.
//   * The actions are enumerated. `a2a:message.send` is the one a peer needs
//     and the one that is easy to forget; a policy missing it denies at that
//     step with the caller told only that it was declined, and the operator
//     side of that refusal is a `tracing` event.
//
// One grant here is wider than a deployment may want: `api:hold.release` sits
// with the on-call verbs, so whoever holds the `operator` role can lift a
// legal hold, after which retention may delete what the hold preserved. To
// split it, remove it from the on-call rule and permit it in a rule of its own
// on another role — `context.roles.contains("legal")` — given to the people
// who decide holds.
//
// The complete vocabulary, for a policy that needs more. A guard holds this
// block to the action lists in the crate, so an action added there and not
// added here stops the build — a comment claiming to be a full set is worth
// only what checks it.
//
// Asked by the *runtime*, underneath the surface. All three, always:
//   run:admit         effect:perform  data:release
//
// `effect:perform` covers a declared content check too: it is asked on
// resource `content.check`, so the permit below lets a checker see a value
// its own ceiling allows. `effect:declared`, `effect:egress` and
// `effect:content` label refusals the plane already decided and are never
// asked.
//
// Asked by the A2A surface, for a peer:
//   a2a:message.send  a2a:task.read   a2a:task.continue
//   a2a:task.cancel   a2a:task.push   a2a:card.extended
//   a2a:event.deliver
//
// Asked by the MCP surface (`--mcp-addr`), for a framework calling tools:
//   mcp:tool.list     mcp:tool.call   mcp:task.read   mcp:task.cancel
//   mcp:prompt.read   mcp:resource.read
//
// Asked by the operator API. The incident verbs are the ones to notice: a
// bundle missing them denies during the incident, which is the one time
// nobody is reading policy files.
//   api:run.read      api:run.list    api:run.history api:run.live
//   api:run.waiting   api:run.cancel  api:run.reopen  api:run.abandon
//   api:attention     api:drill.read  api:effect.reconcile
//   api:task.list     api:task.read   api:task.claim  api:task.release
//   api:task.takeover api:task.decide
//   api:case.read     api:case.list
//   api:obligation.list                api:obligation.acknowledge
//   api:hold.list     api:hold.place  api:hold.release
//   api:halt.list     api:halt.place  api:halt.lift
//   api:event.deliver api:deadletter.list
//   api:push.list     api:push.rearm

// A peer may ask this agent to do work, and read back what it asked for.
permit(
    principal,
    action in [
        Action::"a2a:message.send",
        Action::"a2a:task.read",
        Action::"a2a:task.continue",
        // Registering a webhook. Without this a peer's
        // `taskPushNotificationConfig` is declined by *policy* before the push
        // grant is ever consulted — the refusal is correct and says nothing
        // about the URL, which is exactly the uniform decline an external
        // caller gets. `--push-host` is the second gate, not the first.
        Action::"a2a:task.push"
    ],
    resource
) when {
    context.tenant == "default" && context.roles.contains("peer")
};

// Continuing a task supplies the event it awaits, and that is asked again as
// `a2a:event.deliver` on the event's kind — so it is granted per kind, never
// on every kind. The starter awaits none; for an agent that does, uncomment
// this and name the kinds a peer may answer:
//
//   permit(
//       principal,
//       action == Action::"a2a:event.deliver",
//       resource in [Resource::"approval.response"]
//   ) when {
//       context.tenant == "default" && context.roles.contains("peer")
//   };

// A framework may list the served tools, call them, and follow the calls that
// wait. Prompts and resources serve only what the reviewed manifests declare,
// and a framework that lists them at startup must not fail there.
permit(
    principal,
    action in [
        Action::"mcp:tool.list",
        Action::"mcp:tool.call",
        Action::"mcp:task.read",
        Action::"mcp:task.cancel",
        Action::"mcp:prompt.read",
        Action::"mcp:resource.read"
    ],
    resource
) when {
    context.tenant == "default" && context.roles.contains("framework")
};

// The runtime's own gates, which run *underneath* the surface gates above.
//
// **`roles` is deliberately absent here, and this is the trap worth knowing.**
// A role belongs to an authenticated HTTP caller, and `run:admit` and
// `effect:perform` are asked by the runtime — which has a plan and a delegation
// chain, not a request. Writing `context.roles.contains("peer")` on these
// actions does not deny; it makes the policy *fail to evaluate*, and Cedar is
// total, so the rule silently stops contributing and everything is denied for a
// reason that has nothing to do with rules. This crate reports that case
// distinctly rather than as an ordinary refusal:
//
//   ERROR agentplane.policy.denied: policy_error=true
//   detail=error while evaluating policy `policy0`:
//          record does not have the attribute `roles`
//   — fix the policy set; this denial may not mean what it appears to
//
// To key the runtime's gates on *who asked*, give the run a delegation chain:
// its context is merged into these requests, which is the supported path for
// caller attributes below the surface.
//
// **`data:release` is deliberately not permitted here, and that is a refusal
// rather than an omission.** It is the third runtime action, asked on every
// typed release, and Cedar denies what no rule permits — so this bundle passes
// a run's effects and refuses to let a labelled value out. That is the right
// default for a starting policy, because the release gate is the one whose
// rules depend on what a deployment considers sensitive, and there is no
// answer to that which an example can supply. `preflight` will not warn you:
// it reports rules that cannot evaluate, and a rule nobody wrote evaluates
// fine. To open it, permit `data:release` on the labels you mean, reading
// `context.label` — never as a blanket permit beside the two above, which
// silently turns the lattice off. The rule below, uncommented, would let a
// value labelled up to `internal` out of a run and nothing above it; the
// security guide's release gate says what each label means:
// https://hupe1980.github.io/agentplane/docs/security/#information-flow-labels
//
//   permit(principal, action == Action::"data:release", resource) when {
//       context.tenant == "default" &&
//       ["public", "internal"].contains(context.label.sensitivity)
//   };
permit(
    principal,
    action in [Action::"run:admit", Action::"effect:perform"],
    resource
) when {
    context.tenant == "default"
};

// An operator may stop what is running and settle what it left behind. These
// are the verbs an incident needs, granted before it rather than discovered
// missing during it: halt the plane or one agent, cancel or abandon a run,
// settle an effect whose outcome is unknown, and hold or release a case.
@id("on-call verbs")
permit(
    principal,
    action in [
        Action::"api:halt.place",
        Action::"api:halt.lift",
        Action::"api:run.cancel",
        Action::"api:run.abandon",
        Action::"api:effect.reconcile",
        Action::"api:hold.place",
        Action::"api:hold.release"
    ],
    resource
) when {
    context.tenant == "default" && context.roles.contains("operator")
};

// An operator may see what the plane did and is doing — the halts in force,
// the runs in flight and waiting, and what needs a person — and clear what it
// is holding.
permit(
    principal,
    action in [
        Action::"api:halt.list",
        Action::"api:run.live",
        Action::"api:run.waiting",
        Action::"api:attention",
        Action::"api:run.list",
        Action::"api:run.read",
        Action::"api:run.history",
        Action::"api:task.list",
        Action::"api:task.read",
        Action::"api:task.claim",
        Action::"api:task.release",
        Action::"api:task.decide",
        Action::"api:case.read"
    ],
    resource
) when {
    context.tenant == "default" && context.roles.contains("operator")
};