Automation & agents
Execution authorization
The single boundary between a validated plan and the deployment engine. Off by default, fail-closed once configured, and bound to each plan's content hash.
Phelix authorizes execution at a single boundary between a validated plan and the deployment engine - request → plan → authorization → (approval) → execution - and never request → execution for a protected path. This is the layer that makes Phelix safe to hand to automation: an agent decides what should be deployed, Phelix decides whether this caller may execute that exact plan, and a human decides who is allowed to and what needs signing off.
Authentication vs authorization
| Question | Status in Phelix |
|---|---|
| Authentication - who is calling? | Not implemented for execution. A local phelix invocation is an unauthenticated local process, and Phelix says so. |
| Authorization - may this caller execute this plan? | Implemented - this guide. |
Phelix does not fabricate identity. There is no --actor flag, no --as flag, and no environment variable through which a caller can name itself an authorized principal. phelix auth login is unrelated: it authenticates you to the dashboard backend for monitoring, grants no execution rights, and is never consulted by the authorization boundary.
The actor model
The boundary receives an abstract caller - never an HTTP header, a CLI flag, or MCP metadata. An actor has three fields, matching the machine contract’s actor block:
{ "type": "cli", "id": null, "authenticated": false }| Field | Meaning |
|---|---|
| type | cli, api, agent, mcp, or service - the transport, not a privilege level |
| id | the established identity, or null; only an authenticated actor ever has one |
| authenticated | whether the identity was verified |
The invariant id != null ⇒ authenticated is enforced where actors are validated, so an unauthenticated caller can never match a rule that names an identity. Today there is exactly one authenticator - the local CLI - which takes no input and always reports {type: "cli", id: null, authenticated: false}. What a local invocation proves is write access to this host’s Phelix data directory: recorded as provenance, never as an identity.
Modes
Configuring the policy
The policy lives on the host under the data directory at ~/.phelix/authz/policy.json (0600, relocatable with PHELIX_DATA_DIR). It is deliberately not in a project’s phelix.yaml: an agent editing the repository it deploys could otherwise rewrite the file that grants it execution. Find the exact path with phelix authz status.
{ "schema_version": "1", "mode": "enforced", "rules": [ { "actor": { "type": "cli", "authenticated": false }, "action": "rebuild", "target": "billing", "effect": "allow", "require_approval": true, "description": "local operator may deploy billing, with sign-off" } ]}Evaluation is fixed and total:
- A malformed request or policy is AUTHZ_INVALID - fail closed.
- An execution with no plan binding is denied.
- An explicit
denyrule wins over anyallowrule, always. - An
allowrule withrequire_approvalis satisfied only by an approval bound to this exact plan. - Anything else is denied by default.
What is protected
| Path | Gated |
|---|---|
| phelix plan apply <plan-id> | Yes - plan-bound, fully authorized |
| phelix rebuild | Yes - planless, so denied under enforcement |
| phelix rollback | Yes - planless, so denied under enforcement |
| Webhook deployments | Yes, transitively - the webhook queue re-invokes phelix rebuild |
| Remote rebuild / rollback (monitor daemon) | Yes - same mechanism, gated in-process |
| plan create / show / list, inspect, context, status, log | No - read-only |
| phelix build | No - produces an artifact without serving it |
| phelix start / stop / restart | No - does not change the deployed version |
Under enforced authorization, a planless mutation is denied even by the most permissive possible rule set: the plan binding is not a rule, it is the boundary’s shape. That closes the obvious bypass of “skip the plan, run rebuild directly”, and it is why build / start / stop being ungated is not a hole - they cannot promote a new version.
Plan binding
Authorization is never granted for a vague action like “deploy billing”. It is evaluated against the immutable plan, and the binding is both plan_id (which artifact) and plan_hash (what it will do):
- A decision for plan A can never permit plan B, even when the action and target are identical.
- A plan whose content hash differs from the approved one no longer matches that approval.
- There is no reusable authorization token: the decision is re-evaluated against the exact plan every time.
Approval
An allow rule with require_approval: true means: permitted, once an approval bound to this exact plan exists.
phelix authz check <plan-id> # what would the boundary say?phelix authz approve <plan-id> --expect-hash sha256:… # record the approvalphelix plan apply <plan-id> # now executesphelix authz revoke <plan-id> # remove the approval--expect-hash asserts the hash you inspected; a mismatch is refused rather than approved - machine callers should always pass it, so an approval is never granted to content the approver did not see. An approval is immutable: the file at ~/.phelix/authz/approvals/<plan-id>.json is created exactly once, re-approving is ALREADY_EXISTS, and a changed decision is a revoke followed by a new approve.
Local compatibility
Introducing authorization changes nothing on a host that has not configured it. The boundary between “unchanged” and “enforced” is a single file:
| Policy file | Posture |
|---|---|
| absent | legacy_local - existing local CLI behavior, preserved |
| present, mode: legacy_local | the same, stated explicitly |
| present, mode: enforced | the rule set decides; no match means deny |
| present, unreadable | AUTHZ_UNAVAILABLE - fail closed (retryable) |
| present, invalid | AUTHZ_INVALID - fail closed (not retryable) |
Error codes
| Code | Exit | Retryable | Meaning |
|---|---|---|---|
| AUTHZ_DENIED | 11 | no | This caller may not execute this. Do not retry. |
| AUTHZ_UNAVAILABLE | 11 | yes | The boundary could not be consulted (I/O). Nothing executed. |
| AUTHZ_INVALID | 11 | no | The host policy or the request is malformed. Needs an operator. |
| APPROVAL_REQUIRED | 11 | no | A plan-bound approval is required and none exists. |
| APPROVAL_STALE | 11 | no | An approval exists but its binding no longer matches this plan. |
| APPROVAL_INVALID | 11 | no | The approval artifact is corrupt or was edited after creation. |
Exit 11 uniformly means the boundary refused or could not decide, and nothing mutated. AUTHZ_UNAVAILABLE is the only retryable one - an agent must never be told to repeat a denied action, and an approval requirement is not a transient failure. Plan staleness keeps its own PLAN_* codes and exit 3. See Exit codes and the machine contract.