@grantor/mcp
Advanced tools
+1
-1
| { | ||
| "name": "@grantor/mcp", | ||
| "mcpName": "com.chaingrantor/grantor-mcp", | ||
| "version": "0.1.4", | ||
| "version": "0.1.5", | ||
| "description": "Grantor permission broker for multi-agent frameworks \u2014 grant, delegate, check, revoke bounded capabilities over MCP. No authorization server anywhere.", | ||
@@ -6,0 +6,0 @@ "license": "SEE LICENSE IN LICENSE", |
+11
-0
@@ -22,2 +22,3 @@ // chain.js — the on-chain seam: viem clients + the minimal inline ABI | ||
| "function delegationEpoch(uint256 id, bytes32 label) view returns (uint64)", | ||
| "function tenants(uint256 id) view returns (uint8 tier, uint64 periodEnd, uint256 balance, uint32 appCount, uint32 agentCount, uint32 userCount)", | ||
| "event TenantCreated(uint256 indexed id, address indexed owner, uint8 tier)", | ||
@@ -101,2 +102,12 @@ ]); | ||
| async tenantBalance(tenant) { | ||
| const [, , balance] = await publicClient.readContract({ | ||
| address: cfg.registry, | ||
| abi: REGISTRY_ABI, | ||
| functionName: "tenants", | ||
| args: [BigInt(tenant)], | ||
| }); | ||
| return balance; | ||
| }, | ||
| async isAgentKey(tenant, commitmentHex) { | ||
@@ -103,0 +114,0 @@ return publicClient.readContract({ |
+1
-0
@@ -99,2 +99,3 @@ #!/usr/bin/env node | ||
| tier: flags.tier !== undefined ? Number(flags.tier) : undefined, | ||
| tenant: flags.tenant !== undefined ? Number(flags.tenant) : undefined, | ||
| emitRpc: flags["emit-rpc"], | ||
@@ -101,0 +102,0 @@ out: flags.out, |
+104
-28
@@ -89,2 +89,32 @@ // setup.js — the `setup-sandbox` verb: create+fund+enroll+pre-bump a real | ||
| /** | ||
| * Poll a chain read until `isOk(value)` holds, instead of aborting on the | ||
| * first stale answer. Exists because load-balanced public RPCs | ||
| * (mainnet.base.org) route consecutive reads to different replicas: the | ||
| * 2026-08-21 sandbox provisioning FALSE-ABORTED at step 5/8 when the | ||
| * post-`drawPeriod` `status()` read hit a replica that hadn't seen the tx — | ||
| * the money was fine, the assert wasn't. Still fails closed after | ||
| * `attempts` reads; the error names `--tenant` resume so an operator who | ||
| * DOES hit the exhausted case knows the recovery path. | ||
| */ | ||
| export async function retryReadAssert(label, read, isOk, { attempts = 5, delayMs = 3000, sleep } = {}) { | ||
| const wait = sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms))); | ||
| let value; | ||
| for (let i = 1; i <= attempts; i++) { | ||
| value = await read(); | ||
| if (isOk(value)) return value; | ||
| if (i < attempts) { | ||
| console.error( | ||
| `grantor-mcp setup-sandbox: ${label} not confirmed yet (read ${i}/${attempts} saw ${value}) — retrying in ${delayMs}ms (replica lag?)`, | ||
| ); | ||
| await wait(delayMs); | ||
| } | ||
| } | ||
| throw new Error( | ||
| `grantor-mcp setup-sandbox: ${label} still unconfirmed after ${attempts} reads (last saw ${value}) — ` + | ||
| "if the RPC is load-balanced this can be replica lag on a transaction that DID land; " + | ||
| "re-run with --tenant <id> to resume without re-paying", | ||
| ); | ||
| } | ||
| /** | ||
| * Create+fund+enroll+pre-bump one real on-chain tenant and write a | ||
@@ -100,2 +130,8 @@ * Task-1-shaped `sandbox-config.json`. Runs against ANY RPC — local devnet | ||
| * @param {number} [args.tier] tier to create the tenant at (default 1). | ||
| * @param {number} [args.tenant] EXISTING tenant id to resume an interrupted | ||
| * run against: skips `createTenant`, skips funding entirely if the tenant | ||
| * is already Active, and otherwise tops up only the shortfall between the | ||
| * target (`tierFee * periods`) and the balance already on the tenant. The | ||
| * recovery path for the replica-lag false-abort class (see | ||
| * `retryReadAssert`) — never pays twice for work that already landed. | ||
| * @param {string} [args.emitRpc] RPC url written into the output config in | ||
@@ -107,5 +143,8 @@ * place of the chain config's own `rpcUrl` — lets a mainnet run transact | ||
| * @param {Record<string,string>} [args.env] defaults to `process.env`. | ||
| * @param {(cfg: object, key: string) => object} [args.makeChainImpl] chain-seam | ||
| * factory, defaults to `makeChain` — injection point for the unit tests' | ||
| * fake chain (the live path is proven by `e2e.sh`). | ||
| * @returns {Promise<object>} the written sandbox config. | ||
| */ | ||
| export async function setupSandbox({ chainConfigPath, periods = 12, tier = 1, emitRpc, out, env = process.env }) { | ||
| export async function setupSandbox({ chainConfigPath, periods = 12, tier = 1, tenant, emitRpc, out, env = process.env, makeChainImpl }) { | ||
| // Cheap, chain-independent guards first — no file read, no network, no | ||
@@ -117,2 +156,8 @@ // key required to fail fast on a bad invocation. | ||
| } | ||
| if (tenant !== undefined) { | ||
| const tenantNum = Number(tenant); | ||
| if (!Number.isInteger(tenantNum) || tenantNum < 1) { | ||
| throw new Error(`grantor-mcp setup-sandbox: --tenant must be a positive integer (an existing tenant id to resume), got ${tenant}`); | ||
| } | ||
| } | ||
| if (!chainConfigPath) { | ||
@@ -132,28 +177,57 @@ throw new Error("grantor-mcp setup-sandbox: --chain-config <path> is required"); | ||
| const chain = makeChain(config, key); | ||
| const chain = (makeChainImpl ?? makeChain)(config, key); | ||
| console.error(`grantor-mcp setup-sandbox: 2/8 creating tenant (owner=${owner}, tier=${tier})`); | ||
| const tenantId = await chain.createTenant(owner, tier); | ||
| console.error(`grantor-mcp setup-sandbox: tenant ${tenantId} created`); | ||
| console.error(`grantor-mcp setup-sandbox: 3/8 reading tier ${tier} fee`); | ||
| const fee = await chain.tierFee(tier); | ||
| if (fee > 0n) { | ||
| const amount = fee * BigInt(periodsNum); | ||
| let tenantId; | ||
| let alreadyActive = false; | ||
| if (tenant !== undefined) { | ||
| tenantId = BigInt(Number(tenant)); | ||
| // Single probe, not retried: a stale not-Active answer here cannot cost | ||
| // money — the shortfall topUp below only ever prepays the same tenant, | ||
| // and a redundant drawPeriod reverts on the contract's own | ||
| // "period active" guard, so the worst case is one reverted tx and a | ||
| // clean re-run that then sees Active. | ||
| const probed = await chain.status(tenantId); | ||
| alreadyActive = probed === 1; | ||
| console.error( | ||
| `grantor-mcp setup-sandbox: 4/8 approving + topping up ${amount} (fee=${fee} x periods=${periodsNum}) — ` + | ||
| "the caller must already hold this much USDC; setup-sandbox never mints", | ||
| `grantor-mcp setup-sandbox: 2/8 resuming with existing tenant ${tenantId} (status=${probed}) — createTenant skipped`, | ||
| ); | ||
| await chain.approveAndTopUp(config.usdc, tenantId, amount); | ||
| } else { | ||
| console.error("grantor-mcp setup-sandbox: 4/8 tier fee is 0 — skipping approve+topUp"); | ||
| console.error(`grantor-mcp setup-sandbox: 2/8 creating tenant (owner=${owner}, tier=${tier})`); | ||
| tenantId = await chain.createTenant(owner, tier); | ||
| console.error(`grantor-mcp setup-sandbox: tenant ${tenantId} created`); | ||
| } | ||
| console.error(`grantor-mcp setup-sandbox: 5/8 drawing first period for tenant ${tenantId}`); | ||
| await chain.drawPeriod(tenantId); | ||
| const status = await chain.status(tenantId); | ||
| if (status !== 1) { | ||
| throw new Error(`grantor-mcp setup-sandbox: tenant ${tenantId} status is ${status} after drawPeriod, expected 1 (Active)`); | ||
| if (alreadyActive) { | ||
| console.error("grantor-mcp setup-sandbox: 3/8-5/8 tenant is already Active — skipping funding (no topUp, no drawPeriod)"); | ||
| } else { | ||
| console.error(`grantor-mcp setup-sandbox: 3/8 reading tier ${tier} fee`); | ||
| const fee = await chain.tierFee(tier); | ||
| if (fee > 0n) { | ||
| const target = fee * BigInt(periodsNum); | ||
| // On resume, credit whatever an interrupted run already deposited — | ||
| // top up only the shortfall, never the full target again. | ||
| const balance = tenant !== undefined ? await chain.tenantBalance(tenantId) : 0n; | ||
| const amount = target > balance ? target - balance : 0n; | ||
| if (amount > 0n) { | ||
| console.error( | ||
| `grantor-mcp setup-sandbox: 4/8 approving + topping up ${amount} (fee=${fee} x periods=${periodsNum}${balance > 0n ? `, minus ${balance} already on the tenant` : ""}) — ` + | ||
| "the caller must already hold this much USDC; setup-sandbox never mints", | ||
| ); | ||
| await chain.approveAndTopUp(config.usdc, tenantId, amount); | ||
| } else { | ||
| console.error(`grantor-mcp setup-sandbox: 4/8 tenant balance ${balance} already covers the ${target} target — skipping approve+topUp`); | ||
| } | ||
| } else { | ||
| console.error("grantor-mcp setup-sandbox: 4/8 tier fee is 0 — skipping approve+topUp"); | ||
| } | ||
| console.error(`grantor-mcp setup-sandbox: 5/8 drawing first period for tenant ${tenantId}`); | ||
| await chain.drawPeriod(tenantId); | ||
| await retryReadAssert( | ||
| `tenant ${tenantId} status after drawPeriod (expected 1/Active)`, | ||
| () => chain.status(tenantId), | ||
| (s) => s === 1, | ||
| ); | ||
| console.error(`grantor-mcp setup-sandbox: tenant ${tenantId} is Active`); | ||
| } | ||
| console.error(`grantor-mcp setup-sandbox: tenant ${tenantId} is Active`); | ||
@@ -165,6 +239,7 @@ const principalKey = generatePrivateKey(); | ||
| await chain.registerAgentKey(tenantId, commitment); | ||
| const isKey = await chain.isAgentKey(tenantId, commitment); | ||
| if (!isKey) { | ||
| throw new Error(`grantor-mcp setup-sandbox: registerAgentKey landed but isAgentKey is still false for ${principalAddress}`); | ||
| } | ||
| await retryReadAssert( | ||
| `isAgentKey for ${principalAddress} after registerAgentKey`, | ||
| () => chain.isAgentKey(tenantId, commitment), | ||
| (isKey) => isKey === true, | ||
| ); | ||
| console.error("grantor-mcp setup-sandbox: agent key registered"); | ||
@@ -175,6 +250,7 @@ | ||
| await chain.bumpEpoch(tenantId, demoRevokedLabel); | ||
| const epoch = await chain.delegationEpoch(tenantId, demoRevokedLabel); | ||
| if (epoch !== 1) { | ||
| throw new Error(`grantor-mcp setup-sandbox: delegationEpoch after bumpEpoch is ${epoch}, expected 1`); | ||
| } | ||
| const epoch = await retryReadAssert( | ||
| `delegationEpoch for the demo-revoked label after bumpEpoch (expected 1)`, | ||
| () => chain.delegationEpoch(tenantId, demoRevokedLabel), | ||
| (e) => e === 1, | ||
| ); | ||
| console.error(`grantor-mcp setup-sandbox: epoch bumped to ${epoch}`); | ||
@@ -181,0 +257,0 @@ |
Unidentified License
LicenseSomething that seems like a license was found, but its contents could not be matched with a known license.
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
Unidentified License
LicenseSomething that seems like a license was found, but its contents could not be matched with a known license.
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
81104
5.52%1026
9.15%