Skip to content

fix: close least-privilege holes (self-approval, split payments, caller identity) - #71

Draft
FetchDoge wants to merge 6 commits into
mainfrom
fix/least-privilege-holes
Draft

FetchDoge wants to merge 6 commits into
mainfrom
fix/least-privilege-holes

Conversation

@FetchDoge

Copy link
Copy Markdown
Owner

Closes three least-privilege holes found by a runtime harness on 794eaa1. The four-clause gate (_evaluate) is unchanged; the new checks run outside it. Amounts remain positive integer cents.

Holes and fixes

  • H1, self-approval: an agent can no longer approve or deny its own pending spend (self_approval). An optional principal_id at grant binds the approver (principal_mismatch).
  • H2, split payments: the approval threshold is now cumulative over the permit's lifetime by default (reserved + captured + amount). approval_mode="per_spend" remains as an explicit opt-out.
  • H3, caller identity: require_agent_id=True is now the default and is inherited by child permits. Missing → agent_id_required, wrong → agent_mismatch. agent_id is carried through /check, /spend, agent/tools.py, the durable store and the ledger rebuild.
  • Old permits reloaded from an existing ledger come back strict (cumulative and agent_id required), by design.

Breaking HTTP API changes

  • /check and /spend now require agent_id. Without it the response is allowed=false with reason agent_id_required; on /spend one BLOCKED receipt is written, nothing is reserved and PayPal is not called.
  • Several small spends that previously cleared on their own now go to pending_principal_approval once the running total passes the threshold.
  • POST /api/permits accepts optional approval_mode, require_agent_id and principal_id, with new 400s for invalid values. GET /api/permits/<id> returns them.

Evidence (mock rail, local)

  • pytest: 437 passed, 3 skipped (baseline 413 passed, 3 skipped at 794eaa1).
  • demo.py: ledger VERIFIED, 16 receipts.
  • Six-beat --fast replay against the existing fixture: VERIFIED, 26 receipts. The fixture was not regenerated (sha256 unchanged).
  • Least-privilege harness on default settings, 3 runs: 0 escapes, 0 false denies, H1/H2/H3 absent.

Known limits

  • agent_id and the approver are caller-supplied strings, not authenticated credentials.
  • The dashboard approves as actor dashboard, so it is refused on permits that name a principal_id.
  • demo_approval.py, demo_delegation.py (live LLM) and the 3 live-sandbox tests were not run.

Draft. Not for merge without the maintainer's explicit approval.

…ools, durable store and ledger fold

- server.py: /check and /spend accept agent_id (400 if not a non-empty
  string); POST /api/permits accepts approval_mode, require_agent_id,
  principal_id (400 on invalid); GET /api/permits/<id> exposes them.
- agent/tools.py: SpendTools binds the caller agent_id at construction
  (defaults to the permit's agent) and passes it on every spend.
- durability: grant/check/spend forward the least-privilege fields and
  agent_id; snapshot records them.
- sqlite_ledger fold: GRANTED restores principal_id/approval_mode/
  require_agent_id; DELEGATED children inherit require_agent_id.
… their caller agent_id

Mechanical: every in-process spend()/check()/eligible() and HTTP
/check|/spend body now passes the permit's own agent_id, as an honest
caller would. No assertion changed. Unknown-permit cases stay anonymous.
… default

- Permit/grant defaults: approval_mode='cumulative' (lifetime reserved +
  captured + amount vs threshold), require_agent_id=True (carves inherit).
  approval_mode='per_spend' and require_agent_id=False stay as explicit
  opt-outs. _evaluate (four-clause gate) unchanged.
- Tests that assert per-spend or anonymous-caller behaviour now set the
  explicit opt-out; new tests pin the defaults (in-process, HTTP, carve
  inheritance, ledger fold).
- README: document the new defaults and that agent_id is a caller-
  supplied string, not an authenticated credential.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant