import { z } from "zod"; import { forbidden } from "../../errors"; import { limitWrite } from "../../limits"; import { hasPermission } from "../../repos"; import { assertAreasWithinGrant, credentialCoversRepo, scopedRepoPermission, TOKEN_AREAS } from "../../token-scope"; import type { TokenArea, TokenPermissions } from "../../token-scope"; import type { Permission } from "~/lib/types"; import { mintFineGrainedToken, tokenLineage } from "../grants"; import { defineTool, repoArgs, scopedRepo, ToolError } from "./base"; const MAX_TOKEN_DAYS = 7; const DAY_MS = 24 * 60 * 60 * 1000; const CONFIRM_KEY = "confirm_token"; function needed(area: TokenArea, access: "read" | "write"): Permission { if (area === "admin") return access === "write" ? "admin" : "read"; return access; } const createTokenTool = defineTool({ name: "create_repo_token", title: "Create scoped repository token", description: `Mint a fine-grained token for one repository (e.g. for a sub-agent), valid up to ${MAX_TOKEN_DAYS} days but never longer than your own token, and revoked with it. It can never exceed this grant's own access and cannot delegate further. Clients that support elicitation are asked to confirm (advisory: the confirmation comes from the client).`, input: z.object({ ...repoArgs, name: z.string().trim().min(1).max(80), permissions: z.object(Object.fromEntries(TOKEN_AREAS.map((area) => [area, z.enum(["read", "write"]).optional()]))), expires_in_days: z.number().int().min(1).max(MAX_TOKEN_DAYS).default(1), }), write: true, async run({ caller, inputResponses }, args) { await limitWrite(caller.viewer); const lineage = caller.tokenId ? await tokenLineage(caller.tokenId) : null; if (!caller.tokenId || !lineage) throw new ToolError("A token is required to delegate", true); if (lineage.parent_token_id) throw new ToolError("Delegated tokens cannot delegate further", true); const { repo } = await scopedRepo(caller, args.owner, args.repo, "contents", "read"); const requested: TokenPermissions = {}; for (const area of TOKEN_AREAS) { const access = args.permissions[area]; if (access === "read" || access === "write") requested[area] = access; } assertAreasWithinGrant(caller.credential.grant, requested); if (!(await credentialCoversRepo(caller.credential, repo.id))) { throw forbidden("A token cannot grant repositories outside its own scope"); } const permissions: TokenPermissions = {}; for (const area of TOKEN_AREAS) { const access = requested[area]; if (!access) continue; if (!hasPermission(await scopedRepoPermission(repo, caller.credential, area), needed(area, access))) { throw forbidden(`This grant cannot delegate ${area}:${access}`); } permissions[area] = access; } if (Object.keys(permissions).length === 0) throw new ToolError("Request at least one permission"); if (caller.clientCapabilities.elicitation && caller.era === "modern") { const answer = inputResponses?.[CONFIRM_KEY]; if (!answer) { return { inputRequired: { [CONFIRM_KEY]: { method: "elicitation/create", params: { mode: "form", message: `Create token "${args.name}" for ${repo.owner_name}/${repo.name} with ${Object.entries( permissions, ) .map(([area, access]) => `${area}:${access}`) .join(", ")} for ${args.expires_in_days} day(s)?`, requestedSchema: { type: "object", properties: { approve: { type: "boolean", title: "Approve" } }, required: ["approve"], }, }, }, }, }; } const approved = typeof answer === "object" && answer !== null && Reflect.get(answer, "action") === "accept" && Reflect.get(Reflect.get(answer, "content") ?? {}, "approve") === true; if (!approved) throw new ToolError("Token creation was declined", true); } const minted = await mintFineGrainedToken({ userId: caller.viewer.id, name: `${args.name} (via ${caller.agent.name})`, scopes: "oauth:delegated", grant: { permissions, allRepos: false, repoIds: [repo.id], agent: caller.agent.name }, ttlMs: args.expires_in_days * DAY_MS, clientId: caller.agent.clientId, parentTokenId: caller.tokenId, notAfter: lineage.lifetime_ends_at, kind: caller.agent.kind === "unregistered" ? "unregistered" : "delegated", agentId: lineage.agent_id ?? undefined, audience: lineage.audience, }); return { structured: { token: minted.token, id: minted.id, repo: `${repo.owner_name}/${repo.name}`, permissions, expires_at: minted.expiresAt, }, repoId: repo.id, }; }, }); export const accessTools = [createTokenTool];