Skip to main content
Glama
wayneColt
by wayneColt

granted

A time-bound, single-use grant that lets an agent carry out a decision a human already made — on a clock, with a receipt from the substrate, and never silently.

"you give your agent a task, then walk away and get a coffee, only to come back and find the agent got stuck on an approval on the first step and has made no progress. As a result, people often give in and set their agents to 'auto-approve', or --dangerously-skip-permissions, which is, obviously, unsafe."

— the cloudflare-os README

Cloudflare's answer is deferred approval: the agent proceeds against simulated results and the human approves later, in bulk. That fits a company where the human is back in minutes. granted is the answer for the owner-operator whose median time-to-keystroke is measured in days: approve once, in advance, with a deadline; the machine discharges it on the clock; the ledger — not the tool — confirms it; and silence is not a permitted outcome.

Not affiliated with Cloudflare. Python 3.11+, standard library only.

ci

30-second setup

pip install git+https://github.com/wayneColt/granted
granted mint --type report --id 2026-09-30 --by "owner keystroke" \
             --not-after 2026-10-01T00:00:00Z --allow send
granted status --type report --id 2026-09-30
granted discharge --type report --id 2026-09-30 --action send --exec ./send_report.sh

The last line prints one of discharged, refused, uncertain, expired with a reason, and exits 0, 3, 4 or 5 to match. Run it again: refused: consumed. Grants are files in ~/.granted/grants/; the ledger is ~/.granted/ledger.jsonl. GRANTED_HOME, GRANTED_STORE, GRANTED_LEDGER move them.

Related MCP server: MCP Customer Support Demo

Five clauses

  1. A decision is a grant. One object, once. The human's decision becomes one file with the object's type and id in it. There is no grant for "things like this".

  2. A grant carries its deadline. not_after is required; not_before is optional. A grant with no deadline is a standing permission, and the validator refuses it.

  3. The executor discharges inside the grant and never outside it. It may never mint. The library has no path from an executor, an MCP client, or a runner to a new grant. Minting is a CLI verb, run by a hand.

  4. Every outcome is loud. discharged | refused | uncertain | expired, each with a reason from a closed vocabulary, each written to the ledger as an outcome row, each returned — never raised — to the caller. A refusal leaves a row. An expiry leaves a row.

  5. Confirmation comes from the substrate. An intent row before the action, a result row the executor writes after it, and the grant's own consumed flag and receipt, re-read from disk. The executor's return value alone confirms nothing: success with no result row is uncertain.

What stays human

The primitive is for decisions already made. It does not make them, rank them, or infer them. These verbs never ride a grant by default, and nothing here will discharge them unless a human mints for that exact object, on purpose:

  • hire

  • negotiate

  • sign

  • pay

  • first contact with no prior thread

not_scope exists so the grant can say so in words the approver reads.

The grant

{
  "kind": "granted/1",
  "decision": "APPROVE_ONCE",
  "object": {"type": "report", "id": "2026-09-30"},
  "scope": "send the weekly report to the list it went to last week",
  "not_scope": ["any other recipient", "any attachment not already in the draft"],
  "allowed_actions": ["send"],
  "not_before": null,
  "not_after": "2026-10-01T00:00:00Z",
  "minted_by": "owner keystroke",
  "minted_at": "2026-09-20T12:00:00Z",
  "consumed": false,
  "consumed_at": null,
  "receipt": null
}

field

meaning

decision

APPROVE_ONCE: spent on the first discharge. APPROVE_SESSION: many discharges of the same object inside one window; spent at the first outcome that is not discharged, or at not_after.

object

{type, id}. The grant is about this and nothing else.

scope, not_scope

words for the human; the library does not parse them

allowed_actions

if non-empty, a discharge must name one of them

not_before, not_after

ISO 8601 with a zone. Naive stamps are refused.

minted_by

the hand. A string, not an identity system.

consumed, consumed_at, receipt

written by the store; the receipt names the discharge that spent it and its outcome

The JSON Schema is at docs/grant.schema.json. One file per grant; the store finds the live one for an object and reports none | consumed | not_yet | expired | unreadable when there is none.

The state machine

find grant ──none/consumed/not_yet/unreadable──▶ refused
    │        └──expired──────────────────────────▶ expired
    ▼
write intent row ──fails──▶ refused (ledger_unwritable; nothing runs)
    ▼
claim grant (receipt: pending) ──lost──▶ refused
    ▼
executor(grant)
    ├─ raises Uncertain ─────────────────────▶ uncertain  (spent)
    ├─ raises ExecutorFailed / anything ────▶ refused    (spent: effects unknown)
    ├─ returns falsy ───────────────────────▶ refused    (executor_declined; grant stays live)
    └─ returns truthy
         ▼
       ledger.confirms(object)? no ─────────▶ uncertain  (no_result_row; spent)
         ▼ yes, and the row says ok
       settle receipt, re-read grant ──mismatch─▶ uncertain (spent)
         ▼
       discharged  (APPROVE_ONCE: spent)

Once the executor has been called, the grant is spent whatever happens next — except when the executor positively declines, which means "nothing done, try me later". Retrying anything else is a human act: mint again. The full reason vocabulary is granted.discharge.REASONS.

The executor

The library never knows how to send an email or deploy anything. The host supplies executor(grant):

it

means

returns truthy

claims success — and must have written a result row

returns falsy

did nothing; the grant stays live

raises granted.Uncertain

cannot say whether it happened

raises granted.ExecutorFailed

failed part-way; effects unknown

raises anything else

a bug; treated like a failure

from granted import Store, Ledger, discharge

store, ledger = Store("~/.granted/grants"), Ledger("~/.granted/ledger.jsonl")

def send_report(grant):
    message_id = mailer.send(draft_for(grant.object_id))      # the host's code
    ledger.result(grant.object, ok=True, receipt={"message_id": message_id})
    return True

outcome = discharge(store, ledger, {"type": "report", "id": "2026-09-30"}, send_report, action="send")
print(outcome)            # discharged: confirmed (report/2026-09-30) -- result row line 42

The shell executor behind granted discharge --exec writes the result row itself (exit status, stdout and stderr tails). Exit 0 claims success; exit 75 (EX_TEMPFAIL) declines; any other exit is a failure; a --timeout is uncertain. The command sees GRANTED_OBJECT_TYPE, GRANTED_OBJECT_ID, GRANTED_DISCHARGE_ID, GRANTED_GRANT_PATH, GRANTED_LEDGER, GRANTED_NOT_AFTER in its environment.

CLI

granted mint      --type T --id I --by "who" --not-after ISO [--not-before ISO] [--decision D] [--scope ..] [--not-scope ..] [--allow ..]
granted status    --type T --id I
granted discharge --type T --id I --exec "cmd" [--action A] [--timeout S]
granted ledger    [--tail N] [--id I]
granted lag       [FILE|-]
granted mcp       [--executor NAME=COMMAND ...]

--json on any of them. python -m granted works from a clone without installing.

MCP server

granted mcp --executor send_report=./send_report.sh

stdio, JSON-RPC 2.0, no third-party code. Implements initialize, ping, tools/list, tools/call. Three tools:

tool

does

grant_status

live | none | consumed | not_yet | expired | unreadable, the grant on file, the last ledger rows

grant_discharge

discharge through an executor the host registered by name; returns one of the four outcomes

ledger_tail

the last N rows

There is deliberately no mint. Minting is the human's act and lives in the CLI. An agent on this server can learn, discharge, and read back; it cannot widen its own permissions and cannot hand the executor a command of its own.

Client configuration, for a host that takes the usual shape:

{"mcpServers": {"granted": {"command": "granted", "args": ["mcp", "--executor", "send_report=./send_report.sh"]}}}

The clock

adapters/systemd/ is a granted-discharge@.timer + .service pair and a runner. The timer ticks every 15 minutes; the grant's own window decides; the runner stops the timer once there is nothing left to ask. uncertain is left as a failed unit on purpose.

Measuring the lag

The motivation is a number. Keep a JSONL of {"raised": ISO, "answered": ISO} for every time an agent waited on a keystroke, then:

$ granted lag asks.jsonl
n=10 open=1 bad=1 p50=10.0h p80=72.0h max=240.0h

Nearest-rank percentiles. open is the count still waiting. If p50 is minutes, use deferred approval. If it is days, mint.

Status

Surface

Status

Core: schema, store, ledger, discharge

yes; 63 tests under python -m unittest

CLI: mint, status, discharge, ledger, lag, mcp

yes

MCP server (stdio; initialize, ping, tools/list, tools/call)

yes; no mint by design

systemd adapter

yes (unit, timer, runner)

cloudflare-os Gatekeeper

design note + TypeScript skeleton; not compiled or deployed

Door (signed admission endpoint)

contract only

Signed intents in the CLI / MCP path

not in 0.1.0; both trust the process boundary

Store across hosts

not in 0.1.0; a directory on one disk, one lock file per grant

Windows

untested

Build

git clone https://github.com/wayneColt/granted
cd granted
python -m unittest -v
python -m granted --help

No dependencies. License: Apache-2.0 OR MIT.

What this is not

  • Not an approval queue. Nothing waits here; a grant exists or it does not.

  • Not an identity system. minted_by is a string the human types. The door contract in docs/DOOR.md says where signatures go; this release has none.

  • Not a scheduler. The systemd timer is a tick; the deadline is the grant's.

  • Not a policy engine. It cannot tell a safe action from a dangerous one. That is what the human did when they minted, and what not_scope is for.

Release drafts (X, blog) live in RELEASE_DRAFTS.md and wait for an operator keystroke.

Available Tools

3 tools
grant_dischargeA

Carry out the decision a human already recorded for this object, through an executor the host registered by name. Returns one of discharged | refused | uncertain | expired with a reason. Cannot mint; cannot run anything the host did not register.

ParametersJSON Schema
NameRequiredDescriptionDefault
idYesobject id, e.g. 2026-09-30
typeYesobject type, e.g. report
actionNowhich allowed action, if the grant names any
executorYesa registered executor name

TDQS

A4.2/5.0
Behavior4/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

With no annotations provided, the description carries the behavioral disclosure burden. It discloses the full outcome set ('discharged | refused | uncertain | expired with a reason') and major invariants ('Cannot mint; cannot run anything the host did not register'), which is substantially more transparent than typical mutation tools.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Three tight sentences with no filler: purpose is front-loaded, return values are enumerated, and hard constraints are stated. Every sentence adds information an agent needs to decide whether and how to call the tool.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness4/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Despite having no output schema and no annotations, the description supplies the outcome vocabulary, the precondition, and the tool's boundaries. It is adequate for a small 4-parameter tool, though it does not explicitly describe error or not-found behavior.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

The schema already has 100% description coverage for all four parameters, so the baseline is 3. The description reinforces that executor must be a registered name and links type/id to a recorded decision, but it does not add meaningful format or value-level semantics beyond what the schema provides.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description names a specific verb ('carry out'), a specific resource ('the decision a human already recorded for this object'), and the mechanism ('executor the host registered by name'). It also distinguishes the tool from status/list siblings by enumerating possible return statuses and stating 'Cannot mint.'

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The usage context is clear: this tool executes an already-recorded human decision through a registered executor. It also gives explicit exclusions ('Cannot mint; cannot run anything the host did not register'), but it does not explicitly point to sibling tools such as grant_status or ledger_tail as alternatives.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

grant_statusB

Is there a live grant for this object? Returns live | none | consumed | not_yet | expired | unreadable, the grant on file, and the last ledger rows.

ParametersJSON Schema
NameRequiredDescriptionDefault
idYesobject id, e.g. 2026-09-30
typeYesobject type, e.g. report

TDQS

B3.2/5.0
Behavior3/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

With no annotations, the description carries the full burden of behavioral disclosure. It does disclose the possible status values and the returned components, including an 'unreadable' case, which is useful. However, it does not explicitly state read-only behavior, permission implications, or other edge conditions beyond the status list.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is compact and front-loaded: a clear question followed by a concise return-value list. It contains no filler or schema repetition, though the dense status enumeration could be slightly better structured.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness4/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a low-complexity two-parameter tool with no output schema, the description is mostly complete: it names the returned statuses and the accompanying grant/ledger data. It is not fully complete because it omits sibling routing and behavioral caveats, but an agent can reasonably invoke it from this description.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 100%, so the schema already documents the two required parameters with examples. The tool description adds only a generic 'this object' reference and no additional parameter semantics, formats, or constraints.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose4/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description clearly identifies the tool's goal: checking whether a live grant exists for an object, and it lists the exact statuses returned. It does not explicitly contrast itself with sibling tools grant_discharge or ledger_tail, so it misses the highest-level sibling differentiation.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines2/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

No guidance is given about when to use this tool versus grant_discharge or ledger_tail. An agent must infer when checking grant status is appropriate, and there are no exclusions or alternative conditions stated.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

ledger_tailC

The last N ledger rows (intent, result, outcome), optionally for one object id.

ParametersJSON Schema
NameRequiredDescriptionDefault
nNo
object_idNo

TDQS

C2.9/5.0
Behavior2/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

No annotations are provided, so the description carries the full burden of behavioral disclosure. It does not state whether the operation is read-only, how rows are ordered, whether there are side effects, or what happens with an invalid object id. It only describes the shape of the returned data.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is a single sentence that front-loads the main output (last N ledger rows) and then states the optional filter. Every word contributes meaning, and there is no redundancy.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness2/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a tool with no annotationsinations and no output schema, the description is too thin. It does not mention ordering, return shape beyond field names, potential errors, or how this tool relates to the sibling grant_status and grant_discharge tools, leaving an agent without enough context to invoke it confidently.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 0%, but the description does clarify that n refers to the number of rows and object_id is an optional filter for a single object. It does not explain what kind of object_id is expected or how filtering interacts with the last N rows, leaving some ambiguity.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose4/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description identifies the resource (ledger rows), the fields included (intent, result, outcome), and an optional filter (object id), which makes the core purpose clear. However, it lacks an explicit verb like 'retrieve' or 'list', and does not distinguish itself from the sibling tools by name.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines2/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

There is no guidance on when to use this tool versus grant_status or grant_dischargeaging. The phrase 'optionally for one object id' describes a parameter behavior, not a use case or exclusion, leaving tool selection to inference.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

Tool Schema Changelog

Recent tool additions, removals, and schema changes observed during successful MCP inspections.

  1. 3 tool updatesv0.1.0
    • First observedgrant_discharge
    • First observedgrant_status
    • First observedledger_tail

TDQS

A3.7/5.0

Scored across 3 tools

Disambiguation5/5

Each tool has a clearly distinct purpose: grant_status checks the current state of a grant, grant_discharge executes a recorded human decision, and ledger_tail provides raw ledger history. Even though status and ledger_tail both touch ledger rows, their focus and output differ enough to avoid confusion.

Naming Consistency4/5

All tool names use lowercase snake_case and follow a consistent 'domain_action/noun' style, such as grant_status, grant_discharge, and ledger_tail. They deviate from a strict verb_noun pattern but are still predictable and internally consistent.

Tool Count5/5

With only three tools, the surface is tightly scoped to the server's apparent purpose: checking grant state, discharging grants, and auditing the ledger. Each tool covers a distinct operation and none feels redundant or unnecessary.

Completeness5/5

The server intentionally does not create or mint grants—those decisions are recorded by humans and executed via grant_discharge. Within that scope, status checking, execution, and ledger auditing form a complete workflow with no obvious dead ends.

Maintenance

ActivityMaintained
ResponsivenessNo issues

Related MCP Connectors

Related MCP Servers