{"_id":"@avee1234/capgrant","_rev":"4-4b1ddcae098a516236452b42fdcb315e","name":"@avee1234/capgrant","dist-tags":{"latest":"0.3.1"},"versions":{"0.1.0":{"name":"@avee1234/capgrant","version":"0.1.0","keywords":["ai","agent","capability","authorization","permission","scope","grant","delegation","least-privilege","multi-agent","zero-dependency","claude-code","codex","cursor","antigravity"],"license":"MIT","_id":"@avee1234/capgrant@0.1.0","maintainers":[{"name":"avee1234","email":"das.abhijit34@gmail.com"}],"bin":{"capgrant":"bin/capgrant.js"},"dist":{"shasum":"6a3a52bff859e43c384a9869f710312870f985a8","tarball":"https://registry.npmjs.org/@avee1234/capgrant/-/capgrant-0.1.0.tgz","fileCount":13,"integrity":"sha512-rFNletiWaKiuYuMFJfrfeK4q+WDMwja5YmZTA98ffruHjMYLn1fXEBLUz6qwe/in8VEBwOYWwf9AyD3sdeqc3A==","signatures":[{"sig":"MEUCIQC167vME/F0HwR8tmLtC6PSDR23wXNGDbZt3eefucfoVAIgGz7s6Z+1enrx8EzqiuRSZbJYmAuNTV5F+9ufsMrHD1Q=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":89990},"main":"src/index.js","type":"module","engines":{"node":">=18"},"gitHead":"7c3d9b2339177147ab120e1d9377e13c47b82d17","scripts":{"test":"node --test"},"_npmUser":{"name":"avee1234","email":"das.abhijit34@gmail.com"},"_npmVersion":"10.9.8","description":"The open format for scoped, expiring agent capability grants. An issuer grants an agent scoped capabilities (action + resource, with a TTL); the agent checks before acting and an audit scores whether actions stayed in scope. Hierarchical actions, delegati","directories":{},"_nodeVersion":"22.22.3","_hasShrinkwrap":false,"_npmOperationalInternal":{"tmp":"tmp/capgrant_0.1.0_1784000409822_0.5882823649845499","host":"s3://npm-registry-packages-npm-production"}},"0.2.0":{"name":"@avee1234/capgrant","version":"0.2.0","keywords":["ai","agent","capability","authorization","permission","scope","grant","delegation","constraints","least-privilege","multi-agent","signatures","hmac","ed25519","zero-dependency","claude-code","codex","cursor","antigravity"],"license":"MIT","_id":"@avee1234/capgrant@0.2.0","maintainers":[{"name":"avee1234","email":"das.abhijit34@gmail.com"}],"bin":{"capgrant":"bin/capgrant.js"},"dist":{"shasum":"c69b6334cce75aa3bc76671fd20a1b10788e0d78","tarball":"https://registry.npmjs.org/@avee1234/capgrant/-/capgrant-0.2.0.tgz","fileCount":14,"integrity":"sha512-tEp5Kxyf9m9Fb5J7LhcTXp/l8FlQTwIKMupd0pIbeq8vIQRj1tUsmnr6TnGMNl0tOCgS0tZvUL/vuKTEHbP6Vw==","signatures":[{"sig":"MEUCIB0UkoonS0U6OFof32tNuRFCBaVjOPZW/pk23guwnLCLAiEAue5b4BM7egXPf1y53+l1R8VYANyN5P4sr9nIe/JogOM=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":112604},"main":"src/index.js","type":"module","engines":{"node":">=18"},"gitHead":"e9ad5b26eb2b4edda58149497a6269feea07dae1","scripts":{"test":"node --test"},"_npmUser":{"name":"avee1234","email":"das.abhijit34@gmail.com"},"_npmVersion":"10.9.8","description":"The open format for scoped, expiring agent capability grants. An issuer grants an agent scoped capabilities (action + resource, with a TTL); the agent checks before acting and an audit scores whether actions stayed in scope. Hierarchical actions, delegati","directories":{},"_nodeVersion":"22.22.3","_hasShrinkwrap":false,"_npmOperationalInternal":{"tmp":"tmp/capgrant_0.2.0_1784054651831_0.3845253910061226","host":"s3://npm-registry-packages-npm-production"}},"0.3.0":{"name":"@avee1234/capgrant","version":"0.3.0","keywords":["ai","agent","capability","authorization","permission","scope","grant","delegation","constraints","approval","human-in-the-loop","hitl","least-privilege","multi-agent","signatures","hmac","ed25519","zero-dependency","claude-code","codex","cursor","antigravity"],"license":"MIT","_id":"@avee1234/capgrant@0.3.0","maintainers":[{"name":"avee1234","email":"das.abhijit34@gmail.com"}],"bin":{"capgrant":"bin/capgrant.js"},"dist":{"shasum":"9f452ddaad8b1dfd9784e1bf2c783e3954890905","tarball":"https://registry.npmjs.org/@avee1234/capgrant/-/capgrant-0.3.0.tgz","fileCount":15,"integrity":"sha512-+wwbCRoiD69OOwZy6QA35gQabOVdru7wj+QwsgBURjWeJt4UfaCWi51aJT1E78NJ2qwrW2QfwvPKUnKvJ7GBJw==","signatures":[{"sig":"MEUCIQDsq1uM3vAgR9tgUrLuBicuFoJl9g7IisQzG/AeUUSRAQIgUIan5o1K9D1w77/AUEI9pZ+J1Ni8uonlrIRLSdS6MCs=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":147224},"main":"src/index.js","type":"module","engines":{"node":">=18"},"gitHead":"030430bc650df09fde9e0c0e850a4b60c5afa875","scripts":{"test":"node --test"},"_npmUser":{"name":"avee1234","email":"das.abhijit34@gmail.com"},"_npmVersion":"10.9.8","description":"The open format for scoped, expiring agent capability grants. An issuer grants an agent scoped capabilities (action + resource, with a TTL); the agent checks before acting and an audit scores whether actions stayed in scope. Hierarchical actions, delegati","directories":{},"_nodeVersion":"22.22.3","_hasShrinkwrap":false,"_npmOperationalInternal":{"tmp":"tmp/capgrant_0.3.0_1784073105174_0.13457111780994424","host":"s3://npm-registry-packages-npm-production"}},"0.3.1":{"name":"@avee1234/capgrant","version":"0.3.1","description":"The open format for scoped, expiring agent capability grants. An issuer grants an agent scoped capabilities (action + resource, with a TTL); the agent checks before acting and an audit scores whether actions stayed in scope. Hierarchical actions, delegati","type":"module","main":"src/index.js","bin":{"capgrant":"bin/capgrant.js"},"engines":{"node":">=18"},"scripts":{"test":"node --test"},"keywords":["ai","agent","capability","authorization","permission","scope","grant","delegation","constraints","approval","human-in-the-loop","hitl","least-privilege","multi-agent","signatures","hmac","ed25519","zero-dependency","claude-code","codex","cursor","antigravity"],"license":"MIT","gitHead":"4050c52eb84e94b0e0fc0272b46e2a03034461fe","_id":"@avee1234/capgrant@0.3.1","_nodeVersion":"25.8.0","_npmVersion":"11.11.0","dist":{"integrity":"sha512-EJVi3AmlHc1qM1a1jCzQalspRsDPRutCsWsxeDdP2zmMKUeKvrshAjZcXUuReCEheJ8GK22W0entdmqUoRz/Vg==","shasum":"037fb6c6601c971faffa3e672ca168cd45ab8bf1","tarball":"https://registry.npmjs.org/@avee1234/capgrant/-/capgrant-0.3.1.tgz","fileCount":15,"unpackedSize":156840,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEYCIQDb8LPE2j9btRTnVKhXXh4tsxRd+uJswN3yS2sGU/TV9wIhAMbuA6m5fsYRWrVCEahk1MYvFktQjtbxp3h2k8UlO8Tw"}]},"_npmUser":{"name":"avee1234","email":"das.abhijit34@gmail.com"},"directories":{},"maintainers":[{"name":"avee1234","email":"das.abhijit34@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/capgrant_0.3.1_1784123956987_0.8065704745348363"},"_hasShrinkwrap":false}},"time":{"created":"2026-07-14T03:40:09.606Z","modified":"2026-07-15T13:59:17.273Z","0.1.0":"2026-07-14T03:40:09.952Z","0.2.0":"2026-07-14T18:44:12.005Z","0.3.0":"2026-07-14T23:51:45.326Z","0.3.1":"2026-07-15T13:59:17.123Z"},"license":"MIT","keywords":["ai","agent","capability","authorization","permission","scope","grant","delegation","constraints","approval","human-in-the-loop","hitl","least-privilege","multi-agent","signatures","hmac","ed25519","zero-dependency","claude-code","codex","cursor","antigravity"],"description":"The open format for scoped, expiring agent capability grants. An issuer grants an agent scoped capabilities (action + resource, with a TTL); the agent checks before acting and an audit scores whether actions stayed in scope. Hierarchical actions, delegati","maintainers":[{"name":"avee1234","email":"das.abhijit34@gmail.com"}],"readme":"# capgrant\n\n**The open format for scoped, expiring agent capability grants.** An issuer grants an agent a set of *capabilities* — \"you may `fs.write` under `src/**` for the next 30 minutes\" — to a shared, append-only registry. Before it acts, the agent `check`s whether a live grant authorizes the action; after the fact, an `audit` scores whether the fleet stayed in scope. So a fleet of agents stops running with unbounded ambient authority, and you can prove afterward that each one stayed in bounds. Zero dependencies.\n\n> Working name — see [`vision.md`](vision.md). Grounded in the mid-2026 state of multi-agent coding.\n\nA fleet of AI coding agents runs with far too much ambient authority. Each agent can usually write anywhere, run anything, and reach any network endpoint the process can — because the harness, not the task, defines the blast radius (Claude Code, Codex, Cursor, Google Antigravity all inherit the same broad process permissions). The ecosystem standardized how agents *call* tools (MCP) and how they *hand off* tasks (A2A), but not the **authority** an agent carries: what it may do, over what, for how long, and who said so. capgrant is the missing authorization layer: an open format for *scoped intent*, not another sandbox.\n\n```bash\nnpx @avee1234/capgrant grant --issuer alice --subject builder --cap \"fs.write:src/**\" --ttl 30m\nnpx @avee1234/capgrant check fs.write src/auth/login.ts --subject builder   # allowed? exit 0/1\nnpx @avee1234/capgrant list                                                  # who holds what, expiring when\nnpx @avee1234/capgrant delegate --parent <id> --subject reviewer --cap \"fs.write:src/auth/**\"\nnpx @avee1234/capgrant revoke <id> --reason \"task done\"                      # cascades to delegated children\nnpx @avee1234/capgrant audit actions.json                                    # did the fleet stay in scope?\nnpx @avee1234/capgrant hook install                                          # check staged writes before every commit\n```\n\n**Why it's different:** advisory *and* verifiable, not a sandbox — capgrant doesn't intercept syscalls, it's the shared *format* any harness can read and write. The registry is append-only JSONL with content-hash IDs, so it never merge-conflicts with itself even when many agents write at once. Least privilege that composes: grants **delegate** (a delegable grant mints a strictly narrower sub-grant) and revocation **cascades** down the delegation chain, so authority can only narrow as it flows outward and pulling one grant pulls everything derived from it. Harness-neutral: Claude Code, Codex, Cursor, Google Antigravity, or a factory worker.\n\nSame open-format-and-conformance playbook as [worklease](https://github.com/abhid1234/worklease) (coordination) and [provenant](https://github.com/abhid1234/provenant) (provenance) — the authorization standard for the one thing a fleet can't currently express: *what each agent is allowed to do, over what, for how long, and who said so.*\n\n## The grant format\n\nA grant is one JSON object. `id` is the sha256 content hash of the record itself (its own `id` excluded), so a record's identity *is* its content — a tampered line no longer matches its id and is dropped on read. `expires` is `created + ttl_seconds`, so the lease is self-describing.\n\n```json\n{\n  \"id\": \"9f2c1a…\",\n  \"type\": \"grant\",\n  \"issuer\": \"alice\",\n  \"subject\": \"builder\",\n  \"capabilities\": [\n    { \"action\": \"fs.write\", \"resource\": \"src/**\" },\n    { \"action\": \"net.fetch\", \"resource\": \"api.github.com\" }\n  ],\n  \"ttl_seconds\": 1800,\n  \"created\": \"2026-07-11T12:00:00Z\",\n  \"expires\": \"2026-07-11T12:30:00Z\",\n  \"delegable\": true,\n  \"status\": \"active\"\n}\n```\n\n- `issuer` — **who** is granting the authority.\n- `subject` — **which agent** the grant is for (only its own grants ever authorize it).\n- `capabilities` — **what**: one or more `{ action, resource }` leaves, each with an optional `constraints` object (byte cap, per-request budget, method allow-list, path-depth cap — see [Capability constraints](#capability-constraints)).\n- `ttl_seconds` / `created` / `expires` — **for how long** (an ISO-8601-**UTC** lease; offsets and impossible calendar dates are rejected).\n- `delegable` — may the subject mint a strictly narrower sub-grant?\n- `parent` *(delegated grants only)* — the parent grant's id (revocation cascades along it).\n\nA **revocation** withdraws a grant: `{ id, type: \"revocation\", grant_id, issuer, reason, at }`. Revocations are appended, never deletions — the registry stays append-only, and the resolver folds them in (and cascades them) at read time.\n\n## The model\n\nTwo independent axes must both line up for a capability to **cover** a request — this is what makes capgrant an *authorization* format, not a coordination one:\n\n- **Action hierarchy** — a dotted namespace where implication flows **downward only**. `fs` implies `fs.write` and `fs.write.foo`; `fs.write` does **not** imply `fs` (narrower never grants broader); `*` implies anything. A broader grant covers a narrower request, never the reverse.\n- **Resource patterns** — `*` matches anything; a glob-like resource (`src/**`, `src/*`) resolves through a zero-dependency glob-overlap engine (a concrete path is a degenerate glob, so the check is correct even for a file that doesn't exist yet); an opaque token (a host, an id) is matched by exact equality.\n- **TTL** — a grant with `expires ≤ now` is derived to `expired` at read time; the stored log is never rewritten.\n- **Delegation** — a `delegable` grant can mint a sub-grant, subject to three invariants: the parent must be delegable, every delegated capability must be **covered** by some parent capability (no privilege escalation), and the sub-grant must not outlive the parent.\n- **Cascade revocation** — revoking a grant revokes every grant whose parent chain leads back to it, to a fixed point, so one revocation collapses an entire delegation subtree.\n\n### Capability constraints\n\nBeyond the two axes, a capability may carry an optional `constraints` object — extra conditions a covered request must **also** satisfy. They are **advisory and stateless**: capgrant keeps no counters, so a constraint scores the budget a *single request declares for itself* against the cap the grant permits. A request passes its context (`{ bytes, method, calls, rate }`) to `check`; if a covering capability has constraints the request violates, it's **denied with the specific constraint named** (`\"… within scope of grant a1b2c3d4 but violates constraint max_bytes: 5000 > 4096\"`).\n\n```json\n{ \"action\": \"net.fetch\", \"resource\": \"api.github.com\",\n  \"constraints\": { \"methods\": [\"GET\", \"HEAD\"], \"max_bytes\": 1048576, \"rate\": 5 } }\n```\n\n| constraint | request context | semantics |\n| --- | --- | --- |\n| `max_bytes` (number) | `bytes` | the request's declared payload size must be **≤** the cap |\n| `max_calls` (number) | `calls` | a per-request declared call budget must stay **≤** the cap (stateless — the budget one request claims, not a running total) |\n| `rate` (number) | `rate` | a per-request declared requests-per-interval figure must stay **≤** the cap |\n| `methods` (array) | `method` | for `net.*` actions, the request's method must be **in** the allowed set (case-sensitive) |\n| `path_depth` (number) | *(the resource itself)* | the concrete resource's depth — its count of `/`-separated segments — must be **≤** the cap |\n\nTwo rules keep it safe and compatible:\n\n- **Backward-compatible.** A capability with no `constraints` behaves exactly as v0.1, and a `check` with no request context still works — a numeric/method constraint only bites when the request *declares* that dimension (a request that declares nothing is unconstrained on that axis). `path_depth` is the one exception: a request always has a resource, so it always applies. Unknown constraint keys are ignored (forward-compat), never a silent denial.\n- **Delegation can only TIGHTEN.** A delegated capability may narrow a constraint (a smaller `max_bytes`, a subset of `methods`) or add one to a dimension the parent left open — never loosen it. Dropping a cap the parent set (which would mean \"unlimited\") is a privilege escalation and is refused, exactly like widening the action or resource.\n\n## Library API\n\nZero-dependency ESM. `import { … } from \"@avee1234/capgrant\"`. Every core function is pure and clock-injected (`created` / `now` are passed in, no I/O except the registry store), so the whole decision layer is deterministic and unit-testable.\n\n**Schema & validation** — never throw; each returns `{ valid, errors }` collecting *every* violation.\n- `validateGrant(obj)` / `validateCapability(obj)` / `validateRevocation(obj)` / `validateRegistry(arr)`\n- `validateApprovalRequest(obj)` / `validateDecision(obj)` — the HITL record validators\n- `isDottedAction(s)`, `isIso8601Utc(s)` — the two format primitives\n- `GRANT_FIELDS`, `CAPABILITY_FIELDS`, `STATUSES`, `RECORD_TYPES`, `ERROR_CODES`\n\n**Capability engine** — pure, total matching over strings.\n- `actionImplies(granted, requested)` — the downward-only action hierarchy\n- `resourceMatches(pattern, resource)` — `*` / glob-overlap / exact-token matching\n- `capabilityCovers(cap, action, resource, requestContext?)` — both axes hold **and** constraints are satisfied\n- `capabilityCoverage(cap, action, resource, requestContext?)` → `{ axes, violation }` — the richer atom `check` uses to report *why*\n- `constraintViolation(constraints, ctx)` → `null` or the first violated constraint (`\"max_bytes: 5000 > 4096\"`)\n- `constraintsSubsume(parent, child)` — is the child constraint set no looser than the parent's? (the delegation tighten-only rule)\n- `resourceDepth(resource)`, `CONSTRAINT_KEYS`\n- `globsOverlap(a, b)` — the zero-dep glob satisfiability engine\n\n**Record constructors** — throw on bad input rather than emit a malformed (or escalating) grant.\n- `makeGrant(capabilities, { issuer, subject, ttl_seconds, created, delegable, parent })`\n- `delegate(parentGrant, capabilities, { issuer, subject, ttl_seconds, created })` — enforces the delegation invariants\n- `revoke(grantId, { issuer, reason, at })`\n- `requestApproval(action, resource, { subject, reason, requested_by, created })` — raise an `approval_request` (HITL)\n- `decide(request, { approver, decision, at, reason?, grant_ttl_seconds? })` — a `decision`; an `approve` mints the just-in-time grant on fold\n- `parseTtl(input)` — `30s` / `20m` / `2h` / `1d` / bare seconds → integer seconds, or `null`\n\n**Registry store** — the append-only JSONL layer.\n- `loadRegistry(path, { now, expire })` → `{ grants, requests, notes }` (missing file → empty, no throw)\n- `appendRecord(path, record)` — append exactly one line (`O_APPEND`); existing lines are never rewritten\n- `resolveRecords(records, { now, expire })` → `{ grants, requests, notes }` — fold a raw log into the current grant set **and** approval-request set (integrity filter, revocations, cascade, TTL, plus HITL: request status + any just-in-time grant an approval minted)\n- `defaultRegistryPath(cwd)` — `CAPGRANT_REGISTRY`, else `.capgrant/registry.jsonl`\n- `canonicalize`, `computeRecordId`, `listActive`, `formatRelative`, `shortId`\n\n**Decide & audit** — pure, over a resolved grant array.\n- `check(action, resource, grants, { subject, now, bytes?, method?, calls?, rate?, request?, requests? })` → `{ allowed, needs_approval, matched_grant, reason }` (request context scores a constrained grant; specific denial reasons, including the violated constraint; pass the resolved `requests` and a soft deny sets `needs_approval: true` when a matching PENDING request exists — `allowed` is unchanged and additive)\n- `audit(actions, grants, { now })` → `{ score, total, allowed, violations }` (each action may carry `bytes` / `method` / `calls` / `rate`)\n\n**Signed grants** — layered, optional tamper-evidence across trust domains (`node:crypto`, zero-dep). Signers throw on a bad key; verifiers never throw.\n- `signHmac(record, secret)` / `verifyHmac(record, secret, sigHex?)` — shared-secret HMAC-SHA256 (detached or embedded `signature`)\n- `generateKeypair()` → `{ publicKey, privateKey }` PEM\n- `signAsym(record, privateKeyPem)` / `verifyAsym(record, publicKeyPem, sigHex?)` — ed25519 (deterministic, RFC 8032)\n\n**Git adapter** *(the dogfood surface)*\n- `stagedPaths`, `checkStagedWrites(paths, { registry, subject, now })`, `installHook`, `hookPath`, `renderHookBlock`\n\n## CLI\n\n```bash\ncapgrant grant --issuer <id> --subject <id> --cap <action:resource> [...] [--ttl <dur>] [--delegable] [--registry <path>] [--json]\ncapgrant check <action> <resource> --subject <id> [--registry <path>] [--json]\ncapgrant list [--all] [--subject <id>] [--registry <path>] [--json]\ncapgrant delegate --parent <id> --issuer <id> --subject <id> --cap <action:resource> [...] --ttl <dur> [--registry <path>] [--json]\ncapgrant revoke <id> --issuer <id> --reason \"<why>\" [--registry <path>] [--json]\ncapgrant audit <actions.json> [--registry <path>] [--json]\ncapgrant request <action> <resource> --subject <id> --reason \"<why>\" [--requested-by <id>] [--registry <path>] [--json]\ncapgrant approve <request-id> --approver <id> --ttl <dur> [--registry <path>] [--json]\ncapgrant deny <request-id> --approver <id> [--reason \"<why>\"] [--registry <path>] [--json]\ncapgrant pending [--subject <id>] [--registry <path>] [--json]\ncapgrant hook install|run [--registry <path>] [--json]\ncapgrant validate <file> [--json]\n```\n\n- **`grant`** — issue a scoped, expiring grant: the subject may perform each `<action>` on each `<resource>` until it expires. Repeat `--cap` for multiple capabilities. The record is validated before it's written. Exit `0` on write.\n- **`check <action> <resource>`** — the pre-action gate: does a live grant for `--subject` authorize this? Exit `0` = allowed, `1` = denied (with a specific reason — empty registry, no grant for the subject, out of scope, expired, or revoked).\n- **`list`** — active grants (who holds what, expiring when). `--all` also shows revoked/expired grants labeled with their effective status; `--subject` filters to one holder.\n- **`delegate`** — mint a narrower sub-grant from a delegable parent (full id or unambiguous prefix). Every capability must fall inside the parent's authority and it cannot outlive the parent. Exit `1` on any escalation.\n- **`revoke <id> --reason`** — withdraw a grant by appending a revocation; **cascades** to any grant delegated from it. A no-op with a note if already revoked (still exit `0`).\n- **`audit <actions.json>`** — replay a JSON array of `{ action, resource, subject, at? }` and score how many stayed in scope (each `at` is that action's `now`, so a grant that was live *then* counts even if it has since expired). Exit `0` = all in scope, `1` = at least one violation.\n- **`request <action> <resource>`** — raise a **human-in-the-loop approval request** for an action no live grant covers. Its status starts `pending`; `--requested-by` defaults to the subject. Exit `0` on write.\n- **`approve <request-id> --approver --ttl <dur>`** — approve a pending request (full id or unambiguous prefix), minting a **just-in-time grant** that expires `--ttl` after the decision. Exit `0` on write.\n- **`deny <request-id> --approver [--reason]`** — deny a pending request; nothing is minted. Exit `0` on write.\n- **`pending`** — list approval requests still awaiting a decision (`--subject` filters to one holder).\n- **`validate <file>`** — validate a grant, revocation, approval_request, decision, or registry JSON file (auto-detected). Exit `1` if invalid.\n\nCommon flags: `--issuer <id>` (or `CAPGRANT_ISSUER`), `--subject <id>` (or `CAPGRANT_AGENT`), `--registry <path>` (or `CAPGRANT_REGISTRY`, default `.capgrant/registry.jsonl`), `--json` for machine-readable output.\n\n## The git-hook dogfood\n\n`capgrant hook install` writes a **git pre-commit hook** that treats a commit as a batch of `fs.write` actions — one per staged path — and `check`s each against the committing agent's grants. A path the agent has no `fs.write` grant for is an out-of-scope write.\n\n```bash\ncapgrant hook install            # advisory: warns on out-of-scope writes, never blocks a commit\nCAPGRANT_ENFORCE=1 git commit …  # enforce mode: an out-of-scope write aborts the commit (exit 1)\n```\n\nThe hook is **advisory by default** (vision principle: coordinate, don't enforce) — it prints the out-of-scope writes and lets the commit through. Set `CAPGRANT_ENFORCE=1` in the commit environment to make an out-of-scope write fatal, and `CAPGRANT_AGENT` to the committing agent's id so its own grants count. Install is **idempotent** and **preserves an existing hook** — it manages only a marked block (`# >>> capgrant >>>` … `# <<< capgrant <<<`), so re-running it never duplicates the block or clobbers hand-written hook logic.\n\n## The registry\n\nThe store is an **append-only JSONL file** (default `.capgrant/registry.jsonl`), meant to be **committed** so grants travel with the repo across worktrees and harnesses. New records are appended as whole lines; existing lines are never rewritten. Every record's `id` is a content hash of its own content, so a duplicated append is idempotent on read and two agents appending at once union-merge cleanly instead of conflicting. A line that fails its integrity check (its `id` no longer matches its content — i.e. it was tampered) or won't parse is skipped with a note surfaced to stderr — one bad line never discards the rest of the registry.\n\n## Human-in-the-loop approval\n\nA grant answers *\"what is this agent **pre**-authorized to do.\"* But real fleets hit actions no live grant covers — and a flat `DENY` there is exactly the \"agent permission fatigue\" that stalls autonomy. capgrant closes the loop: instead of denying, the agent raises an **approval request** to a human, and the human's **decision** to approve **mints a scoped, expiring grant for exactly that action** — the *just-in-time* grant. Authority is now either granted ahead of time **or** requested → approved just in time, over the same append-only log, with the same content-hash ids and pure, clock-injected fold.\n\nTwo new record types join the registry:\n\n- **`approval_request`** — `{ subject, action, resource, reason, requested_by, created, status:'pending' }`. Some agent wants to perform `action` on `resource` and needs a human to say yes.\n- **`decision`** — `{ request_id, decision:'approve'|'deny', approver, at, reason?, grant_ttl_seconds? }`. An `approve` mints a grant for the request's subject/action/resource that expires `grant_ttl_seconds` after the decision's `at`, **parented to the request** so its provenance is the approval.\n\n`resolveRecords` folds both: a request's status derives to `approved` / `denied` / `pending` from its latest decision, and an approved request **yields a live grant that participates in `check` / `audit` exactly like any grant** (it's minted at read time, never appended — the log stays append-only). `check` gained an additive `needs_approval` flag: when no grant covers the request but a matching **pending** request exists, the deny is *soft* (`needs_approval: true`) rather than flat — a signal a human can turn into a grant. `allowed` is unchanged, so the whole feature is backward-compatible.\n\n```js\nimport { requestApproval, decide, resolveRecords, check } from \"@avee1234/capgrant\";\n\n// 1. No live grant covers this — the agent asks instead of taking a hard deny.\nconst req = requestApproval(\"fs.write\", \"src/auth/login.ts\", {\n  subject: \"agent-42\",\n  reason: \"hotfix the login redirect\",\n  requested_by: \"agent-42\",\n  created: \"2026-07-14T10:00:00.000Z\",\n});\n\n// check sees the pending request → soft deny, not a flat deny.\nconst t1 = Date.parse(\"2026-07-14T10:01:00Z\");\nlet { grants, requests } = resolveRecords([req], { now: t1 });\ncheck(\"fs.write\", \"src/auth/login.ts\", grants, { subject: \"agent-42\", now: t1, requests });\n// → { allowed: false, needs_approval: true, reason: \"no grant — request approval …\" }\n\n// 2. A human approves with a 10-minute TTL → a just-in-time grant is minted on fold.\nconst dec = decide(req, {\n  approver: \"abhi\",\n  decision: \"approve\",\n  at: \"2026-07-14T10:02:00.000Z\",\n  grant_ttl_seconds: 600,\n});\n\nconst t2 = Date.parse(\"2026-07-14T10:03:00Z\");   // inside the 10-min TTL\n({ grants } = resolveRecords([req, dec], { now: t2 }));\ncheck(\"fs.write\", \"src/auth/login.ts\", grants, { subject: \"agent-42\", now: t2 });\n// → { allowed: true, needs_approval: false, matched_grant: <minted grant>, … }\n\n// …and after the 10-minute TTL that minted grant expires like any TTL grant.\nconst t3 = Date.parse(\"2026-07-14T10:20:00Z\");\n({ grants } = resolveRecords([req, dec], { now: t3 }));\ncheck(\"fs.write\", \"src/auth/login.ts\", grants, { subject: \"agent-42\", now: t3 });\n// → { allowed: false, needs_approval: false, reason: \"denied: … have expired\" }\n```\n\nOn the CLI this is `request` → `approve` / `deny`, with `pending` to list what's waiting:\n\n```bash\ncapgrant request fs.write src/auth/login.ts --subject agent-42 --reason \"hotfix the login redirect\"\ncapgrant pending                                   # what's awaiting a human\ncapgrant approve <request-id> --approver abhi --ttl 10m   # mints the just-in-time grant\ncapgrant deny <request-id> --approver abhi --reason \"not this file\"\n```\n\nThe approval flow is harness-neutral: Claude Code, Codex, Cursor, or Google Antigravity can all raise a request an operator approves — the request and the minted grant are just records in the shared registry.\n\n## Signed grants\n\nThe content-hash `id` makes a record self-verifying **within** one trust domain — a tampered line no longer matches its id and is dropped on read. But an id is *integrity*, not *authorship*: anyone can recompute it. When a grant or revocation crosses a trust boundary — one team's registry consumed by another, a grant minted by a service you don't share a filesystem with — you want to prove **who** stood behind it. `src/sign.js` adds two optional, layered schemes over the same canonical, id-excluded pre-image the hash is built from (with any `signature` field stripped first, so a signature is never part of its own pre-image and an **embedded** signature verifies identically to a **detached** one).\n\n```js\nimport { signHmac, verifyHmac, generateKeypair, signAsym, verifyAsym } from \"@avee1234/capgrant\";\n\n// HMAC — shared secret, cheapest.\nconst sig = signHmac(grant, process.env.CAPGRANT_SECRET);       // hex, deterministic\nverifyHmac(grant, process.env.CAPGRANT_SECRET, sig);            // → true (detached)\nverifyHmac({ ...grant, signature: sig }, process.env.CAPGRANT_SECRET); // → true (embedded)\n\n// ed25519 — asymmetric, no shared secret; the holder of the private key signs.\nconst { publicKey, privateKey } = generateKeypair();            // PEM\nconst asig = signAsym(grant, privateKey);                       // hex, deterministic (RFC 8032)\nverifyAsym(grant, publicKey, asig);                             // → true\n```\n\n- **HMAC** (`signHmac` / `verifyHmac`) — an HMAC-SHA256 both parties can compute from a shared secret.\n- **ed25519** (`generateKeypair` / `signAsym` / `verifyAsym`) — an asymmetric signature: the signer holds the private key, anyone with the public key verifies, no shared secret. Zero-config (ed25519 needs no parameters) and deterministic.\n- **Signers throw** on a bad key or record (like the record constructors — you should never emit a broken signature). **Verifiers never throw** — a missing, malformed, or wrong-typed signature or key is simply an *unverified* record (`false`), never an exception, so a verification loop can't be crashed by hostile input.\n\nBoth layers are advisory and composable — sign with either, both, or neither; the registry, `check`, and `audit` are unchanged by a `signature` field (it's excluded from the pre-image, so it never perturbs the id or a decision). It's harness-neutral tamper-evidence, not a new gate.\n\n## Install\n\n```bash\nnpm install @avee1234/capgrant      # library\nnpx @avee1234/capgrant grant …      # CLI, no install\n```\n\nRequires Node ≥ 18. Run the test suite with `node --test`.\n\nStatus: **v0.3** — human-in-the-loop approval (request → approve → just-in-time grant), on top of capability constraints + signed grants (HMAC / ed25519); see [`roadmap.md`](roadmap.md). MIT · zero dependencies · harness-neutral.\n","readmeFilename":"README.md"}