@grantor/mcp
Advanced tools
+1
-1
| { | ||
| "name": "@grantor/mcp", | ||
| "mcpName": "com.chaingrantor/grantor-mcp", | ||
| "version": "0.1.7", | ||
| "version": "0.1.8", | ||
| "description": "Grantor permission broker for multi-agent frameworks \u2014 grant, delegate, check, revoke bounded capabilities over MCP, and wrap any stdio MCP server with enforced permissions. No authorization server anywhere.", | ||
@@ -6,0 +6,0 @@ "license": "SEE LICENSE IN LICENSE", |
+13
-4
@@ -175,6 +175,15 @@ # @grantor/mcp | ||
| is. | ||
| - **Metering is broker-local.** `max_uses`/`remaining_uses` live in this | ||
| process's state file (`~/.grantor-mcp/state.json` by default), not | ||
| on-chain. Two brokers, or two runs against the same config without a | ||
| shared state file, do not share a budget. | ||
| - **Metering is broker-local — and, within one broker, atomic and | ||
| conserving.** `max_uses`/`remaining_uses` live in this process's state | ||
| file (`~/.grantor-mcp/state.json` by default), not on-chain. Inside one | ||
| broker process the meter holds two invariants: `check` reserves the use | ||
| in the same synchronous step as the balance gate (two concurrent | ||
| last-use calls can never both pass; a denied call refunds; a crash | ||
| between authorization and the tool's side effect loses a use — it can | ||
| never double-spend one), and `delegate` TRANSFERS uses from the parent's | ||
| live pool (a tree of delegations can never hold more aggregate uses than | ||
| the root grant; a drained parent refuses to delegate). Across processes | ||
| the meter is NOT shared or atomic: run one broker per state file. | ||
| `to`-mode links (external holders) carry no broker meter at all — their | ||
| bound is the signed per-path `max_uses` caveat alone. | ||
@@ -181,0 +190,0 @@ None of this is a limitation you have to accept — it's what changes the |
+17
-4
@@ -29,2 +29,14 @@ // check.js — verify a capability deed (broker-held child OR a presented | ||
| if (child.usesRemaining <= 0) return deny("UsesExhausted", "this grant's local use budget is spent"); | ||
| // ATOMIC CONSUMPTION: reserve the use HERE, synchronously, before any | ||
| // await. The verify below awaits a chain read — gating on the balance | ||
| // and decrementing only after that await would let two concurrent | ||
| // last-use checks both observe remaining=1 and both allow. Reserving in | ||
| // the same synchronous block as the gate makes the pair atomic under | ||
| // Node's single thread; a failed verify refunds below, so a denied call | ||
| // never consumes. Crash window: a crash between this reserve and the | ||
| // tool's side effect LOSES a use (fail-safe) — it can never double-spend | ||
| // one. Cross-PROCESS metering is not atomic: one broker process per | ||
| // state file (see README). | ||
| child.usesRemaining -= 1; | ||
| saveState(statePath, state); | ||
| chal = randomBytes(16).toString("hex"); // self-issued: broker is holder AND verifier (documented) | ||
@@ -46,8 +58,9 @@ const base = mintUserDeed(child.userSecretKeyHex, cfg.tenant, cfg.audience, cfg.origin, chal, now + 300); | ||
| } catch (e) { | ||
| if (child) { | ||
| // Refund the reserved use — a denied call must not consume budget. | ||
| child.usesRemaining += 1; | ||
| saveState(statePath, state); | ||
| } | ||
| return deny(e.name || "Error", e.message); | ||
| } | ||
| if (child) { | ||
| child.usesRemaining -= 1; | ||
| saveState(statePath, state); | ||
| } | ||
| const out = { allow: true, sub: resolved.claims.sub, remaining_uses: child ? child.usesRemaining : null }; | ||
@@ -54,0 +67,0 @@ if (note) out.grantor_note = note; |
@@ -33,3 +33,17 @@ // delegate.js — a HELD child re-delegates a narrower slice of its own grant | ||
| const parentMaxUses = Math.min(...parent.grants.map((g) => g.cav?.max_uses ?? Infinity)); | ||
| const maxUses = max_uses === undefined ? parentMaxUses : Math.min(parentMaxUses, max_uses); | ||
| const requested = max_uses === undefined ? parentMaxUses : Math.min(parentMaxUses, max_uses); | ||
| // CONSERVATION: a broker-held delegation TRANSFERS uses from the parent's | ||
| // live pool rather than minting a fresh meter — otherwise N delegations of | ||
| // a 50-use grant put N×50 uses in play and the tree manufactures | ||
| // authority. The child's signed caveat AND its local meter both get the | ||
| // transferred amount (clamped to what the parent actually has left); | ||
| // narrowing vs the parent's own caveat is still checked below. `to`-mode | ||
| // links carry no broker meter (the holder is external) — their bound is | ||
| // the signed caveat alone, per-path, documented in the README. | ||
| const maxUses = to === undefined ? Math.min(requested, parent.usesRemaining) : requested; | ||
| if (to === undefined && maxUses <= 0) { | ||
| throw new Error( | ||
| "delegate: parent has no uses remaining to transfer — the pool is conserved; delegation cannot mint uses", | ||
| ); | ||
| } | ||
| const toolNames = tools ?? toolNamesFromGrants(parent.grants, DEFAULT_NS); | ||
@@ -68,2 +82,5 @@ const grants = buildToolGrants({ tools: toolNames, ns: DEFAULT_NS, exp, maxUses }); | ||
| child.chain = [...parent.chain.slice(0, -1), newLink, await selfHopLink(child, cfg.tenant, newLink)]; | ||
| // The transfer half of conservation: what the child received, the parent | ||
| // no longer has. Saved atomically with the child's creation (one write). | ||
| parent.usesRemaining -= maxUses; | ||
| state.children[child.id] = child; | ||
@@ -70,0 +87,0 @@ saveState(statePath, state); |
@@ -13,3 +13,7 @@ // tools.js — inspect an MCP server and SUGGEST a wrap command. This is the | ||
| // (a `delete_read_marker` is write-ish). | ||
| const WRITEY = /(write|creat|delet|remov|updat|move|renam|exec|run|shell|command|spawn|send|post|put|patch|insert|drop|kill|install|deploy|push|publish|set_|^set$|upload|edit|append|mkdir|rm_|^rm$|clear|reset|revoke|approve|transfer|sign)/i; | ||
| // Tuned against real servers: dogfooding vs @modelcontextprotocol/ | ||
| // server-github (2026-08-25) showed `merge_pull_request`, `fork_repository` | ||
| // and `add_issue_comment` slipping through as "read-like" — merge/fork/add_ | ||
| // are mutations. (`comment` alone stays unmatched: get_*_comments is a read.) | ||
| const WRITEY = /(write|creat|delet|remov|updat|move|renam|exec|run|shell|command|spawn|send|post|put|patch|insert|drop|kill|install|deploy|push|publish|set_|^set$|upload|edit|append|mkdir|rm_|^rm$|clear|reset|revoke|approve|transfer|sign|merge|fork|add_|^add$|submit|close|dismiss|apply)/i; | ||
@@ -16,0 +20,0 @@ export function splitByRisk(names) { |
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
AI-detected potential code anomaly
Supply chain riskAI has identified unusual behaviors that may pose a security risk.
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
116268
2.59%1691
2.11%289
3.21%11
-8.33%