UNPKG

kestrel.markets

Version:

A typed, token-efficient language + runtime for agentic trading: agents author bounded plans, the runtime fires them at the tick. CLI + typed library + MCP server.

235 lines (233 loc) 8.52 kB
import { generateAgentKeypair } from "./bin-z6kh7na8.js"; import { parseArgs } from "./bin-1gc4zavq.js"; import { CliError, EXIT } from "./bin-t9ggwnv5.js"; import { defaultCredentialsPath, defaultKeypairPath, saveCredential, saveKeypair } from "./bin-q0g6xvsw.js"; import"./bin-wckvcay0.js"; // src/cli/commands/register.ts import { execFileSync } from "node:child_process"; var DEFAULT_API = "https://api.kestrel.markets"; function usageErr(message) { return new CliError({ code: "USAGE", exit: EXIT.USAGE, message }); } function readGitConfig(key) { try { const out = execFileSync("git", ["config", "--get", key], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }); const v = out.trim(); return v.length > 0 ? v : undefined; } catch { return; } } function defaultGitIdentity() { const name = readGitConfig("user.name"); const email = readGitConfig("user.email"); return { ...name !== undefined ? { name } : {}, ...email !== undefined ? { email } : {} }; } function resolveApiBase(globals, env) { const raw = globals.api ?? env.KESTREL_API; if (raw === undefined || raw === "" || raw === "default") return DEFAULT_API; return raw.replace(/\/+$/, ""); } async function registerCommand(argv, ctx, globals, deps = {}) { const { flags, bools } = parseArgs(argv, new Set(["no-git-identity", "keypair"]), new Set(["name", "scopes"])); const env = deps.env ?? process.env; const fetchImpl = deps.fetch ?? globalThis.fetch; if (fetchImpl === undefined) { throw new CliError({ code: "RUNTIME_UNAVAILABLE", exit: EXIT.RUNTIME_UNAVAILABLE, message: "global fetch unavailable — need node ≥18 or bun to register" }); } const base = resolveApiBase(globals, env); const withGit = !bools.has("no-git-identity"); const identity = withGit ? (deps.gitIdentity ?? defaultGitIdentity)() : {}; const name = flags.get("name"); const scopes = flags.get("scopes")?.split(",").map((s) => s.trim()).filter((s) => s.length > 0); disclose(ctx, base, withGit, identity); const body = {}; if (name !== undefined) body.name = name; if (withGit && (identity.name !== undefined || identity.email !== undefined)) { body.claimed_git = { ...identity.name !== undefined ? { name: identity.name } : {}, ...identity.email !== undefined ? { email: identity.email } : {} }; } if (scopes !== undefined && scopes.length > 0) body.scope_request = scopes.join(","); const res = await fetchImpl(`${base}/agents/register`, { method: "POST", headers: { "content-type": "application/json", accept: "application/json" }, body: JSON.stringify(body) }); if (res.status === 403) { const detail = await problemDetail(res); throw usageErr(`registration refused: ${detail}`); } if (res.status === 429) { throw new CliError({ code: "HTTP_429", exit: EXIT.GENERIC, message: "registration rate-limited — retry later or reuse the credential you already hold" }); } if (!res.ok) { const detail = await problemDetail(res, `registration failed (HTTP ${res.status})`); throw new CliError({ code: `HTTP_${res.status}`, exit: res.status >= 500 ? EXIT.RUNTIME_UNAVAILABLE : EXIT.GENERIC, message: `registration failed (HTTP ${res.status}): ${detail}`, ...res.status === 422 ? { hint: "--scopes is a COMMA-SEPARATED scope request (e.g. --scopes data,sim); an unknown scope literal is rejected — remove it and retry" } : {} }); } const reg = await res.json(); const credPath = deps.credentialsPath ?? defaultCredentialsPath(env); const wantKeypair = bools.has("keypair"); let enrolled; if (wantKeypair) { enrolled = await enrollKeypair(ctx, fetchImpl, base, reg, deps, env); } const credential = { version: 1, api: base, agent_id: reg.agent_id, capability: reg.capability, token_type: reg.token_type, scopes: reg.scopes, expiry: reg.expiry, ...reg.subject !== undefined ? { subject: reg.subject } : {}, ...enrolled !== undefined ? { keypair_enrolled: true } : {}, ...withGit && (identity.name !== undefined || identity.email !== undefined) ? { claimed: { ...identity.name !== undefined ? { name: identity.name } : {}, ...identity.email !== undefined ? { email: identity.email } : {} } } : {} }; saveCredential(credential, credPath); emitResult(ctx, reg, credPath, enrolled); return 0; } async function enrollKeypair(ctx, fetchImpl, base, reg, deps, env) { if (reg.subject === undefined) { process.stderr.write(`disclose: --keypair skipped — the platform did not return a subject to bind the key to `); return; } const { publicJwk, privateJwk } = await generateAgentKeypair(); process.stderr.write(`disclose: generated a local Ed25519 keypair; enrolling the PUBLIC half (the private key never leaves this machine) `); const enrollRes = await fetchImpl(`${base}/agents/enroll-key`, { method: "POST", headers: { "content-type": "application/json", accept: "application/json", authorization: `Bearer ${reg.capability}` }, body: JSON.stringify({ public_key: publicJwk }) }); if (!enrollRes.ok) { const detail = await problemDetail(enrollRes); process.stderr.write(`disclose: --keypair enrollment failed (HTTP ${enrollRes.status}: ${detail}); the durable capability still works `); return; } const body = await enrollRes.json(); const keyPath = deps.keypairPath ?? defaultKeypairPath(env); const keypair = { version: 1, api: base, subject: reg.subject, public_jwk: publicJwk, private_jwk: privateJwk, audience: body.jwt.audience, max_ttl_seconds: body.jwt.max_ttl_seconds, enrolled_at: new Date().toISOString() }; saveKeypair(keypair, keyPath); return { keyPath, thumbprint: body.public_key_thumbprint }; } function disclose(ctx, base, withGit, identity) { if (ctx.mode === "json") { process.stderr.write(JSON.stringify({ disclosure: { endpoint: `${base}/agents/register`, sends_claimed_git_identity: withGit, claimed: withGit ? identity : null, note: "claimed git identity is UNVERIFIED (freely spoofable); pass --no-git-identity to omit it" } }) + ` `); return; } const lines = []; lines.push(`registering with ${base}/agents/register`); if (withGit && (identity.name !== undefined || identity.email !== undefined)) { lines.push(`will send CLAIMED git identity (UNVERIFIED): ${identity.name ?? "(no name)"} <${identity.email ?? "no-email"}>`); lines.push("this identity is freely spoofable and is NOT trusted as authentication; pass --no-git-identity to omit it"); } else if (withGit) { lines.push("no git user.name/user.email found — sending no claimed identity"); } else { lines.push("--no-git-identity: sending NO claimed git identity"); } for (const l of lines) process.stderr.write(`disclose: ${l} `); } function emitResult(ctx, reg, credPath, enrolled) { if (ctx.mode === "json") { process.stdout.write(JSON.stringify({ schema: "kestrel.register/v1", agent_id: reg.agent_id, token_type: reg.token_type, scopes: reg.scopes, expiry: reg.expiry, credential_path: credPath, keypair: enrolled ? { enrolled: true, keyfile_path: enrolled.keyPath, thumbprint: enrolled.thumbprint } : null, upgrade: reg.upgrade ?? null }) + ` `); return; } if (ctx.mode === "text") { const kp = enrolled ? ` keyfile=${enrolled.keyPath}` : ""; process.stdout.write(`registered agent_id=${reg.agent_id} scopes=${reg.scopes.join(",")} expiry=${reg.expiry} credential=${credPath}${kp} `); return; } process.stdout.write(`registered as ${reg.agent_id} `); process.stdout.write(` scopes: ${reg.scopes.join(", ")} `); process.stdout.write(` expiry: ${reg.expiry} `); process.stdout.write(` credential: ${credPath} `); if (enrolled) { process.stdout.write(` keyfile: ${enrolled.keyPath} (0600 — Ed25519 private key; JWT auth) `); process.stdout.write(` key id: ${enrolled.thumbprint} `); } if (reg.upgrade?.verify_email_hint) process.stdout.write(` upgrade: ${reg.upgrade.verify_email_hint} `); } async function problemDetail(res, fallback = "scope not mintable at registration") { try { const b = await res.json(); return b.detail ?? b.title ?? fallback; } catch { return fallback; } } export { registerCommand, defaultGitIdentity, DEFAULT_API };