Reference
access.yaml
Who the humans in your office are, what role each holds and where, and who may approve hires, access changes and budgets.
access.yaml lives at the root of the policy repo. Like personnel files, it changes only by a merged pull request. The office reads it on every reconcile (policy push webhook, boot, every 5 minutes), adds or updates the humans it lists, and records human.access.changed in the ledger. A human removed from the file is disabled and signed out (ledger human.disabled), never deleted; the owner from office.yaml is never disabled. Unknown keys and duplicate emails are validation errors (ledger policy.invalid); while the file is invalid, only the owner break-glass applies.
Example#
humans:
- email: owner@example.com
name: Owner Name
roles: [{ role: owner, scope: office }]
- email: lead@example.com
name: Dana Lead
roles: [{ role: lead, scope: "dept:engineering" }]
policy_roles:
hiring_approvers: [owner@example.com]
access_approvers: [owner@example.com]
budget_approvers: [owner@example.com]humans#
| Field | Type | Meaning |
|---|---|---|
email | email (stored lowercase) | The human's sign-in identity. Appears in the ledger as human:<email>. |
name | string | Display name. |
roles | list of { role, scope }, default [] | Grants. A human can hold different roles on different scopes. |
When the reconciler adds a human without a password, it creates a one-time invite (ledger human.invited). Every invite link is valid for 72 hours, however it was made. The link itself is never stored in the ledger. Admins (access.propose) see live invites under Pending invites on the People page, each with its copyable link, its expiry and New link, which replaces it with a fresh one (only one live link per person). People without a password also have an Invite link button, and on the host there is officectl invite <email>. The human opens /invite/<token> and sets a password.
Roles#
Roles nest. Each role can do everything the roles below it can.
| Role | In short |
|---|---|
viewer | See the office, agents and runs. |
operator | Also chat with agents, send mail, start runs. |
lead | Also read transcripts, stop runs, decide approvals, propose hires and capability changes. |
admin | Also suspend and resume agents, propose access changes, read the ledger and settings. |
owner | Everything. |
The exact minimum role for every action is in the Permission catalog.
Scopes#
| Scope | Covers |
|---|---|
office | Everything in the office. |
dept:<name> | Every agent whose personnel file has department: <name>. |
agent:<slug> | That one agent. |
A human's effective role for an action is the highest role among their grants whose scope covers the target. For example, lead on dept:engineering lets Dana kill runs of engineering agents, but not of agents in leadership.
policy_roles#
Who approves what, as lists of emails (default empty). These are separate from UI roles.
| Field | Approves a policy change that touches |
|---|---|
hiring_approvers | An agent: a new hire, a charter, or any personnel field other than the budget (capabilities, status, manager, run settings). |
access_approvers | access.yaml or connections/. |
budget_approvers | An agent's budget. |
Enforced on every merge in the office. When someone merges a policy PR in the office (the PR page, or Approve & merge on Needs you), the office works out from the diff which kinds the change touches and refuses unless that person is listed for every one of them. A change to a budget and to capabilities needs someone who is both a hiring and a budget approver. The owner may always merge. The PR page says who can merge it, and pr.merged records the kinds in the ledger.
The forge underneath still applies branch protection: only the approvers team may merge the policy repo's main, and --require-approvals <n> at officectl init adds required reviews. The office keeps that team in step with these lists. People never sign in to Gitea, so in practice every merge goes through the office; a Gitea admin who merges in Gitea directly bypasses the per-kind check.
Owner break-glass#
The owner.email in office.yaml is always an owner, even when access.yaml is missing, invalid, or doesn't list them. A broken policy file can't lock the owner out.