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 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.
POST /automaton.v1.RulesService/GetRules HTTP/1.1
Host: api.atmon.ai
Authorization: Bearer amk_your_project_key
Content-Type: application/json
{}
The response:
{
"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.
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:
{
"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.
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:
{
"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 |