leasebroker
v0.1.1
Published
Local-first broker that issues time-bounded, narrowly-scoped capability leases to AI agents and MCP servers, instead of standing permissions.
Maintainers
Readme
leasebroker
A local-first broker that issues time-bounded, narrowly-scoped capability leases to AI agents and their tools/MCP servers — instead of standing, broad permissions.
An agent asks for exactly the capability a task needs ("read these paths / call this API / spend ≤ $Y, for task T, for N minutes"); a policy decides; the broker issues a signed, scoped, expiring lease the agent must present to act. The broker enforces the scope in-path (MCP middleware proxy), logs every event to a tamper-evident audit trail, can revoke a lease mid-flight, and can require a human veto on high-risk grants. Deny-by-default, least-privilege.
Status: shipped. All lanes implemented and green:
tsc --noEmit,vitest,bun run build,node dist/cli/index.js --help, and the end-to-end demo all pass.
Install
npm install leasebroker
# or
bun add leasebrokerRun without installing:
npx leasebroker --helpCLI Commands
All commands share a --state-dir <path> flag (default: .leasebroker/ in cwd). Override with LEASEBROKER_STATE_DIR.
leasebroker request
Submit a lease request. Pass JSON via --request or stdin.
leasebroker request --request '{
"agentId": "my-agent",
"taskId": "task-42",
"capabilities": [
{ "kind": "fs.read", "paths": ["./data/**"] }
],
"requestedDurationMs": 3600000
}'Prints the granted PASETO token (or reqId for veto-required, or a denial reason).
leasebroker pending
List all pending (veto-required) requests awaiting approval.
leasebroker pendingleasebroker approve <reqId>
Approve a pending request; issues the lease.
leasebroker approve req-abc123leasebroker deny <reqId>
Deny a pending request; no lease is issued.
leasebroker deny req-abc123leasebroker revoke <leaseId>
Revoke an active lease before it expires.
leasebroker revoke lease-xyz789leasebroker serve
Start the MCP enforcement proxy fronting a downstream MCP server. The proxy intercepts every tools/call, verifies the lease, enforces scope, and forwards or denies.
# Front a downstream MCP server (stdio)
leasebroker serve \
--downstream-cmd node \
--downstream-args '["./my-mcp-server.js"]'
# With a custom policy file
leasebroker serve \
--downstream-cmd node \
--downstream-args '["./my-mcp-server.js"]' \
--policy ./rules.jsonAgents present their lease token in _meta['x-lease-token'] at the MCP initialize handshake. All subsequent tools/call requests are verified against that bound lease.
leasebroker policy
View or load policy rules.
# View current rules
leasebroker policy
# Load rules from a JSON file
leasebroker policy --load ./rules.jsonleasebroker audit
View the audit log (hash-chained, append-only).
# View last 20 events
leasebroker audit --last 20
# Filter by lease ID
leasebroker audit --lease-id lease-xyz789
# Filter by event type
leasebroker audit --type issuanceRunning the Demo
The demo shows two red→green scenarios entirely offline (no network, no real keys):
bun run demo
# or
npm run demoDemo 1 — Filesystem path-scope:
Unbrokered agent reads both ./fixtures/data/ and ./fixtures/private/. Brokered agent (lease: fs.read ./fixtures/data/**) — private directory read is DENIED.
Demo 2 — Spend cap + endpoint scope:
Unbrokered agent charges any amount to any endpoint. Brokered agent (lease: spend cap=100 USD, http.call api.example.com/**) — over-cap charge DENIED, off-list endpoint DENIED.
Programmatic API
import {
generateKeyPair, PasetoV4PublicSigner,
loadRules, DeclarativePolicyEngine,
InMemoryAuditSink, InMemoryPendingStore,
InMemoryRevocationList, InMemorySpendLedger,
Broker, LeaseEnforcer, LeasebrokerProxy,
} from 'leasebroker';
// Set up the stack
const kp = generateKeyPair('k1');
const signer = new PasetoV4PublicSigner(kp);
const policy = new DeclarativePolicyEngine(
loadRules([{ ruleId: 'allow-fs-read', effect: 'allow', capabilityKind: 'fs.read' }])
);
const broker = new Broker(policy, signer, new InMemoryAuditSink(), new InMemoryPendingStore(), kp.kid);
// Issue a lease
const result = broker.request({
agentId: 'my-agent',
taskId: 'task-1',
capabilities: [{ kind: 'fs.read', paths: ['./data/**'] }],
requestedDurationMs: 3_600_000,
});
if (result.type === 'granted') {
console.log('Token:', result.token); // v4.public.…
// Enforce it
const enforcer = new LeaseEnforcer(signer, new InMemoryRevocationList(), new InMemorySpendLedger());
const check = enforcer.check(result.token, { kind: 'fs.read', path: './data/hello.txt' });
console.log(check.ok); // true
}Development
bun install
npm run typecheck # tsc --noEmit
npm run test # vitest run
npm run build # compile to dist/
npm run demo # red→green capability-brokering demoDesign
- Specification (WHAT/WHY):
specs/lease-broker/spec.md - Architecture decisions:
docs/adrs.md - Implementation plan (HOW):
plan.md
Key architecture decisions:
- Enforcement is MCP middleware (ADR-B): a proxy that fronts downstream MCP servers, verifying scope on every tool call.
- Leases are PASETO v4.public tokens (Ed25519, via
@noble/ed25519) — tamper-evident and verifiable offline (ADR-A). - Policy is declarative allow-rules, with a seam to Cedar later (ADR-C).
- The lease is immutable; cumulative spend and revocation are tracked as state keyed by lease id (ADR-B/D).
License
Apache-2.0 — see LICENSE.
