Skip to content

scute.harness: the agent harness for Python, and scute.agents - #5

Merged
imit merged 1 commit into
mainfrom
harness
Sep 28, 2026
Merged

imit merged 1 commit into
mainfrom
harness

Conversation

@imit

@imit imit commented Sep 28, 2026

Copy link
Copy Markdown
Contributor

What this adds

scute.harness is the agent harness for Python (RB-41, step 6), the counterpart of @scute/harness (TypeScript) and Scute::Harness (Ruby). Guards decide on every tool call an agent makes, backed by Scute's engine for who may do what: the agent's roles, the person it works for, and the task.

from scute.harness import Harness, guards

harness = Harness(agent="support-bot", guards=[guards.permissions(), guards.approval(when={"tier": "high"})],
                  tools={"refund_invoice": {"tier": "high"}})
run = harness.run(acts_for=user_id, actions=["invoice:read", "invoice:refund"], ttl=900)
verdict = run.check("refund_invoice", {"invoice_id": "INV-1", "amount": 90})
# verdict.kind: proceed / transform / approve / verify / guide / redirect / deny
# verdict.message is for the model, verdict.say for the person
  • Tools map to permissions. refund_invoice needs invoice:refund on the invoice named by invoice_id (or invoiceId, or id). You can override that per tool: permission, key, tier, attributes, resource, or False for no check.
  • Runs.
    • The task is minted with the secret on first use.
    • harness.run(token=...) runs on a task token your backend minted, without the secret.
    • Runs resume by id from a store (in memory, or anything with get/set).
    • Checks go one at a time per run, so budgets and single-use proofs hold under parallel calls.
    • The run closes for good on task_closed, budget_exceeded or a dead token, and never quietly re-mints.
  • Guards. permissions (Scute's engine), verify_person, approval, requester_only, grounding, args, budget and content (credentials, PII, injection), plus define for your own. Each runs in enforce, monitor or observe mode, with on_decision and on_alert hooks. A guard that raises counts as deny.
  • Human steps, all with the task token.
    • Verification: start_verification, submit_code, verification_status, complete_verification (a challenge your backend started). The run's session carries the verification into the next check.
    • Reviewer approvals are filed for the exact call, with approval_status.
    • run.confirm for a person's confirmation.
    • run.human_tools() gives the model the scute_* tools, as callables with JSON Schema parameters.
  • In a tool: run.property(name) and run.sign(name, claims=... | data=...), plus run.whoami(), run.complete(), run.revoke(), run.wrap(tool) (also a decorator), run.after, run.allowed_tools.
  • No token or secret in reprs, snapshots, errors or hook events. Snapshots mask the task token and verification tokens.

scute.agents (secret key): create, get, list, update, delete, suspend, resume, tasks, start_task, complete_task, revoke_task, assertion.

Authz decisions now also carry the say line the agent check answers with.

Compared with TypeScript and Ruby

Same as Ruby:

  • the feature set: guards, modes, decisions and ranks, the messages for the model, human steps, human tools, properties
  • the hardening: approvals bound to the exact call, proofs spent last, arguments as context.args, per-person run state, budgets reserved under a lock, linear-time content patterns, token-boundary grounding

From TypeScript: the run also closes on budget_exceeded.

Not in this PR:

  • No async API. The harness is sync only; async wasn't a cheap add-on.
  • No framework adapters. TypeScript has the Vercel AI SDK adapter and Ruby has RubyLLM. Here, run.wrap and run.human_tools() are framework-neutral.
  • Session trust can't come from the harness. "Trusted in this session" grants exist in the API (AgentSession#trust!), but no route sets them. The harness passes the session on every check and records verification on it, but can't produce trusted_in_session.

Tests

  • Unit (fake agent API, ported from the Ruby specs including the regressions): 51 new tests. Full unit suite: 192 passed.

  • ruff, mypy src, mypy --strict src tests/live and the new test files: clean.

  • Live (tests/live/test_08_harness.py, against scute-api-v2), with an agent registered through scute.agents and a run for a test identity:

    • proceed inside the task
    • deny outside the task (outside_task, with the say line)
    • step-up: verify with 424242, and the next check passes (verified)
    • a reviewer's approval: filed for the exact call, approved with the admin API, then that call proceeds (approved); other arguments file a request of their own
    • a property's secret, plus a JWS and a raw ES256 signature, both checked against the property's JWKS
    • scute_whoami from the human tools
    • a budget of 2: the 3rd action is budget_exceeded, the run is closed and the agent paused, and later checks fail closed
    • a run on a task token minted by the backend, with no secret

    pytest -m live: 63 passed. It adds no sign-ins (it reuses the suite's signed-in test identity). After the run the app has no users, roles, resources, agents or properties.

Run the live suite with pytest -m live (setup in the README's "Live suite" section).

scute.harness is the Python counterpart of @scute/harness and
Scute::Harness: guards decide on every tool call an agent makes, backed by
Scute's engine for who may do what (the agent's roles, the person it works
for, and the task).

- Harness(agent, guards=, tools=, mode=, store=, on_decision=, on_alert=)
  with app_id / secret / base_url or a Scute client. Tool names map to
  permissions (refund_invoice -> invoice:refund, the object from invoice_id),
  overridable per tool.
- harness.run(acts_for=, actions=, resources=, ttl=, ...) mints the task
  with the secret on first use; harness.run(token=) runs on a task token your
  backend minted, without the secret.
- run.check(tool, args) returns a Verdict (proceed, transform, approve,
  verify, guide, redirect, deny) with a message for the model and a say line
  for the person; run.after, run.wrap (also a decorator).
- Guards: permissions, verify_person, approval, requester_only, grounding,
  args, budget, content, define; enforce / monitor / observe modes.
- Human steps with the task token: start_verification, submit_code,
  verification_status, complete_verification; reviewer approvals filed for
  the exact call and approval_status; run.confirm; run.human_tools() as
  callables with JSON Schema parameters.
- run.property, run.sign, run.whoami, run.complete, run.revoke. The run
  closes for good on task_closed, budget_exceeded or a dead token. Checks go
  one at a time per run. Tokens never appear in reprs, snapshots or errors.

scute.agents: register agents, start, complete and revoke tasks, suspend and
resume, delegation assertions (secret key). authz decisions carry the say
line from agent checks.

Unit tests against a fake agent API (ported from the Ruby specs, regressions
included) and a live module (tests/live/test_08_harness.py): 63 live tests
pass against scute-api-v2.
@imit
imit merged commit aa016dd into main Sep 28, 2026
4 checks passed
@imit
imit deleted the harness branch September 28, 2026 19:59
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