<!-- Generated by `just docs` from proto/automaton/v1/policy.proto. Edit the source, not this file. -->

# RulesService

Read the rules this project runs under, in the same words the console shows them in, and propose a change to them. Proposing writes nothing: it files a proposal that lands in the project's inbox, and a person approves or declines it there. That boundary is the point rather than a limitation, and it is the same one the approval gate draws around a parked call.

Every call is a POST to `https://api.atmon.ai/automaton.v1.RulesService/<Call>` with a JSON body, and authenticates with `Authorization: Bearer <your project key>`. Field names in JSON are lowerCamelCase, so the field written `tool_slug` below is `toolSlug` on the wire. [How to call the API](./index.md) has the whole convention.

## Calls

| Call | Request | Response | Summary |
| --- | --- | --- | --- |
| `GetRules` | `GetRulesRequest` | `GetRulesResponse` | Reads the project's rules in plain words, grouped by what each one governs. |
| `ProposeRulesChange` | `ProposeRulesChangeRequest` | `ProposeRulesChangeResponse` | Files a proposed change to the rules. |
| `GetRuleProposal` | `GetRuleProposalRequest` | `GetRuleProposalResponse` | Reads one filed proposal in any state, so a caller can wait for the decision and report it. |

### GetRules

Reads the project's rules in plain words, grouped by what each one governs.
An empty answer at version 0 means no rules have ever been written.

Request `GetRulesRequest`, response `GetRulesResponse`.

```http
POST /automaton.v1.RulesService/GetRules HTTP/1.1
Host: api.atmon.ai
Authorization: Bearer amk_your_project_key
Content-Type: application/json

{}
```

The response:

```json
{
  "version": 0,
  "rules": [{
    "area": "...",
    "text": "..."
  }]
}
```

### ProposeRulesChange

Files a proposed change to the rules. It writes no policy: the answer is a
proposal id in the PENDING state, and a person approves or declines it in
the console. Refiling an identical pending proposal answers with the one
already filed. A project may file a small number of proposals per hour;
past that the call is refused with resource_exhausted.

Request `ProposeRulesChangeRequest`, response `ProposeRulesChangeResponse`.

```http
POST /automaton.v1.RulesService/ProposeRulesChange HTTP/1.1
Host: api.atmon.ai
Authorization: Bearer amk_your_project_key
Content-Type: application/json

{
  "changes": [{
    "setSpendCeiling": { ... },
    "requireApproval": { ... },
    "setAllowedTools": { ... }
  }],
  "reason": "..."
}
```

The response:

```json
{
  "proposal": {
    "id": "...",
    "changes": [{ ... }],
    "reason": "...",
    "effects": ["..."],
    "state": "RULE_PROPOSAL_STATE_PENDING",
    "note": "...",
    "createdAt": "2026-01-31T09:15:00Z",
    "decidedAt": "2026-01-31T09:15:00Z",
    "appliedPolicyVersion": 0
  },
  "deduplicated": true
}
```

### GetRuleProposal

Reads one filed proposal in any state, so a caller can wait for the
decision and report it. A declined proposal reads as declined with whatever
the decider wrote; do not file it again.

Request `GetRuleProposalRequest`, response `GetRuleProposalResponse`.

```http
POST /automaton.v1.RulesService/GetRuleProposal HTTP/1.1
Host: api.atmon.ai
Authorization: Bearer amk_your_project_key
Content-Type: application/json

{
  "id": "..."
}
```

The response:

```json
{
  "proposal": {
    "id": "...",
    "changes": [{ ... }],
    "reason": "...",
    "effects": ["..."],
    "state": "RULE_PROPOSAL_STATE_PENDING",
    "note": "...",
    "createdAt": "2026-01-31T09:15:00Z",
    "decidedAt": "2026-01-31T09:15:00Z",
    "appliedPolicyVersion": 0
  }
}
```

## Messages

### GetRuleProposalRequest

| Field | Type | # | Notes |
| --- | --- | --- | --- |
| `id` | `string` | 1 |  |

### GetRuleProposalResponse

| Field | Type | # | Notes |
| --- | --- | --- | --- |
| `proposal` | `RuleProposal` | 1 |  |

### GetRulesRequest

No fields. The call takes its scope from the authenticated project.

### GetRulesResponse

| Field | Type | # | Notes |
| --- | --- | --- | --- |
| `version` | `int64` | 1 | version is the policy document's version. Zero means the project has never written rules, and rules is then empty: nothing is capped, nothing is gated, and everything in the catalog is allowed. |
| `rules` | repeated `RuleStatement` | 2 |  |

### ProposeRulesChangeRequest

| Field | Type | # | Notes |
| --- | --- | --- | --- |
| `changes` | repeated `RulesChange` | 1 |  |
| `reason` | `string` | 2 |  |

### ProposeRulesChangeResponse

| Field | Type | # | Notes |
| --- | --- | --- | --- |
| `proposal` | `RuleProposal` | 1 |  |
| `deduplicated` | `bool` | 2 | deduplicated is true when an identical proposal was already pending and this call answered with that one instead of filing a second. Refiling the same change is not an error and it does not make a second row. |

### RequireApproval

RequireApproval proposes adding calls to the approval gate. It only ever
adds: a proposal cannot take a tool out of the gate, because loosening the
gate through the surface an agent writes to would make the gate advisory.
Removing one is a console edit by a person.

| Field | Type | # | Notes |
| --- | --- | --- | --- |
| `tool_slugs` | repeated `string` | 1 | tool_slugs are catalog-wide slugs, "github.delete_repo". Requiring approval for one app's destructive actions means listing that app's destructive tools; the document has no per-app rule, and naming the tools is what it can actually enforce. |
| `include_destructive_class` | `bool` | 2 | include_destructive_class gates every tool the catalog classes destructive, in every app. It can be set true and never false. |

### RuleProposal

RuleProposal is one filed change and everything decided about it.

| Field | Type | # | Notes |
| --- | --- | --- | --- |
| `id` | `string` | 1 |  |
| `changes` | repeated `RulesChange` | 2 |  |
| `reason` | `string` | 3 | reason is what the proposer said the change is for, in its own words. It is shown to whoever decides, so it is the argument rather than a label. |
| `effects` | repeated `string` | 4 | effects is the change in plain words, one line per change, the same sentences the console renders beside the decision. |
| `state` | `RuleProposalState` | 5 |  |
| `note` | `string` | 6 | note is what whoever decided it said. On a decline it is the reason not to file the same thing again. |
| `created_at` | `google.protobuf.Timestamp` | 7 |  |
| `decided_at` | `google.protobuf.Timestamp` | 8 |  |
| `applied_policy_version` | `int64` | 9 | applied_policy_version is the document version the approval wrote. Zero while the proposal is pending and on one that was declined. |

### RuleStatement

Rules as an agent reads them, and proposes changes to.

The document above is written for the code that enforces it. An assistant
asked "what are the rules here" needs the same facts in the language the
person who wrote them used, and an assistant that spots a missing ceiling
needs somewhere to say so. Neither of those is a policy write: the asker is
never the approver, so a change an agent drafts becomes a proposal that lands
beside the approvals in the project's inbox, and a person decides it.

RuleStatement is one rule in force, written the way the console shows it
rather than the way the document stores it.

| Field | Type | # | Notes |
| --- | --- | --- | --- |
| `area` | `string` | 1 | area groups the statement, one of: spending, approvals, tools, pace, data, retention, visibility, connections. |
| `text` | `string` | 2 |  |

### RulesChange

RulesChange is one edit to the project's rules. Exactly one field is set, and
a change carrying none or more than one is refused when it is filed rather
than stored as something nobody can apply. A proposal that wants two edits
carries two of these.

Three fields rather than a oneof: on the JSON wire the two are written
identically, and the rule is enforced at the desk either way, with a message
naming the field and what to send instead.

| Field | Type | # | Notes |
| --- | --- | --- | --- |
| `set_spend_ceiling` | `SetSpendCeiling` | 1 |  |
| `require_approval` | `RequireApproval` | 2 |  |
| `set_allowed_tools` | `SetAllowedTools` | 3 |  |

### SetAllowedTools

SetAllowedTools proposes narrowing what may run to a named list. An empty
list on either field leaves that list alone; the document's own convention is
that an empty allow list allows everything, so clearing one is a loosening
and is not proposable here.

| Field | Type | # | Notes |
| --- | --- | --- | --- |
| `allowed_tools` | repeated `string` | 1 | catalog-wide slugs |
| `allowed_toolkits` | repeated `string` | 2 |  |

### SetSpendCeiling

SetSpendCeiling proposes one ceiling. A ceiling for the same scope, window,
entity and toolkit replaces the one in force; a new combination is added. A
proposal setting every dimension to zero removes the ceiling, which is a
loosening and reads that way in the proposal's own words.

| Field | Type | # | Notes |
| --- | --- | --- | --- |
| `scope` | `BudgetScope` | 1 |  |
| `entity_id` | `string` | 2 | BUDGET_SCOPE_ENTITY only |
| `window` | `BudgetWindow` | 3 |  |
| `max_model_micro_usd` | `int64` | 4 |  |
| `max_actions` | `int64` | 5 |  |
| `max_provider_calls` | `int64` | 6 |  |
| `toolkit_slug` | `string` | 7 | max_provider_calls only |

## Enums

### BudgetScope

BudgetScope is whose spending a ceiling counts, following VelocityLimit's
scopes minus the tool scope: money is spent by a project or by one of its
entities, and a per-tool ceiling would be a quota, which max_provider_calls
already expresses.

| Value | # | Meaning |
| --- | --- | --- |
| `BUDGET_SCOPE_UNSPECIFIED` | 0 |  |
| `BUDGET_SCOPE_PROJECT` | 1 |  |
| `BUDGET_SCOPE_ENTITY` | 2 |  |

### BudgetWindow

BudgetWindow is the period a ceiling applies over, evaluated in UTC. TOTAL
never resets, so only raising the ceiling releases what it parked; DAILY and
MONTHLY reset at the UTC boundary and release a parked job with no operator
action.

| Value | # | Meaning |
| --- | --- | --- |
| `BUDGET_WINDOW_UNSPECIFIED` | 0 |  |
| `BUDGET_WINDOW_TOTAL` | 1 |  |
| `BUDGET_WINDOW_DAILY` | 2 |  |
| `BUDGET_WINDOW_MONTHLY` | 3 |  |

### RuleProposalState

| Value | # | Meaning |
| --- | --- | --- |
| `RULE_PROPOSAL_STATE_UNSPECIFIED` | 0 |  |
| `RULE_PROPOSAL_STATE_PENDING` | 1 | waiting on a person |
| `RULE_PROPOSAL_STATE_APPROVED` | 2 | applied to the document |
| `RULE_PROPOSAL_STATE_DECLINED` | 3 | decided against, and it stays on the record |
