infra-kit 0.7.12 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/mcp.js.map CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../src/entry/mcp.ts", "../src/lib/error-handlers/index.ts", "../src/mcp/server.ts", "../src/mcp/resources/index.ts", "../src/mcp/tools/index.ts", "../src/lib/tool-handler/confirm-token.ts", "../src/lib/session-env/session-env.ts", "../src/lib/tool-handler/argument-form.ts", "../src/lib/tool-handler/tool-handler.ts"],
4
- "sourcesContent": ["import { serveStdio } from '@modelcontextprotocol/server/stdio'\nimport process from 'node:process'\nimport { setTimeout as delay } from 'node:timers/promises'\n\nimport { setupErrorHandlers } from 'src/lib/error-handlers'\nimport { initLoggerMcp } from 'src/lib/logger'\nimport { suppressTypelessPackageJsonWarning } from 'src/lib/node-warnings'\n\nimport { createMcpServer } from '../mcp/server'\n\n// The MCP tools run the same commands as the CLI, so they hit the same consumer `.ts` configs.\n// This keeps the client's log pane clean; it cannot affect protocol framing either way, since\n// the stdio transport frames JSON-RPC on stdout and `emitWarning` only ever writes to stderr.\nsuppressTypelessPackageJsonWarning()\n\nconst logger = initLoggerMcp()\n\n/**\n * `serveStdio` calls this LAZILY, on the first inbound message, and may call it TWICE on one\n * connection via the probe-then-legacy discard path. The factory must therefore stay free of\n * non-idempotent process-scope side effects.\n */\nconst buildOrDie = async () => {\n try {\n return await createMcpServer()\n } catch (error) {\n logger.error({ err: error, msg: 'Failed to create MCP server' })\n logger.flush()\n\n process.exit(1)\n }\n}\n\nsetupErrorHandlers(logger)\n\nconst handle = serveStdio(buildOrDie, {\n onerror: (error) => {\n logger.error({ err: error, msg: 'MCP stdio entry error' })\n logger.flush()\n },\n})\n\n// NOT \"listening\": `serveStdio` returns while the transport start is still pending, so nothing is\n// established at this point beyond the entry having been wired up. Claiming readiness here would\n// print \"listening\" on a transport that failed to start.\nlogger.info({ msg: 'MCP stdio entry started.' })\n\nlet isShuttingDown = false\n\nconst shutdown = async (signal: NodeJS.Signals) => {\n // A second signal inside the 1500 ms window must not start a second teardown.\n if (isShuttingDown) {\n return\n }\n\n isShuttingDown = true\n\n logger.info({ msg: `Received ${signal}. Shutting down...` })\n\n // Exit 0 is unconditional: a rejecting close() must not escape to `unhandledRejection` and turn a\n // clean shutdown into exit 1. The SDK already routes close errors to `onerror`; this is the belt.\n try {\n await Promise.race([handle.close(), delay(1500)])\n } catch (error) {\n logger.error({ err: error, msg: 'MCP stdio close failed during shutdown' })\n }\n\n logger.flush()\n\n process.exit(0)\n}\n\nprocess.on('SIGINT', () => {\n void shutdown('SIGINT')\n})\n\nprocess.on('SIGTERM', () => {\n void shutdown('SIGTERM')\n})\n", "import process from 'node:process'\nimport type { Logger } from 'pino'\n\nimport { LOG_FILE_PATH } from '../logger/index'\n\n/**\n * Setup handlers for fatal, non-signal process events.\n *\n * @param logger - The logger instance\n *\n * ONLY FOR SERVER!\n *\n * Signal handling deliberately does NOT live here: a bounded teardown needs the\n * `StdioServerHandle` returned by `serveStdio`, which only the MCP entry holds, so\n * `SIGINT`/`SIGTERM` are registered in `src/entry/mcp.ts` instead. The arms removed from\n * here called `process.exit(0)` without flushing the logger, discarding the final log\n * lines on every clean shutdown; the entry's `shutdown` flushes before exiting.\n */\nexport const setupErrorHandlers = (logger: Logger) => {\n process.on('uncaughtException', (error) => {\n logger.fatal({ err: error, msg: 'Uncaught Exception' })\n logger.error(`Uncaught Exception! Check ${LOG_FILE_PATH}. Shutting down...`)\n logger.flush()\n process.exit(1)\n })\n\n process.on('unhandledRejection', (reason, promise) => {\n logger.fatal({ reason, promise, msg: 'Unhandled Rejection' })\n logger.error(`Unhandled Rejection! Check ${LOG_FILE_PATH}. Shutting down...`)\n logger.flush()\n process.exit(1)\n })\n}\n", "import { McpServer } from '@modelcontextprotocol/server'\n\nimport { mcpMode } from 'src/lib/mcp-mode'\n\nimport packageJson from '../../package.json' with { type: 'json' }\nimport { initializeResources } from './resources'\nimport { initializeTools } from './tools'\n\nexport async function createMcpServer() {\n // Marks `process.stdin` as the JSON-RPC transport for every prompt guard in the\n // process. Set HERE rather than in `entry/mcp.ts` (the sole caller) because that\n // module starts a server at import time and so can never be unit-tested \u2014 a test\n // there could only stub the flag, which proves nothing about it ever being set.\n // This runs before `server.connect`, so no tool handler can outrun it.\n mcpMode.enabled = true\n\n const server = new McpServer(\n {\n name: 'infra-kit',\n version: packageJson.version,\n },\n {\n capabilities: {\n // `initializeResources` registers read-only resources below; the SDK's\n // `registerResource` also declares `resources.listChanged` on top of this.\n resources: { listChanged: true },\n tools: {},\n // Deliberately no `prompts`. Every procedure's surface is its plugin skill;\n // a prompt registered here rendered a duplicate `/infra-kit:release-create (MCP)`\n // row next to it (see docs/release-create-prompt-removal-plan.md).\n },\n },\n )\n\n await initializeResources(server)\n await initializeTools(server)\n\n return server\n}\n", "import type { McpServer } from '@modelcontextprotocol/server'\n\nimport type { InfraKitConfig } from 'src/lib/infra-kit-config'\nimport { getInfraKitConfig } from 'src/lib/infra-kit-config'\n\nimport type { DevContextSnapshot } from './dev-context'\nimport { readDevContext } from './dev-context'\n\n/** Stable URI of the merged-config resource. */\nexport const CONFIG_RESOURCE_URI = 'infra-kit://config'\n\n/** Stable URI of the dev-context resource. */\nexport const DEV_CONTEXT_RESOURCE_URI = 'infra-kit://dev-context'\n\n/**\n * URI of the retired `session` procedure \u2014 served as a three-line deprecation stub, not the procedure.\n *\n * The procedure moved to the plugin's `/infra-kit:session` skill (docs/session-env-picker-plan.md \u00A73.6).\n * The plugin is git-sourced and unpinned, so a consumer can run this CLI against a plugin whose old\n * `/infra-kit:session` command still reads this URI FIRST: the stub keeps that command on the form path\n * (`env-load` without `config`) instead of its 404 fallback. `release-create` and `setup` needed no stub \u2014\n * their commands' fallbacks call the tool directly. Leaves in the release after next.\n */\nexport const SESSION_WORKFLOW_URI = 'infra-kit://workflow/session'\n\nconst SESSION_WORKFLOW_STUB = [\n 'This procedure moved to the `/infra-kit:session` skill; update the plugin.',\n \"Meanwhile: no token \u2192 call `env-load` without `config`; the human's pick is the load.\",\n 'On a tool error or a refused result naming `config`, call `env-list` and list every row in prose, then ask.',\n].join('\\n')\n\n/**\n * The two disk reads the resources need, injected so the registration is unit-testable without touching\n * the filesystem or the config-loader's mtime cache. Production uses the real readers by default.\n */\nexport interface ResourceDeps {\n /** Loads the merged `infra-kit.json` (all override layers applied). May throw when no config exists. */\n loadConfig: () => Promise<InfraKitConfig>\n /** Reads the dev-context fragments `infra-kit dev` last wrote. Never throws; absent session \u21D2 empty. */\n readDevContext: () => DevContextSnapshot\n}\n\nconst defaultDeps: ResourceDeps = {\n loadConfig: getInfraKitConfig,\n readDevContext: () => {\n return readDevContext()\n },\n}\n\n/** Serialize a resource body as pretty JSON text \u2014 the wire form every MCP client can read. */\nconst jsonResource = (uri: string, value: unknown): { contents: { uri: string; mimeType: string; text: string }[] } => {\n return { contents: [{ uri, mimeType: 'application/json', text: JSON.stringify(value, null, 2) }] }\n}\n\n/**\n * Register infra-kit's READ-ONLY MCP resources so an agent can inspect the repo's config and dev state\n * without calling a tool (or mutating anything):\n *\n * - `infra-kit://config` \u2014 the merged `infra-kit.json` an agent would run against.\n * - `infra-kit://dev-context` \u2014 what `infra-kit dev` last wrote (backends up, ports, freshness); an\n * absent dev session resolves to an empty `session: 'none'` payload, NOT\n * an error.\n *\n * Both handlers are side-effect free and swallow their reader's failure into an `{ error }` payload rather\n * than throwing, so a bad on-disk config can never crash the long-lived MCP server.\n */\nexport const initializeResources = async (server: McpServer, deps: ResourceDeps = defaultDeps) => {\n server.registerResource(\n 'infra-kit-config',\n CONFIG_RESOURCE_URI,\n {\n title: 'infra-kit config',\n description: 'The merged infra-kit.json configuration (all override layers applied). Read-only.',\n mimeType: 'application/json',\n },\n async (uri) => {\n try {\n return jsonResource(uri.toString(), await deps.loadConfig())\n } catch (error) {\n return jsonResource(uri.toString(), { error: error instanceof Error ? error.message : String(error) })\n }\n },\n )\n\n server.registerResource(\n 'infra-kit-dev-context',\n DEV_CONTEXT_RESOURCE_URI,\n {\n title: 'infra-kit dev context',\n description:\n 'What `infra-kit dev` last wrote: backends currently up, their ports/origins, and fragment ' +\n 'freshness. Read-only and advisory (no liveness probe). No active session resolves to an empty payload.',\n mimeType: 'application/json',\n },\n // The SDK's `ReadResourceCallback` accepts a synchronous return; the disk read is sync, so no `async`.\n (uri) => {\n return jsonResource(uri.toString(), deps.readDevContext())\n },\n )\n\n server.registerResource(\n 'infra-kit-workflow-session',\n SESSION_WORKFLOW_URI,\n {\n title: 'session procedure (moved)',\n description:\n 'Retired: the session procedure now lives in the /infra-kit:session plugin skill. Update the plugin.',\n mimeType: 'text/markdown',\n },\n (uri) => {\n return { contents: [{ uri: uri.toString(), mimeType: 'text/markdown', text: SESSION_WORKFLOW_STUB }] }\n },\n )\n}\n", "import { CLIENT_CAPABILITIES_META_KEY } from '@modelcontextprotocol/server'\nimport type { ClientCapabilities, McpServer } from '@modelcontextprotocol/server'\nimport { z } from 'zod'\n\nimport { getExposedMcpTools } from 'src/lib/command-catalog'\nimport { createToolHandler } from 'src/lib/tool-handler'\nimport type { ToolCallContext } from 'src/lib/tool-handler'\n\nexport const initializeTools = async (server: McpServer) => {\n // The registered tool set is derived from the single command catalog, filtered\n // by its explicit `mcpExposed` allowlist. Nothing is added or subtracted here \u2014\n // an exposure decision that is not visible in the catalog is invisible to the\n // catalog's own gates, so this loop stays a plain projection of it.\n //\n // `doctor` IS exposed, and it is the one entry whose safety does not come from\n // those gates: it is `mutating: false` (read-only, ungated) purely because\n // `doctorMcpTool` is nullary and forwards nothing, so the CLI's `--fix` write path\n // is unreachable from MCP by construction. The P1 fail-closed gate only inspects\n // `mutating && mcpExposed` entries, so it cannot see that claim go false \u2014 the\n // guard is `src/commands/doctor/__tests__/doctor-mcp-surface.test.ts`, not this list.\n for (const tool of getExposedMcpTools()) {\n server.registerTool(\n tool.name,\n {\n // Display label; hosts fall back to `name` when absent. TOP-LEVEL `title` only \u2014\n // `annotations.title` is deliberately left unset, because some hosts prefer the latter when\n // present and two titles are a divergence waiting to happen.\n title: tool.title,\n description: tool.description,\n // Wrapped HERE, at the single `registerTool` call site, rather than in the 27\n // `defineMcpTool` definitions or in `CatalogMcpTool`. SDK v2 offers a deprecated\n // raw-shape overload that would auto-wrap, but it types the shape as its own\n // `ZodRawShape` (`Record<string, ZodType>`) while zod 4's `z.ZodRawShape` is the\n // looser `Readonly<{ [k: string]: $ZodType }>` \u2014 `$ZodType` lacks `def`/`type`/`_def`\n // and ~42 other members, so the raw shape is NOT assignable and the overload is\n // unreachable for us. `z.object()` closes that gap and lands on v2's PREFERRED\n // (non-deprecated) Standard Schema overload, since a `ZodObject` carries `~standard`.\n //\n // Wrapping here keeps the authoring shape in all 27 `src/commands/**` definitions\n // unchanged and leaves `defineMcpTool`'s `z.infer<z.ZodObject<TIn>>` handler typing\n // untouched \u2014 the migration needs zero edits under `src/commands/`.\n //\n // Gated tools additionally accept `confirmToken` here, at the boundary: `z.object` strips\n // undeclared keys, so without this the round-2 token the gate hands out would never reach\n // `createToolHandler`'s verification and every confirmation would be refused as absent.\n inputSchema: z.object(\n tool.requiresHumanConfirm === true ? withConfirmToken(tool.inputSchema) : tool.inputSchema,\n ),\n outputSchema: z.object(tool.outputSchema),\n // ADVISORY protocol hints, derived in the catalog from `mutating`. The authority for\n // destructive operations remains `requiresHumanConfirm` + `lib/tool-handler`'s confirm gate;\n // nothing in that gate reads these, and the spec forbids a client treating them as security.\n annotations: tool.annotations,\n // `anthropic/requiresUserInteraction` rides here on `setup`, the one tool that carries it. It is the\n // only human gate on the MCP path: the host prompts on EVERY call, in `bypassPermissions` too,\n // and an allow rule cannot skip it. Undefined for every other tool, which is the same as absent.\n _meta: tool.meta,\n },\n // The SDK's second callback argument (session, request state, signal) is threaded through so\n // the handler signature lands once; the gate binds tokens by tool name and needs none of it.\n wrapForRegistration(\n createToolHandler({\n toolName: tool.name,\n handler: tool.handler,\n requiresHumanConfirm: tool.requiresHumanConfirm,\n formProvider: tool.formProvider,\n // `server.server` is the underlying protocol instance the McpServer facade wraps; the\n // integration test in `mcp/__tests__/server.test.ts` reaches through it the same way.\n //\n // Envelope FIRST, accessor second. On a 2026-era connection the capabilities ride in each\n // request's envelope, and over stdio nothing ever seeds the server-level accessor from it\n // (the SDK does that only in its HTTP entry) \u2014 so the accessor alone read `undefined` on\n // every modern-era stdio call and no form was ever offered there. The accessor still answers\n // for a 2025-era connection, whose requests carry no envelope.\n getClientCapabilities: (ctx) => {\n const fromEnvelope = ctx?.mcpReq?.envelope?.[CLIENT_CAPABILITIES_META_KEY] as ClientCapabilities | undefined\n\n return fromEnvelope ?? server.server.getClientCapabilities()\n },\n }),\n ),\n )\n }\n}\n\nconst withConfirmToken = (shape: z.ZodRawShape): z.ZodRawShape => {\n return {\n ...shape,\n confirmToken: z\n .string()\n .optional()\n .describe('Round-2 only: the token returned by the round-1 gate, proving the arguments are unchanged.'),\n }\n}\n\nconst wrapForRegistration = (handle: ReturnType<typeof createToolHandler>) => {\n return (params: unknown, ctx: ToolCallContext) => {\n return handle(params, ctx)\n }\n}\n", "import { createRequestStateCodec } from '@modelcontextprotocol/server'\nimport { randomBytes } from 'node:crypto'\n\n/**\n * @fileoverview\n *\n * Round-1 gate payload \u2192 round-2 binding for the destructive-op confirm gate.\n *\n * Round 1 mints a `confirmToken` over the tool name and the CANONICAL form of the\n * arguments; round 2 must present that token together with the same arguments, or\n * the tool does not run.\n *\n * Without the binding an agent could obtain a gate for `env:dev` and re-call with\n * `confirm:true` for `env:prod` \u2014 the substitution hole\n * `docs/infra-kit-slash-commands-plan.md` \u00A70.12 records.\n */\n\n/** Keys the gate itself owns; they are never part of the signed argument set. */\nconst GATE_KEYS = new Set(['confirm', 'confirmToken'])\n\n/** Default token lifetime. Long enough for a human to read the gate, short enough to bound replay. */\nexport const CONFIRM_TOKEN_TTL_SECONDS = 600\n\n/** What a round-1 gate signs. `args` is the canonical JSON of the tool arguments. */\nexport interface ConfirmPayload {\n args: string\n}\n\n/** The subset of the SDK codec the gate uses; `ctx` is the tool name the token is bound to. */\nexport interface ConfirmCodec {\n mint: (payload: ConfirmPayload, toolName: string) => Promise<string>\n verify: (token: string, toolName: string) => Promise<ConfirmPayload>\n}\n\nexport type ConfirmRefusal = 'absent' | 'malformed' | 'mac' | 'expired' | 'bind' | 'mismatch'\n\ninterface BindContext {\n toolName: string\n}\n\ntype SdkBindContext = NonNullable<Parameters<ReturnType<typeof createRequestStateCodec>['mint']>[1]>\n\nconst asBindContext = (toolName: string): SdkBindContext => {\n return { toolName } as unknown as SdkBindContext\n}\n\nexport type ConfirmVerdict = { ok: true } | { ok: false; reason: ConfirmRefusal }\n\nconst sortKeysDeep = (value: unknown): unknown => {\n if (Array.isArray(value)) {\n return value.map(sortKeysDeep)\n }\n\n if (value !== null && typeof value === 'object') {\n const record = value as Record<string, unknown>\n\n return Object.fromEntries(\n Object.keys(record)\n .sort()\n .map((key) => {\n return [key, sortKeysDeep(record[key])]\n }),\n )\n }\n\n return value\n}\n\n/**\n * Stable JSON for an argument object: keys sorted recursively, arrays kept in order.\n * `JSON.stringify` is key-order-sensitive and round-2 arguments are re-serialized by\n * the client in whatever order the host emits, so signing or verifying the raw form\n * would refuse legitimate confirmations intermittently, per client.\n *\n * @example\n * canonicalArgs({ b: 2, a: { d: 1, c: [2, 1] } }) // => '{\"a\":{\"c\":[2,1],\"d\":1},\"b\":2}'\n */\nexport const canonicalArgs = (args: unknown): string => {\n return JSON.stringify(sortKeysDeep(args))\n}\n\n/**\n * The tool's own arguments: the incoming params with the gate's keys removed. This is\n * the ONE filter both rounds share, so round 2 (which carries `confirm` and\n * `confirmToken` on top of round 1's arguments) verifies against exactly what round 1 signed.\n *\n * @example\n * stripGateKeys({ env: 'dev', confirm: true, confirmToken: 'v1.x.y' }) // => { env: 'dev' }\n */\nexport const stripGateKeys = (params: unknown): Record<string, unknown> => {\n if (typeof params !== 'object' || params === null) return {}\n\n return Object.fromEntries(\n Object.entries(params as Record<string, unknown>).filter(([key]) => {\n return !GATE_KEYS.has(key)\n }),\n )\n}\n\n/**\n * Build the gate's codec. The key is per-process and random by default: one stdio\n * server process serves both rounds of every confirmation, so a key that dies with\n * the process is sufficient \u2014 that stops being true on any HTTP transport, where the\n * key would have to be shared across workers. Tests inject a short `ttlSeconds` or a\n * second key to exercise the expiry and tamper refusals.\n *\n * @example\n * const codec = createConfirmCodec()\n * const token = await codec.mint({ args: '{\"env\":\"dev\"}' }, 'env-clear')\n * await codec.verify(token, 'env-clear') // => { args: '{\"env\":\"dev\"}' }\n */\nexport const createConfirmCodec = (options: { key?: Uint8Array; ttlSeconds?: number } = {}): ConfirmCodec => {\n // The SDK types the bind context as its own `ServerContext`; the gate only ever binds to the\n // tool name, so a one-field object stands in for it on both mint and verify.\n const codec = createRequestStateCodec<ConfirmPayload>({\n key: options.key ?? randomBytes(32),\n ttlSeconds: options.ttlSeconds ?? CONFIRM_TOKEN_TTL_SECONDS,\n bind: (ctx) => {\n return (ctx as unknown as BindContext).toolName\n },\n })\n\n return {\n mint: (payload, toolName) => {\n return codec.mint(payload, asBindContext(toolName))\n },\n verify: (token, toolName) => {\n return codec.verify(token, asBindContext(toolName))\n },\n }\n}\n\nlet defaultCodec: ConfirmCodec | undefined\n\n/** The process-wide codec every registered tool shares, created on first use. */\nexport const getDefaultConfirmCodec = (): ConfirmCodec => {\n defaultCodec ??= createConfirmCodec()\n\n return defaultCodec\n}\n\nconst readConfirmToken = (params: unknown): string | undefined => {\n if (typeof params !== 'object' || params === null) return undefined\n\n const { confirmToken } = params as { confirmToken?: unknown }\n\n return typeof confirmToken === 'string' ? confirmToken : undefined\n}\n\nconst REFUSALS_FROM_CODEC: ReadonlySet<ConfirmRefusal> = new Set(['malformed', 'mac', 'expired', 'bind'])\n\nconst codecRefusal = (error: unknown): ConfirmRefusal => {\n const message = error instanceof Error ? error.message : ''\n\n return REFUSALS_FROM_CODEC.has(message as ConfirmRefusal) ? (message as ConfirmRefusal) : 'malformed'\n}\n\n/**\n * Mint the round-1 token for `params` (gate keys excluded), bound to `toolName`.\n *\n * @example\n * await mintConfirmToken(codec, 'env-clear', { env: 'dev' }) // => 'v1.<payload>.<mac>'\n */\nexport const mintConfirmToken = async (codec: ConfirmCodec, toolName: string, params: unknown): Promise<string> => {\n return codec.mint({ args: canonicalArgs(stripGateKeys(params)) }, toolName)\n}\n\n/**\n * Decide whether a round-2 call may run. Absence is a refusal in its own right \u2014\n * \"verify only when a token is present\" would let an agent omit the token and\n * substitute arguments freely, reopening the hole this module closes.\n *\n * @example\n * await verifyConfirmToken(codec, 'env-clear', { env: 'prod', confirm: true, confirmToken })\n * // => { ok: false, reason: 'mismatch' } (the token was minted for env:dev)\n */\nexport const verifyConfirmToken = async (\n codec: ConfirmCodec,\n toolName: string,\n params: unknown,\n): Promise<ConfirmVerdict> => {\n const token = readConfirmToken(params)\n\n if (token === undefined) return { ok: false, reason: 'absent' }\n\n let payload: ConfirmPayload\n\n try {\n payload = await codec.verify(token, toolName)\n } catch (error) {\n return { ok: false, reason: codecRefusal(error) }\n }\n\n if (payload.args !== canonicalArgs(stripGateKeys(params))) return { ok: false, reason: 'mismatch' }\n\n return { ok: true }\n}\n", "import fs from 'node:fs'\nimport path from 'node:path'\nimport process from 'node:process'\n\nimport {\n ENV_CLEAR_FILE,\n ENV_LOAD_FILE,\n INFRA_KIT_ENV_CLEARED_VAR,\n INFRA_KIT_SESSION_VAR,\n getSessionCacheDir,\n parseUnsetNamesFromEnvFile,\n parseVarsFromEnvFile,\n} from 'src/lib/constants'\nimport { logger } from 'src/lib/logger'\nimport { PROTECTED_CHILD_ENV_NAMES } from 'src/lib/mcp-proxy/protected-env'\n\n/**\n * What the session dir says is loaded right now, read the way `~/.zshenv` reads it\n * (src/commands/init/init.ts): env-load.sh wins unless env-clear.sh is strictly newer.\n * `signature` identifies the file the state came from so an unchanged file is never\n * re-parsed; `no-session` carries none because nothing is ever applied for it.\n */\nexport type SessionEnvState =\n | { kind: 'load'; vars: Record<string, string>; unset: string[]; signature: string }\n | { kind: 'clear'; unset: string[]; signature: string }\n | { kind: 'none'; signature: 'none' }\n | { kind: 'no-session' }\n\nexport interface SessionEnvApplyResult {\n set: string[]\n unset: string[]\n changed: boolean\n}\n\n/**\n * Names the file may carry but the overlay must never touch, on assign or on unset.\n * The proxy's set is reused so the two overlays cannot disagree about what plumbing\n * is; the four additions decide WHICH session dir this process reads and where the\n * plugin lives \u2014 a Doppler config defining `INFRA_KIT_SESSION` would otherwise point\n * the next read at another session.\n */\nconst NEVER_OVERLAID: ReadonlySet<string> = new Set([\n ...PROTECTED_CHILD_ENV_NAMES,\n INFRA_KIT_SESSION_VAR,\n 'XDG_CACHE_HOME',\n 'CLAUDE_PROJECT_DIR',\n 'CLAUDE_PLUGIN_ROOT',\n])\n\n/**\n * Captured on the FIRST apply, not at import: unit tests build their environment\n * before calling, and the lazy stdio factory may run twice per connection, so an\n * import-time capture could freeze either the wrong env or a half-applied one.\n */\nlet baseline: NodeJS.ProcessEnv | null = null\n/** Every name the previous apply wrote or deleted \u2014 what the next apply restores first. */\nconst touched = new Set<string>()\nlet lastSignature: string | null = null\n/** Latched on the first throw of `getSessionCacheDir`: one log line per process, then silence. */\nlet noSession = false\n\nexport const resetSessionEnvForTests = (): void => {\n baseline = null\n touched.clear()\n lastSignature = null\n noSession = false\n}\n\nconst statOrNull = (file: string): fs.Stats | null => {\n try {\n return fs.statSync(file)\n } catch {\n return null\n }\n}\n\n/**\n * Unlike the proxy's `NO_SESSION` fallback this never reads a `no-session` dir:\n * `env-load` over MCP throws in the same situation, so nothing could have written one.\n */\nconst resolveSessionDir = (): string | null => {\n if (noSession) return null\n\n try {\n return getSessionCacheDir()\n } catch {\n noSession = true\n logger.warn(`session-env: ${INFRA_KIT_SESSION_VAR} unset \u2014 no overlay`)\n\n return null\n }\n}\n\n/**\n * Which file the shell would source, decided from the stat pair alone so the signature\n * exists BEFORE anything is parsed. `atomicWriteFileSync` renames a fresh temp file into\n * place, so every write lands a new inode and `ino` makes the signature exact.\n */\ntype SourcedFile =\n | { kind: 'load'; file: string; signature: string }\n | { kind: 'clear'; file: string; signature: string }\n | { kind: 'none'; signature: 'none' }\n\nconst chooseSourcedFile = (dir: string): SourcedFile => {\n const loadPath = path.join(dir, ENV_LOAD_FILE)\n const clearPath = path.join(dir, ENV_CLEAR_FILE)\n const load = statOrNull(loadPath)\n const clear = statOrNull(clearPath)\n\n // zshenv: `! clear -nt load` \u2014 a tie goes to the load file.\n if (load && !(clear && clear.mtimeMs > load.mtimeMs)) {\n return { kind: 'load', file: loadPath, signature: `load:${load.ino}:${load.mtimeMs}:${load.size}` }\n }\n\n if (clear) return { kind: 'clear', file: clearPath, signature: `clear:${clear.ino}:${clear.mtimeMs}` }\n\n return { kind: 'none', signature: 'none' }\n}\n\nconst parseSourcedFile = (sourced: SourcedFile): SessionEnvState => {\n if (sourced.kind === 'load') {\n return {\n kind: 'load',\n vars: parseVarsFromEnvFile(sourced.file),\n unset: parseUnsetNamesFromEnvFile(sourced.file),\n signature: sourced.signature,\n }\n }\n\n if (sourced.kind === 'clear') {\n return { kind: 'clear', unset: parseUnsetNamesFromEnvFile(sourced.file), signature: sourced.signature }\n }\n\n return sourced\n}\n\nexport const readSessionEnvState = (dir = resolveSessionDir()): SessionEnvState => {\n if (dir === null) return { kind: 'no-session' }\n\n return parseSourcedFile(chooseSourcedFile(dir))\n}\n\n/** `readSessionEnvState` minus the parse when the file is the one already applied. */\nconst readSessionEnvStateIfMoved = (): SessionEnvState | null => {\n const dir = resolveSessionDir()\n\n if (dir === null) return { kind: 'no-session' }\n\n const sourced = chooseSourcedFile(dir)\n\n return sourced.signature === lastSignature ? null : parseSourcedFile(sourced)\n}\n\nconst restoreTouched = (): void => {\n for (const name of touched) {\n const original = baseline![name]\n\n if (original === undefined) {\n delete process.env[name]\n } else {\n process.env[name] = original\n }\n }\n\n touched.clear()\n}\n\ninterface Overlay {\n set: string[]\n unset: string[]\n skipped: string[]\n}\n\nconst assign = (overlay: Overlay, name: string, value: string): void => {\n if (NEVER_OVERLAID.has(name)) {\n overlay.skipped.push(name)\n\n return\n }\n\n process.env[name] = value\n touched.add(name)\n overlay.set.push(name)\n}\n\nconst remove = (overlay: Overlay, name: string): void => {\n if (NEVER_OVERLAID.has(name)) {\n overlay.skipped.push(name)\n\n return\n }\n\n delete process.env[name]\n touched.add(name)\n overlay.unset.push(name)\n}\n\nconst overlayState = (state: Exclude<SessionEnvState, { kind: 'no-session' }>): Overlay => {\n const overlay: Overlay = { set: [], unset: [], skipped: [] }\n\n if (state.kind === 'load') {\n for (const [name, value] of Object.entries(state.vars)) assign(overlay, name, value)\n for (const name of state.unset) remove(overlay, name)\n }\n\n if (state.kind === 'clear') {\n for (const name of state.unset) remove(overlay, name)\n assign(overlay, INFRA_KIT_ENV_CLEARED_VAR, '1')\n }\n\n return overlay\n}\n\nconst describeState = (state: Exclude<SessionEnvState, { kind: 'no-session' }>): string => {\n return state.kind === 'load' ? `load, ${Object.keys(state.vars).length} vars` : state.kind\n}\n\n/**\n * Make `process.env` equal `baseline \u2295 state`. SYNCHRONOUS on purpose: the SDK\n * dispatches tool handlers concurrently, and a restore+overlay that cannot yield\n * cannot interleave \u2014 `process.env` is always one full result, never a mix of two.\n *\n * The stat pair is the whole per-call cost: the default state is read through the\n * signature check, so an unchanged file is never parsed. Never throws: a missing\n * session id or a vanished file degrades to \"nothing applied\", corrected on the\n * next call.\n */\nexport const applySessionEnv = (state = readSessionEnvStateIfMoved()): SessionEnvApplyResult => {\n baseline ??= { ...process.env }\n\n if (state === null || state.kind === 'no-session' || state.signature === lastSignature) {\n return { set: [], unset: [], changed: false }\n }\n\n restoreTouched()\n\n const { set, unset, skipped } = overlayState(state)\n\n lastSignature = state.signature\n\n if (skipped.length > 0) logger.warn(`session-env: skipped protected names [${skipped.join(', ')}]`)\n\n logger.info(`session-env applied: set [${set.join(', ')}] unset [${unset.join(', ')}] (${describeState(state)})`)\n\n return { set, unset, changed: true }\n}\n", "import { acceptedContent, inputRequired, inputResponse } from '@modelcontextprotocol/server'\nimport type { InputRequiredResult } from '@modelcontextprotocol/server'\nimport type { z } from 'zod'\n\nimport { withDeadline } from 'src/lib/deadline'\nimport type { ArgumentFormProvider } from 'src/types'\n\n/**\n * @fileoverview\n *\n * The argument-form half of the MCP chokepoint: build the form, read what came back, and refuse\n * anything that came back narrower than it went out.\n *\n * Every function here is TOTAL. The form path runs on two things the handler does not control \u2014 a\n * provider written by someone else, and content the client sent back \u2014 and the SDK validates\n * neither (\"treat them as untrusted input\", `\u00ABd.mts\u00BB:1406`, `:1500`, `:2114`). A throw escaping\n * from here would leave the chokepoint's catch returning a TOOL ERROR on a call that was owed a\n * confirm gate, which is the failure this module exists to prevent. So every provider call and\n * every SDK call below is wrapped, and every failure mode \u2014 rejection, deadline, `null`, a thrown\n * `TypeError` \u2014 reads as the same single answer: `null`, meaning \"no form; fall to the gate\".\n */\n\n/**\n * The identifier the form is registered under in `inputRequests`, and the key the client's reply\n * comes back under in `ctx.mcpReq.inputResponses`. One key, one form, one round trip.\n */\n// Module-private on purpose. Both sides of the round trip are in this file \u2014 `buildArgumentForm`\n// registers under it, `readFormAction` and `readAcceptedArgs` read back under it \u2014 so the key is an\n// internal convention, not a contract anyone outside gets to depend on. Exporting it would invite a\n// caller to reach into `inputResponses` themselves and re-derive an action without the totality the\n// functions below are built to guarantee.\nconst FORM_KEY = 'args'\n\n/**\n * How long the chokepoint waits for a provider to produce a schema.\n *\n * A provider may reach the network (the release provider runs `git ls-remote` and a Jira fetch),\n * and those are `Promise.allSettled`-wrapped inside it \u2014 so they never FAIL, they only take as long\n * as they take. Without a ceiling a slow remote or an SSH key prompt turns `tools/call` into a call\n * that returns nothing at all: no form, no gate, no error. The deadline converts that into today's\n * behaviour instead.\n */\nexport const FORM_DEADLINE_MS = 3_000\n\n/**\n * The action a retried request carried for the form, with every non-elicitation response kind\n * folded into `missing`.\n *\n * `inputResponse` is documented TOTAL \u2014 a malformed or absent entry reads as `{kind:'missing'}` \u2014\n * and that is also where a key the SDK DROPPED lands: a dropped entry is removed from\n * `inputResponses` and only its name is kept in `droppedInputResponseKeys`, so it reaches us as an\n * absent key. That is why nothing here reads `droppedInputResponseKeys`: the drop is already\n * covered, as a `missing` that is not an accept.\n */\nexport type FormAction = 'accept' | 'decline' | 'cancel' | 'missing'\n\n/**\n * Read the form's action out of a retried request's responses.\n *\n * `InputResponseView` is a union and `action` exists only on its `kind:'elicit'` member, so the\n * narrowing is load-bearing rather than cosmetic.\n *\n * @example\n * readFormAction({ args: { action: 'decline' } }) // => 'decline'\n * readFormAction(undefined) // => 'missing'\n */\nexport const readFormAction = (responses: Record<string, unknown> | undefined): FormAction => {\n const view = inputResponse(responses, FORM_KEY)\n\n return view.kind === 'elicit' ? view.action : 'missing'\n}\n\n/**\n * The provider's schema for these arguments, or `null` on ANY failure: a rejection, a deadline\n * overrun, a synchronous throw before the promise is even returned, or the provider's own `null`.\n *\n * Called on BOTH rounds. Round 1 needs it to build the form; round 2 needs a schema to validate what\n * came back (\u00ABd.mts\u00BB:1406), and the round-2 `params` are the round-1 arguments the client echoed\n * back, so re-asking the provider reproduces it.\n */\n// The REBUILD on round 2 is deliberate, and it is not free. The two rounds are two separate\n// `tools/call` requests \u2014 round 2 is a re-entry, discriminated by `responses !== undefined` \u2014 so this\n// runs at most once per request and never twice within one. Across an accepted form that is two\n// builds, and for a provider that reaches the network (the release provider runs `git ls-remote` and\n// a Jira fetch) four calls where a single build would cost two. Both alternatives are worse.\n//\n// Reusing round 1's schema is not \"threading a value through\": nothing in this chokepoint survives a\n// request. The gate is stateless BY CONSTRUCTION \u2014 round 2 is trusted because it returns an HMAC the\n// server minted, not because the server remembered anything \u2014 so a reused schema needs either a\n// keyed server-side cache with its own eviction, or transport through the client. The second hands\n// the client the validator that is the ONLY thing checking the content it just sent. The first\n// re-introduces on a long-lived process exactly the per-request state `confirm-token.ts` was built\n// to avoid, to save two network calls per human decision.\n//\n// The rebuild does mean round 2 validates against a MOVED world: a choice that was legal when the\n// human made it can fail now, and they lose it. That is the correct direction to fail. A provider\n// enumerates candidates precisely because the current world constrains them \u2014 the release provider\n// lists the versions that already exist so a cut cannot collide \u2014 so honouring a stale selection\n// executes the collision the enumeration was there to prevent. The loss is not silent either: it\n// lands on the `formDiscarded` gate, which names the discard in a field AND in prose. Telling a\n// drift-caused failure apart from ordinary invalid content would need round 1's schema to compare\n// against, which is the cross-request state above \u2014 bought to reword a message, not to change an\n// outcome. `f14` in the tests pins the drifted round trip.\nconst trySchema = async (\n provider: ArgumentFormProvider,\n params: unknown,\n deadlineMs: number,\n): Promise<z.ZodObject<z.ZodRawShape> | null> => {\n try {\n return await withDeadline(provider.buildRequestedSchema(params), deadlineMs)\n } catch {\n return null\n }\n}\n\n/**\n * The form to send, or `null` meaning \"offer no form; fall to the gate\".\n *\n * The `elicit()` call is wrapped for a documented reason, not defensively: a Standard Schema it\n * cannot express as the restricted wire shape throws a `TypeError` BEFORE anything is sent\n * (\u00ABd.mts\u00BB:1404-1407) \u2014 a nested object property is enough \u2014 and a provider can arm that without\n * writing anything the type system objects to.\n *\n * @example\n * await buildArgumentForm(provider, { releases: [] }, 3000)\n * // => { resultType: 'input_required', inputRequests: { args: { method: 'elicitation/create', \u2026 } } }\n */\nexport const buildArgumentForm = async (\n provider: ArgumentFormProvider,\n params: unknown,\n deadlineMs: number,\n): Promise<InputRequiredResult | null> => {\n const schema = await trySchema(provider, params, deadlineMs)\n\n if (schema === null) return null\n\n try {\n return inputRequired({\n inputRequests: {\n [FORM_KEY]: inputRequired.elicit({ message: provider.message, requestedSchema: schema }),\n },\n })\n } catch {\n return null\n }\n}\n\n/**\n * The accepted form content, VALIDATED and merged over the round-1 arguments \u2014 or `null` when it\n * cannot be trusted, which the caller reads as \"gate with the round-1 arguments\".\n *\n * Three untrusted steps, each wrapped: the provider's schema, the SDK's schema-aware validation of\n * the content, and the provider's own merge.\n */\n// `acceptedContent`'s schema-aware overload is used because nothing between the client and here has\n// checked the content. It throws a `TypeError` on an asynchronously validating schema\n// (\u00ABd.mts\u00BB:1468-1470), and `z.ZodObject<z.ZodRawShape>` freely permits `.refine(async \u2026)` \u2014 so the\n// wrap is the enforcement of a contract the types cannot express. It returns `undefined` both for\n// \"not an accept\" and for \"failed validation\"; the caller has already discriminated the action, so\n// here `undefined` means only the latter. `toArgs` is the one member called on client-supplied\n// input: its contract says it never throws, and the wrap is what makes that true rather than promised.\nexport const readAcceptedArgs = async (\n provider: ArgumentFormProvider,\n params: unknown,\n responses: Record<string, unknown> | undefined,\n deadlineMs: number,\n): Promise<Record<string, unknown> | null> => {\n const schema = await trySchema(provider, params, deadlineMs)\n\n if (schema === null) return null\n\n let content: Record<string, unknown> | undefined\n\n try {\n content = acceptedContent(responses, FORM_KEY, schema) as Record<string, unknown> | undefined\n } catch {\n return null\n }\n\n if (content === undefined) return null\n\n try {\n return provider.toArgs(content, params)\n } catch {\n return null\n }\n}\n\n/** A plain object \u2014 not an array, not `null`, not a primitive. */\nconst isRecord = (value: unknown): value is Record<string, unknown> => {\n return typeof value === 'object' && value !== null && !Array.isArray(value)\n}\n\n/**\n * True when `after` removed a top-level key of `before`, or CHANGED the length of one of its\n * arrays. The chokepoint refuses such a merge and gates on the round-1 arguments instead.\n *\n * Both array directions are violations, and the growth half is the silent one: an injected entry\n * the caller never supplied stays schema-valid and EXECUTES. Deliberately TOP-LEVEL ONLY \u2014 nested\n * removal and equal-shape substitution are OUT of scope and are guarded only by the provider's merge.\n *\n * @example\n * narrowsArgs({ releases: [1, 2] }, { releases: [1] }) // => true (truncated)\n * narrowsArgs({ releases: [1] }, { releases: [1, 2] }) // => true (grown)\n * narrowsArgs({ a: 1, b: 2 }, { a: 9, b: 2 }) // => false (substitution is the feature)\n */\n// A form narrows a CHOICE; it never withdraws or invents an item the caller listed. So the length\n// rule is a ceiling rather than a heuristic \u2014 no legitimate provider, present or future, needs it\n// relaxed, and one that trips it is wrong by construction. The depth limit is the opposite kind of\n// decision: a recursive walk would forbid a human blanking an optional field they no longer want,\n// and nothing generic separates \"the human cleared this\" from \"the provider dropped this\".\nexport const narrowsArgs = (before: unknown, after: unknown): boolean => {\n if (!isRecord(before)) return false\n // `before` is a record and `after` is not: EVERY argument was narrowed away.\n if (!isRecord(after)) return true\n\n for (const [key, value] of Object.entries(before)) {\n if (!(key in after)) return true\n\n const replacement = after[key]\n\n if (Array.isArray(value) && (!Array.isArray(replacement) || replacement.length !== value.length)) return true\n }\n\n return false\n}\n", "import type { ClientCapabilities, InputRequiredResult } from '@modelcontextprotocol/server'\n\nimport { assertNever } from 'src/lib/assert-never'\nimport { commandEcho } from 'src/lib/command-echo'\nimport { ensureUserProjectConfig } from 'src/lib/config-bootstrap'\nimport { logger } from 'src/lib/logger'\nimport { applySessionEnv as applySessionEnvFromFile } from 'src/lib/session-env'\nimport { textContent } from 'src/types'\nimport type { ArgumentFormProvider, ToolsExecutionResult } from 'src/types'\n\nimport { FORM_DEADLINE_MS, buildArgumentForm, narrowsArgs, readAcceptedArgs, readFormAction } from './argument-form'\nimport type { FormAction } from './argument-form'\nimport { getDefaultConfirmCodec, mintConfirmToken, stripGateKeys, verifyConfirmToken } from './confirm-token'\nimport type { ConfirmCodec, ConfirmRefusal } from './confirm-token'\n\ninterface ToolHandlerArgs {\n toolName: string\n handler: (params: any) => Promise<ToolsExecutionResult>\n /**\n * When true, this tool is gated by the destructive-op confirm gate below. Sourced from the\n * catalog tool's {@link CatalogMcpTool.requiresHumanConfirm} at registration (`mcp/tools/index.ts`).\n */\n requiresHumanConfirm?: boolean\n /**\n * Optional per-tool argument-form seam, sourced from the catalog tool's\n * {@link CatalogMcpTool.formProvider} at registration. Wired on gated and ungated tools alike (the\n * ungated `env-load` carries one). Absent must always mean \"the chokepoint behaves exactly as it\n * always has\": no form on either path.\n */\n formProvider?: ArgumentFormProvider\n /**\n * Reads the connected client's declared capabilities, so the chokepoint can ask whether this client\n * can render a form at all before offering one. Injected as a CLOSURE rather than a value because\n * capabilities are only known after initialize, and this handler is built at registration time.\n *\n * It receives the per-call context because on a 2026-era connection the capabilities travel in the\n * request ENVELOPE (`ctx.mcpReq.envelope`), and the server-level accessor is seeded from it only by\n * the SDK's HTTP entry \u2014 over stdio it stays `undefined` for the life of the process, which read as\n * \"cannot form\" and silently refused every modern-era client a form (found by the E-M1 lane).\n */\n getClientCapabilities?: (ctx: ToolCallContext | undefined) => ClientCapabilities | undefined\n /** Token codec for the gate. Defaults to the process-wide one; tests inject short-TTL or foreign-key codecs. */\n confirmCodec?: ConfirmCodec\n /**\n * How long the form path waits for {@link ArgumentFormProvider.buildRequestedSchema}. Defaults to\n * {@link FORM_DEADLINE_MS}; tests inject a few milliseconds so the deadline lane asserts a bound\n * instead of hanging until the runner's own timeout.\n */\n formDeadlineMs?: number\n /**\n * Re-applies the session's `env-load` file to `process.env`. Defaults to the real overlay; tests\n * inject a spy for the ordering lanes, and the mutation build a no-op so it never reads a session dir.\n */\n applySessionEnv?: () => void\n}\n\n/**\n * The SDK hands every tool callback a second argument (session, request state, abort signal).\n * Threaded through untouched so the signature lands once.\n */\nexport interface ToolCallContext {\n sessionId?: string\n /**\n * The per-request MCP envelope. Only the multi-round-trip carrier is declared: `inputResponses`\n * is how a client's answer to the argument form comes BACK, and its presence \u2014 not its content \u2014\n * is what separates a first call from a re-entry.\n */\n mcpReq?: {\n inputResponses?: Record<string, unknown>\n /**\n * The 2026-era request envelope. Read for ONE key \u2014 the client capabilities the SDK stamps under\n * `CLIENT_CAPABILITIES_META_KEY` \u2014 because that is where a modern-era stdio connection carries\n * them; the injection site owns the lookup.\n */\n envelope?: Record<string, unknown>\n /**\n * Keys the SDK dropped because they were not bare response objects. Declared for completeness\n * and deliberately UNREAD: a dropped key is absent from `inputResponses`, so it already reads\n * as `{kind:'missing'}` \u2014 an action that is not an accept, which is the decline path.\n */\n droppedInputResponseKeys?: string[]\n }\n}\n\nexport type GateState = 'run' | 'form' | 'declined' | 'gate' | 'verify' | 'run-form'\n\n/** Everything `resolveGateState` needs, all of it knowable BEFORE any work is done. */\nexport interface GateInputs {\n /** `requiresHumanConfirm === true` \u2014 spelled at the call site, not re-derived here. */\n gated: boolean\n /** The round-2 execute signal. */\n confirmed: boolean\n /** Present iff the client came BACK with an answer to the form. Content is irrelevant here. */\n responses: Record<string, unknown> | undefined\n /** The client declared form-mode elicitation. */\n canForm: boolean\n hasProvider: boolean\n /** The provider wants a form for THESE arguments. */\n formable: boolean\n /** The client came back with an ACCEPT specifically \u2014 decline, cancel and absence are all `false`. */\n accepted: boolean\n}\n\nconst isConfirmed = (params: unknown): boolean => {\n return typeof params === 'object' && params !== null && (params as { confirm?: unknown }).confirm === true\n}\n\n/**\n * The CANDIDATE state for a call. First-match-wins, in the order written, **every condition stated\n * in full** \u2014 no row relies on an earlier row having excluded anything.\n *\n * It is a candidate, not a verdict: `form` is the one outcome that can decline itself, because\n * whether a form can actually be built is knowable only after the provider has run under a\n * deadline. Every other outcome is terminal in the caller's own body.\n *\n * \"Gate\" describes rows G1\u2013G4 only: they are today's gate, unchanged, and they consume every\n * `gated` input before the ungated rows are reached. Rows U1\u2013U4 are a CALL state, not a gate\n * state \u2014 nothing on that path withholds execution pending consent; the form only fills arguments.\n * The name is kept because the tests and the mutation build import it.\n */\n// `!confirmed` is spelled on G1, G2 AND G3 for one reason: a non-elicitation round 2 carries\n// `confirm:true` with no `inputResponses`, so without it G3 matches, the call re-gates forever\n// and `verify` is unreachable. `responses !== undefined` is likewise the ONLY first-call/came-back\n// discriminator \u2014 `acceptedContent(...)` being falsy reads the same for decline, cancel and absence\n// alike, which would send a decline back to the form and re-prompt forever.\n//\n// `confirmed` is unread on U1\u2013U4: no ungated tool declares `confirm`, and the SDK's zod parse strips\n// undeclared keys before the params reach here, so it is `false` by construction on that path.\n//\n// The `!gated` conjuncts on U1\u2013U3 are defence-in-depth, not load-bearing: G1\u2013G4 partition `gated`\n// exhaustively, so the U rows are reachable only under `!gated` by ORDER. The order is what the\n// gated lanes pin \u2014 moving the U rows above the G rows lets an accepted form run without a token.\n//\n// Exported for its OWN tests, and not re-exported from `index.ts`, so the package's public surface\n// is unchanged. Some conjuncts are backed a second time downstream \u2014 `hasProvider` is also enforced\n// by the provider narrowing in `resolveForm` \u2014 so an end-to-end assertion cannot tell which\n// guard held. Testing the predicate directly is what makes deleting one of them observable.\nexport const resolveGateState = (input: GateInputs): GateState => {\n if (\n input.gated &&\n !input.confirmed &&\n input.responses === undefined &&\n input.canForm &&\n input.hasProvider &&\n input.formable\n ) {\n return 'form'\n }\n\n if (input.gated && !input.confirmed && input.responses !== undefined && !input.accepted) return 'declined'\n\n if (input.gated && !input.confirmed && (input.responses === undefined || input.accepted)) return 'gate'\n\n if (input.gated && input.confirmed) return 'verify'\n\n if (!input.gated && input.responses === undefined && input.canForm && input.hasProvider && input.formable) {\n return 'form'\n }\n\n if (!input.gated && input.responses !== undefined && input.hasProvider && !input.accepted) return 'declined'\n\n if (!input.gated && input.responses !== undefined && input.hasProvider && input.accepted) return 'run-form'\n\n // U4, condition complete: `!gated \u2227 ((responses === undefined \u2227 !(canForm \u2227 hasProvider \u2227 formable))\n // \u2228 (responses !== undefined \u2227 !hasProvider))` \u2014 the exact complement of U1\u2013U3 under `!gated`.\n // Spelled in the exhaustive partition test rather than here, so the fall-through stays the\n // fall-through.\n return 'run'\n}\n\n/**\n * Every gate and refusal payload sets `isError: true`: the MCP SDK validates a result's\n * `structuredContent` against the tool's `outputSchema` UNLESS `isError` is set, and no gated\n * tool's schema matches these payloads. A soft stop, not a failure \u2014 the tool did NOT run.\n */\nconst softStop = (structuredContent: Record<string, unknown>): ToolsExecutionResult => {\n return {\n content: textContent(JSON.stringify(structuredContent, null, 2)),\n structuredContent,\n isError: true,\n }\n}\n\n/** What the gate must SAY beyond its standing instruction, and to whom the arguments belong. */\ninterface GateNotices {\n /**\n * The human answered the form and their answer was thrown away \u2014 it failed validation, or it\n * narrowed the arguments. The gate then presents the AGENT'S values for approval in place of the\n * ones the human just chose, so saying so is not decoration: an unlabelled swap of the whole\n * selection is a worse substitution than the single-field one the merge exists to prevent.\n */\n formDiscarded: boolean\n hasProvider: boolean\n}\n\nconst FORM_DISCARDED_CLAUSE =\n 'The values you submitted in the form could NOT be applied and were DISCARDED \u2014 they failed validation or narrowed the arguments \u2014 so the arguments shown above are the ORIGINAL ones, not your selection.'\n\n// PM-D: only one gated tool in eight carries a provider, so a user who learns \"infra-kit asks me\n// before it does this\" from the one that does will read the other seven's silence as safety. The\n// true rule is \"no dialog \u21D2 this tool has no provider\", and nothing else in the payload says so.\nconst NO_FORM_CLAUSE =\n 'This tool does not prompt for its arguments; you are being asked to approve the values shown above.'\n\n/**\n * The round-1 gate: names the tool, echoes the arguments it will bind to, and carries the\n * `confirmToken` round 2 must return alongside `confirm:true` and the SAME arguments.\n */\nconst buildConfirmGate = async (\n codec: ConfirmCodec,\n toolName: string,\n params: unknown,\n notices: GateNotices,\n): Promise<ToolsExecutionResult> => {\n const resolvedArgs = stripGateKeys(params)\n const confirmToken = await mintConfirmToken(codec, toolName, params)\n const message = [\n notices.formDiscarded ? FORM_DISCARDED_CLAUSE : undefined,\n `${toolName} mutates external state and is gated. It was NOT executed. Re-call ${toolName} with the same arguments plus \"confirm\": true and this \"confirmToken\" to execute.`,\n notices.hasProvider ? undefined : NO_FORM_CLAUSE,\n ]\n .filter((clause) => {\n return clause !== undefined\n })\n .join(' ')\n\n return softStop({\n status: 'confirmation_required',\n tool: toolName,\n resolvedArgs,\n confirmToken,\n // A field and not only prose: an agent reacts to `formDiscarded` programmatically, for the same\n // reason `status` and `reason` are fields here rather than sentences.\n formDiscarded: notices.formDiscarded,\n message,\n })\n}\n\n/**\n * The form was answered with anything other than an accept. Terminal, and deliberately NOT a second\n * form: re-issuing one on a decline is an infinite prompt loop with the human's \"no\" as its engine.\n */\nconst buildFormDeclined = (toolName: string, action: FormAction): ToolsExecutionResult => {\n return softStop({\n status: 'form_declined',\n tool: toolName,\n action,\n message: `${toolName} was NOT executed: the argument form came back as \"${action}\". No confirmation is pending \u2014 call ${toolName} again to start over.`,\n })\n}\n\n/** Why an accepted form's content was thrown away on the ungated path. */\ntype FormDiscardReason = 'validation' | 'narrowed'\n\nconst DISCARD_TEXT: Record<FormDiscardReason, string> = {\n validation: 'failed validation',\n narrowed: 'narrowed the arguments',\n}\n\n/**\n * The ungated counterpart of the gate's `formDiscarded` notice. On a gated tool a discard still has\n * somewhere to go \u2014 the gate, carrying the round-1 arguments for approval. On an ungated tool there\n * is no gate, and running the round-1 arguments would silently execute the values the human never\n * saw in place of the ones they chose. Terminal, and never a second form: the human already answered.\n */\nconst buildFormDiscarded = (toolName: string, reason: FormDiscardReason): ToolsExecutionResult => {\n return softStop({\n status: 'form_discarded',\n tool: toolName,\n reason,\n message: `${toolName} was NOT executed: the values you submitted in the form ${DISCARD_TEXT[reason]} and were DISCARDED. No confirmation is pending \u2014 call ${toolName} again to start over.`,\n })\n}\n\nconst REFUSAL_TEXT: Record<ConfirmRefusal, string> = {\n absent: 'no \"confirmToken\" was supplied',\n malformed: 'the \"confirmToken\" is malformed',\n mac: 'the \"confirmToken\" was not issued by this server',\n expired: 'the \"confirmToken\" has expired',\n bind: 'the \"confirmToken\" was issued for a different tool',\n mismatch: 'the arguments differ from the ones the \"confirmToken\" was issued for',\n}\n\n/** The round-2 refusal: a terminal stop, deliberately NOT a second gate. */\nconst buildConfirmRefusal = (toolName: string, reason: ConfirmRefusal): ToolsExecutionResult => {\n return softStop({\n status: 'confirmation_refused',\n tool: toolName,\n reason,\n message: `${toolName} was NOT executed: ${REFUSAL_TEXT[reason]}. Call ${toolName} again WITHOUT \"confirm\" to receive a fresh gate, then re-call with the same arguments plus \"confirm\": true and the returned \"confirmToken\".`,\n })\n}\n\n/** Everything the stop resolution below needs from the tool's registration. */\ninterface StopDeps {\n toolName: string\n codec: ConfirmCodec\n requiresHumanConfirm: boolean | undefined\n formProvider: ArgumentFormProvider | undefined\n getClientCapabilities: ((ctx: ToolCallContext | undefined) => ClientCapabilities | undefined) | undefined\n formDeadlineMs: number\n}\n\n/** The arguments the gate will bind its token to, and whether they replaced a discarded selection. */\ninterface GateArgs {\n params: unknown\n formDiscarded: boolean\n}\n\n/**\n * Runs one form PREDICATE, treating any throw as `false`.\n *\n * `isFormable` and the capability probe are the two calls that decide whether a form is even\n * offered, and both run OUTSIDE the wraps `argument-form.ts` puts around `buildRequestedSchema`\n * and `toArgs`. Unwrapped, a provider that throws here escapes through the handler's outer catch\n * as a TOOL ERROR on a call that was owed a GATE \u2014 the exact failure those wraps exist to prevent,\n * reached one step earlier.\n *\n * `types.ts` states a never-throws contract for `isFormable`, but a contract is not a mechanism:\n * it is enforced per provider, which is the residual class the chokepoint's non-narrowing check was\n * adopted to remove. Failing to `false` here means the worst a broken predicate can do is decline a\n * form the human would have seen \u2014 never skip the gate, never reach the handler.\n */\nconst tryPredicate = (read: () => boolean): boolean => {\n try {\n return read()\n } catch {\n return false\n }\n}\n\n/**\n * The arguments a gate reached from an ACCEPTED form should carry. The round-1 arguments on every\n * path except one: a validated, merged, non-narrowing result.\n *\n * A failure here never goes back to the form \u2014 that is the re-prompt loop through a second door \u2014\n * and it never proceeds with the client's values either. It gates on what the agent originally\n * asked for, and says so.\n */\nconst resolveGateArgs = async (\n deps: StopDeps,\n params: unknown,\n responses: Record<string, unknown> | undefined,\n formAction: FormAction,\n): Promise<GateArgs> => {\n const provider = deps.formProvider\n\n if (provider === undefined || formAction !== 'accept') return { params, formDiscarded: false }\n\n const merged = await readAcceptedArgs(provider, params, responses, deps.formDeadlineMs)\n\n if (merged === null) {\n logger.info({ msg: `Tool execution form discarded (validation): ${deps.toolName}` })\n\n return { params, formDiscarded: true }\n }\n\n // Compared against `stripGateKeys(params)` \u2014 the same normalization the token is minted over, so\n // the check and the signature are looking at exactly one thing.\n if (narrowsArgs(stripGateKeys(params), merged)) {\n logger.info({ msg: `Tool execution form discarded (narrowed): ${deps.toolName}` })\n\n return { params, formDiscarded: true }\n }\n\n return { params: merged, formDiscarded: false }\n}\n\n/**\n * The chokepoint's verdict: either a result that answers the call in the handler's place, or the\n * arguments the handler runs with. A union rather than `null`-means-run because the ungated form\n * path runs with MERGED arguments, not the ones the call arrived with.\n */\ntype StopResolution =\n { kind: 'stop'; result: ToolsExecutionResult | InputRequiredResult } | { kind: 'run'; params: unknown }\n\nconst stopWith = (result: ToolsExecutionResult | InputRequiredResult): StopResolution => {\n return { kind: 'stop', result }\n}\n\nconst runWith = (params: unknown): StopResolution => {\n return { kind: 'run', params }\n}\n\n/** State `gate`, and the landing for a gated `form` that could not be built. */\nconst buildGate = async (\n deps: StopDeps,\n params: unknown,\n responses: Record<string, unknown> | undefined,\n formAction: FormAction,\n): Promise<ToolsExecutionResult> => {\n const gateArgs = await resolveGateArgs(deps, params, responses, formAction)\n\n logger.info({ msg: `Tool execution gated (awaiting confirm): ${deps.toolName}` })\n\n return await buildConfirmGate(deps.codec, deps.toolName, gateArgs.params, {\n formDiscarded: gateArgs.formDiscarded,\n hasProvider: deps.formProvider !== undefined,\n })\n}\n\n/**\n * State `form` \u2014 the one candidate that can decline itself: a provider that rejects, outruns its\n * deadline, resolves `null`, or hands `elicit()` a shape it cannot express falls through here with\n * no thrown error \u2014 to the gate on a gated tool, to the handler with the round-1 arguments on an\n * ungated one (where the handler, not the chokepoint, decides whether those arguments are enough).\n */\nconst resolveForm = async (\n deps: StopDeps,\n params: unknown,\n responses: Record<string, unknown> | undefined,\n formAction: FormAction,\n gated: boolean,\n): Promise<StopResolution> => {\n const form =\n deps.formProvider === undefined ? null : await buildArgumentForm(deps.formProvider, params, deps.formDeadlineMs)\n\n if (form !== null) {\n logger.info({ msg: `Tool execution form requested: ${deps.toolName}` })\n\n return stopWith(form)\n }\n\n logger.info({ msg: `Tool execution form unavailable: ${deps.toolName}` })\n\n return gated ? stopWith(await buildGate(deps, params, responses, formAction)) : runWith(params)\n}\n\n/**\n * State `run-form`: an ungated tool's accepted form. What runs is the provider's merge of content\n * validated against a schema built on THIS request \u2014 never the round-1 arguments after a discard,\n * and never a second form. The non-narrowing check is a chokepoint invariant kept for future\n * providers; a merge that only ever ADDS a key cannot trip it.\n */\nconst resolveUngatedForm = async (\n deps: StopDeps,\n params: unknown,\n responses: Record<string, unknown> | undefined,\n): Promise<StopResolution> => {\n const provider = deps.formProvider\n\n // U3 spells `hasProvider`, so this narrows for the types only; `run` is what U4 answers without one.\n if (provider === undefined) return runWith(params)\n\n const merged = await readAcceptedArgs(provider, params, responses, deps.formDeadlineMs)\n\n if (merged === null) {\n logger.info({ msg: `Tool execution form discarded (validation): ${deps.toolName}` })\n\n return stopWith(buildFormDiscarded(deps.toolName, 'validation'))\n }\n\n if (narrowsArgs(stripGateKeys(params), merged)) {\n logger.info({ msg: `Tool execution form discarded (narrowed): ${deps.toolName}` })\n\n return stopWith(buildFormDiscarded(deps.toolName, 'narrowed'))\n }\n\n logger.info({ msg: `Tool execution form accepted: ${deps.toolName}` })\n\n return runWith(merged)\n}\n\n/** State `verify`: round 2 of the gate. */\nconst resolveVerify = async (deps: StopDeps, params: unknown): Promise<StopResolution> => {\n const verdict = await verifyConfirmToken(deps.codec, deps.toolName, params)\n\n if (verdict.ok) return runWith(params)\n\n logger.info({ msg: `Tool execution refused (${verdict.reason}): ${deps.toolName}` })\n\n return stopWith(buildConfirmRefusal(deps.toolName, verdict.reason))\n}\n\n/**\n * The chokepoint's answer for a call.\n *\n * @example\n * await resolveStop(deps, { env: 'prod' }, undefined) // => { kind: 'stop', result: <the round-1 confirm gate> }\n */\nconst resolveStop = async (\n deps: StopDeps,\n params: unknown,\n ctx: ToolCallContext | undefined,\n): Promise<StopResolution> => {\n const responses = ctx?.mcpReq?.inputResponses\n const formAction = readFormAction(responses)\n // Spelled `=== true` on purpose: `mcp-confirm-gate-mutation.test.ts` neuters exactly this\n // predicate at build time to prove the gate's e2e assertions are load-bearing.\n const gated = deps.requiresHumanConfirm === true\n const state = resolveGateState({\n gated,\n confirmed: isConfirmed(params),\n responses,\n // `caps?.elicitation?.form`, NEVER `caps?.elicitation`: the SDK normalizes a bare\n // `{elicitation:{}}` to `{elicitation:{form:{}}}`, so both spellings agree on every fixture\n // except a url-only client \u2014 which is exactly the client that must NOT be offered a form.\n canForm: tryPredicate(() => {\n return deps.getClientCapabilities?.(ctx)?.elicitation?.form !== undefined\n }),\n hasProvider: deps.formProvider !== undefined,\n formable: tryPredicate(() => {\n return deps.formProvider?.isFormable(params) === true\n }),\n accepted: formAction === 'accept',\n })\n\n switch (state) {\n case 'run': {\n return runWith(params)\n }\n case 'declined': {\n logger.info({ msg: `Tool execution form declined (${formAction}): ${deps.toolName}` })\n\n return stopWith(buildFormDeclined(deps.toolName, formAction))\n }\n case 'verify': {\n return await resolveVerify(deps, params)\n }\n case 'run-form': {\n return await resolveUngatedForm(deps, params, responses)\n }\n case 'form': {\n return await resolveForm(deps, params, responses, formAction, gated)\n }\n case 'gate': {\n return stopWith(await buildGate(deps, params, responses, formAction))\n }\n default: {\n return assertNever(state)\n }\n }\n}\n\n// The return is a union because the chokepoint can now answer a call with an `InputRequiredResult` \u2014\n// the SDK's \"I need input from the human before I can run\" reply \u2014 as well as with a tool result.\n// It typechecks at the registration site because `ToolCallback` already returns\n// `CallToolResult | InputRequiredResult`.\n//\n// Deliberately UNCONDITIONAL. Narrowing it back for callers that pass no `formProvider` would be\n// asserting \"no provider means a form is unreachable\" \u2014 true only while the state machine keeps\n// requiring a provider, and enforced by nothing if that ever changes. Every caller narrows the\n// union explicitly instead.\nexport const createToolHandler = ({\n toolName,\n handler,\n requiresHumanConfirm,\n formProvider,\n getClientCapabilities,\n confirmCodec,\n formDeadlineMs,\n applySessionEnv = applySessionEnvFromFile,\n}: ToolHandlerArgs): ((\n params: unknown,\n ctx?: ToolCallContext,\n) => Promise<ToolsExecutionResult | InputRequiredResult>) => {\n const deps: StopDeps = {\n toolName,\n codec: confirmCodec ?? getDefaultConfirmCodec(),\n requiresHumanConfirm,\n formProvider,\n getClientCapabilities,\n formDeadlineMs: formDeadlineMs ?? FORM_DEADLINE_MS,\n }\n\n // `ctx` is bound HERE, in the returned closure, and not merely declared on the exported type. The\n // SDK passes it on every call, so a closure that omits the parameter discards it silently while\n // the signature keeps advertising it \u2014 the gap survived PR 1 unnoticed for exactly that reason.\n return async (params: unknown, ctx?: ToolCallContext) => {\n logger.info({ msg: `Tool execution started: ${toolName}`, params, sessionId: ctx?.sessionId })\n try {\n // MCP entry-boundary seed. This wrapper is the sole chokepoint for every exposed tool\n // (`src/mcp/tools/index.ts` holds the only `registerTool` call), and the `mcp` command itself is\n // in program.ts's SEED_EXCLUDED \u2014 so an MCP server seeds lazily, on its FIRST tool invocation,\n // and a server that never receives one never writes to $HOME. The once-per-process guard inside\n // ensureUserProjectConfig makes calls 2..N free on this long-lived process.\n //\n // Deliberately INSIDE the try, not before it: defense-in-depth. ensureUserProjectConfig is\n // contractually never-throw, but if that contract ever broke, an unhandled rejection here would\n // take down the long-lived server. Inside the try it degrades to an ordinary tool error.\n await ensureUserProjectConfig()\n\n // The command handlers record their resolved flags into the `commandEcho` singleton. On the CLI,\n // Commander's `preAction` clears it before every command; this long-lived server never runs that\n // hook, so without a reset here one tool call's flags would leak into the next one's snapshot and\n // the options array would grow for the life of the process.\n commandEcho.reset()\n\n // The env-load file lands mid-session, and every read from here on is `process.env` at call\n // time (Jira, the INFRA_KIT_ENV_* markers, the env children inherit). Applied at the call's\n // ENTRY so the gate, the form and the handler all run under one environment; synchronous so\n // a concurrently dispatched call cannot see a half-applied one.\n applySessionEnv()\n\n // Orthogonal destructive-op confirm gate, and the argument form. Both sit BEFORE the handler and\n // are INDEPENDENT of the `confirmedCommand:true` injected below \u2014 that flag is a prompt-skip /\n // behavior discriminator (e.g. worktrees-remove keys `allowEditorRelaunch` off it) and MUST keep\n // being injected on the real call, or the non-TTY server would hang on an inquirer prompt and\n // the Zed relaunch would re-enable.\n const resolution = await resolveStop(deps, params, ctx)\n\n if (resolution.kind === 'stop') return resolution.result\n\n const payload = await handler({ ...(resolution.params as object), confirmedCommand: true })\n\n logger.info({ msg: `Tool execution successful: ${toolName}` })\n\n return payload\n } catch (error) {\n logger.error({\n err: error,\n params,\n msg: `Tool execution failed: ${toolName}`,\n })\n\n throw error\n }\n }\n}\n"],
5
- "mappings": "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",
6
- "names": ["serveStdio", "process", "delay", "process", "setupErrorHandlers", "logger", "process", "error", "LOG_FILE_PATH", "reason", "promise", "McpServer", "CONFIG_RESOURCE_URI", "DEV_CONTEXT_RESOURCE_URI", "SESSION_WORKFLOW_URI", "SESSION_WORKFLOW_STUB", "defaultDeps", "getInfraKitConfig", "readDevContext", "jsonResource", "uri", "value", "initializeResources", "server", "deps", "error", "CLIENT_CAPABILITIES_META_KEY", "z", "createRequestStateCodec", "randomBytes", "GATE_KEYS", "CONFIRM_TOKEN_TTL_SECONDS", "asBindContext", "toolName", "sortKeysDeep", "value", "record", "key", "canonicalArgs", "args", "stripGateKeys", "params", "createConfirmCodec", "options", "codec", "ctx", "payload", "token", "defaultCodec", "getDefaultConfirmCodec", "readConfirmToken", "confirmToken", "REFUSALS_FROM_CODEC", "codecRefusal", "error", "message", "mintConfirmToken", "verifyConfirmToken", "fs", "path", "process", "NEVER_OVERLAID", "PROTECTED_CHILD_ENV_NAMES", "INFRA_KIT_SESSION_VAR", "baseline", "touched", "lastSignature", "noSession", "statOrNull", "file", "fs", "resolveSessionDir", "noSession", "getSessionCacheDir", "logger", "INFRA_KIT_SESSION_VAR", "chooseSourcedFile", "dir", "loadPath", "path", "ENV_LOAD_FILE", "clearPath", "ENV_CLEAR_FILE", "load", "clear", "parseSourcedFile", "sourced", "parseVarsFromEnvFile", "parseUnsetNamesFromEnvFile", "readSessionEnvStateIfMoved", "dir", "resolveSessionDir", "sourced", "chooseSourcedFile", "lastSignature", "parseSourcedFile", "restoreTouched", "name", "touched", "original", "baseline", "process", "assign", "overlay", "value", "NEVER_OVERLAID", "remove", "overlayState", "state", "INFRA_KIT_ENV_CLEARED_VAR", "describeState", "applySessionEnv", "set", "unset", "skipped", "logger", "acceptedContent", "inputRequired", "inputResponse", "FORM_KEY", "FORM_DEADLINE_MS", "readFormAction", "responses", "view", "inputResponse", "trySchema", "provider", "params", "deadlineMs", "withDeadline", "buildArgumentForm", "schema", "inputRequired", "readAcceptedArgs", "content", "acceptedContent", "isRecord", "value", "narrowsArgs", "before", "after", "key", "replacement", "isConfirmed", "params", "resolveGateState", "input", "softStop", "structuredContent", "textContent", "FORM_DISCARDED_CLAUSE", "NO_FORM_CLAUSE", "buildConfirmGate", "codec", "toolName", "notices", "resolvedArgs", "stripGateKeys", "confirmToken", "mintConfirmToken", "message", "clause", "buildFormDeclined", "action", "DISCARD_TEXT", "buildFormDiscarded", "reason", "REFUSAL_TEXT", "buildConfirmRefusal", "tryPredicate", "read", "resolveGateArgs", "deps", "responses", "formAction", "provider", "merged", "readAcceptedArgs", "logger", "narrowsArgs", "stopWith", "result", "runWith", "buildGate", "gateArgs", "resolveForm", "gated", "form", "buildArgumentForm", "resolveUngatedForm", "resolveVerify", "verdict", "verifyConfirmToken", "resolveStop", "ctx", "readFormAction", "state", "assertNever", "createToolHandler", "handler", "requiresHumanConfirm", "formProvider", "getClientCapabilities", "confirmCodec", "formDeadlineMs", "applySessionEnv", "getDefaultConfirmCodec", "FORM_DEADLINE_MS", "ensureUserProjectConfig", "commandEcho", "resolution", "payload", "error", "initializeTools", "server", "tool", "getExposedMcpTools", "z", "withConfirmToken", "wrapForRegistration", "createToolHandler", "ctx", "CLIENT_CAPABILITIES_META_KEY", "shape", "handle", "params", "createMcpServer", "mcpMode", "server", "McpServer", "package_default", "initializeResources", "initializeTools", "suppressTypelessPackageJsonWarning", "logger", "initLoggerMcp", "buildOrDie", "createMcpServer", "error", "process", "setupErrorHandlers", "handle", "serveStdio", "isShuttingDown", "shutdown", "signal", "delay"]
4
+ "sourcesContent": ["import { serveStdio } from '@modelcontextprotocol/server/stdio'\nimport process from 'node:process'\nimport { setTimeout as delay } from 'node:timers/promises'\n\nimport { setupErrorHandlers } from 'src/lib/error-handlers'\nimport { initLoggerMcp } from 'src/lib/logger'\nimport { suppressTypelessPackageJsonWarning } from 'src/lib/node-warnings'\n\nimport { createMcpServer } from '../mcp/server'\n\n// The MCP tools run the same commands as the CLI, so they hit the same consumer `.ts` configs.\n// This keeps the client's log pane clean; it cannot affect protocol framing either way, since\n// the stdio transport frames JSON-RPC on stdout and `emitWarning` only ever writes to stderr.\nsuppressTypelessPackageJsonWarning()\n\nconst logger = initLoggerMcp()\n\n/**\n * `serveStdio` calls this LAZILY, on the first inbound message, and may call it TWICE on one\n * connection via the probe-then-legacy discard path. The factory must therefore stay free of\n * non-idempotent process-scope side effects.\n */\nconst buildOrDie = async () => {\n try {\n return await createMcpServer()\n } catch (error) {\n logger.error({ err: error, msg: 'Failed to create MCP server' })\n logger.flush()\n\n process.exit(1)\n }\n}\n\nsetupErrorHandlers(logger)\n\nconst handle = serveStdio(buildOrDie, {\n onerror: (error) => {\n logger.error({ err: error, msg: 'MCP stdio entry error' })\n logger.flush()\n },\n})\n\n// NOT \"listening\": `serveStdio` returns while the transport start is still pending, so nothing is\n// established at this point beyond the entry having been wired up. Claiming readiness here would\n// print \"listening\" on a transport that failed to start.\nlogger.info({ msg: 'MCP stdio entry started.' })\n\nlet isShuttingDown = false\n\nconst shutdown = async (signal: NodeJS.Signals) => {\n // A second signal inside the 1500 ms window must not start a second teardown.\n if (isShuttingDown) {\n return\n }\n\n isShuttingDown = true\n\n logger.info({ msg: `Received ${signal}. Shutting down...` })\n\n // Exit 0 is unconditional: a rejecting close() must not escape to `unhandledRejection` and turn a\n // clean shutdown into exit 1. The SDK already routes close errors to `onerror`; this is the belt.\n try {\n await Promise.race([handle.close(), delay(1500)])\n } catch (error) {\n logger.error({ err: error, msg: 'MCP stdio close failed during shutdown' })\n }\n\n logger.flush()\n\n process.exit(0)\n}\n\nprocess.on('SIGINT', () => {\n void shutdown('SIGINT')\n})\n\nprocess.on('SIGTERM', () => {\n void shutdown('SIGTERM')\n})\n", "import process from 'node:process'\nimport type { Logger } from 'pino'\n\nimport { LOG_FILE_PATH } from '../logger/index'\n\n/**\n * Setup handlers for fatal, non-signal process events.\n *\n * @param logger - The logger instance\n *\n * ONLY FOR SERVER!\n *\n * Signal handling deliberately does NOT live here: a bounded teardown needs the\n * `StdioServerHandle` returned by `serveStdio`, which only the MCP entry holds, so\n * `SIGINT`/`SIGTERM` are registered in `src/entry/mcp.ts` instead. The arms removed from\n * here called `process.exit(0)` without flushing the logger, discarding the final log\n * lines on every clean shutdown; the entry's `shutdown` flushes before exiting.\n */\nexport const setupErrorHandlers = (logger: Logger) => {\n process.on('uncaughtException', (error) => {\n logger.fatal({ err: error, msg: 'Uncaught Exception' })\n logger.error(`Uncaught Exception! Check ${LOG_FILE_PATH}. Shutting down...`)\n logger.flush()\n process.exit(1)\n })\n\n process.on('unhandledRejection', (reason, promise) => {\n logger.fatal({ reason, promise, msg: 'Unhandled Rejection' })\n logger.error(`Unhandled Rejection! Check ${LOG_FILE_PATH}. Shutting down...`)\n logger.flush()\n process.exit(1)\n })\n}\n", "import { McpServer } from '@modelcontextprotocol/server'\n\nimport { agentMode } from 'src/lib/agent-mode'\n\nimport packageJson from '../../package.json' with { type: 'json' }\nimport { initializeResources } from './resources'\nimport { initializeTools } from './tools'\n\nexport async function createMcpServer() {\n // Marks `process.stdin` as the JSON-RPC transport for every prompt guard in the\n // process. Set HERE rather than in `entry/mcp.ts` (the sole caller) because that\n // module starts a server at import time and so can never be unit-tested \u2014 a test\n // there could only stub the flag, which proves nothing about it ever being set.\n // This runs before `server.connect`, so no tool handler can outrun it.\n agentMode.source = 'mcp'\n\n const server = new McpServer(\n {\n name: 'infra-kit',\n version: packageJson.version,\n },\n {\n capabilities: {\n // `initializeResources` registers read-only resources below; the SDK's\n // `registerResource` also declares `resources.listChanged` on top of this.\n resources: { listChanged: true },\n tools: {},\n // Deliberately no `prompts`. Every procedure's surface is its plugin skill;\n // a prompt registered here rendered a duplicate `/infra-kit:release-create (MCP)`\n // row next to it (see docs/release-create-prompt-removal-plan.md).\n },\n },\n )\n\n await initializeResources(server)\n await initializeTools(server)\n\n return server\n}\n", "import type { McpServer } from '@modelcontextprotocol/server'\n\nimport type { InfraKitConfig } from 'src/lib/infra-kit-config'\nimport { getInfraKitConfig } from 'src/lib/infra-kit-config'\n\nimport type { DevContextSnapshot } from './dev-context'\nimport { readDevContext } from './dev-context'\n\n/** Stable URI of the merged-config resource. */\nexport const CONFIG_RESOURCE_URI = 'infra-kit://config'\n\n/** Stable URI of the dev-context resource. */\nexport const DEV_CONTEXT_RESOURCE_URI = 'infra-kit://dev-context'\n\n/**\n * URI of the retired `session` procedure \u2014 served as a three-line deprecation stub, not the procedure.\n *\n * The procedure moved to the plugin's `/infra-kit:session` skill (docs/session-env-picker-plan.md \u00A73.6).\n * The plugin is git-sourced and unpinned, so a consumer can run this CLI against a plugin whose old\n * `/infra-kit:session` command still reads this URI FIRST: the stub keeps that command on the form path\n * (`env-load` without `config`) instead of its 404 fallback. `release-create` and `setup` needed no stub \u2014\n * their commands' fallbacks call the tool directly. Leaves in the release after next.\n */\nexport const SESSION_WORKFLOW_URI = 'infra-kit://workflow/session'\n\nconst SESSION_WORKFLOW_STUB = [\n 'This procedure moved to the `/infra-kit:session` skill; update the plugin.',\n \"Meanwhile: no token \u2192 call `env-load` without `config`; the human's pick is the load.\",\n 'On a tool error or a refused result naming `config`, call `env-list` and list every row in prose, then ask.',\n].join('\\n')\n\n/**\n * The two disk reads the resources need, injected so the registration is unit-testable without touching\n * the filesystem or the config-loader's mtime cache. Production uses the real readers by default.\n */\nexport interface ResourceDeps {\n /** Loads the merged `infra-kit.json` (all override layers applied). May throw when no config exists. */\n loadConfig: () => Promise<InfraKitConfig>\n /** Reads the dev-context fragments `infra-kit dev` last wrote. Never throws; absent session \u21D2 empty. */\n readDevContext: () => DevContextSnapshot\n}\n\nconst defaultDeps: ResourceDeps = {\n loadConfig: getInfraKitConfig,\n readDevContext: () => {\n return readDevContext()\n },\n}\n\n/** Serialize a resource body as pretty JSON text \u2014 the wire form every MCP client can read. */\nconst jsonResource = (uri: string, value: unknown): { contents: { uri: string; mimeType: string; text: string }[] } => {\n return { contents: [{ uri, mimeType: 'application/json', text: JSON.stringify(value, null, 2) }] }\n}\n\n/**\n * Register infra-kit's READ-ONLY MCP resources so an agent can inspect the repo's config and dev state\n * without calling a tool (or mutating anything):\n *\n * - `infra-kit://config` \u2014 the merged `infra-kit.json` an agent would run against.\n * - `infra-kit://dev-context` \u2014 what `infra-kit dev` last wrote (backends up, ports, freshness); an\n * absent dev session resolves to an empty `session: 'none'` payload, NOT\n * an error.\n *\n * Both handlers are side-effect free and swallow their reader's failure into an `{ error }` payload rather\n * than throwing, so a bad on-disk config can never crash the long-lived MCP server.\n */\nexport const initializeResources = async (server: McpServer, deps: ResourceDeps = defaultDeps) => {\n server.registerResource(\n 'infra-kit-config',\n CONFIG_RESOURCE_URI,\n {\n title: 'infra-kit config',\n description: 'The merged infra-kit.json configuration (all override layers applied). Read-only.',\n mimeType: 'application/json',\n },\n async (uri) => {\n try {\n return jsonResource(uri.toString(), await deps.loadConfig())\n } catch (error) {\n return jsonResource(uri.toString(), { error: error instanceof Error ? error.message : String(error) })\n }\n },\n )\n\n server.registerResource(\n 'infra-kit-dev-context',\n DEV_CONTEXT_RESOURCE_URI,\n {\n title: 'infra-kit dev context',\n description:\n 'What `infra-kit dev` last wrote: backends currently up, their ports/origins, and fragment ' +\n 'freshness. Read-only and advisory (no liveness probe). No active session resolves to an empty payload.',\n mimeType: 'application/json',\n },\n // The SDK's `ReadResourceCallback` accepts a synchronous return; the disk read is sync, so no `async`.\n (uri) => {\n return jsonResource(uri.toString(), deps.readDevContext())\n },\n )\n\n server.registerResource(\n 'infra-kit-workflow-session',\n SESSION_WORKFLOW_URI,\n {\n title: 'session procedure (moved)',\n description:\n 'Retired: the session procedure now lives in the /infra-kit:session plugin skill. Update the plugin.',\n mimeType: 'text/markdown',\n },\n (uri) => {\n return { contents: [{ uri: uri.toString(), mimeType: 'text/markdown', text: SESSION_WORKFLOW_STUB }] }\n },\n )\n}\n", "import { CLIENT_CAPABILITIES_META_KEY } from '@modelcontextprotocol/server'\nimport type { ClientCapabilities, McpServer } from '@modelcontextprotocol/server'\nimport { z } from 'zod'\n\nimport { getExposedMcpTools } from 'src/lib/command-catalog'\nimport { createToolHandler } from 'src/lib/tool-handler'\nimport type { ToolCallContext } from 'src/lib/tool-handler'\n\nexport const initializeTools = async (server: McpServer) => {\n // The registered tool set is derived from the single command catalog, filtered\n // by its explicit `mcpExposed` allowlist. Nothing is added or subtracted here \u2014\n // an exposure decision that is not visible in the catalog is invisible to the\n // catalog's own gates, so this loop stays a plain projection of it.\n //\n // `doctor` IS exposed, and it is the one entry whose safety does not come from\n // those gates: it is `mutating: false` (read-only, ungated) purely because\n // `doctorMcpTool` is nullary and forwards nothing, so the CLI's `--fix` write path\n // is unreachable from MCP by construction. The P1 fail-closed gate only inspects\n // `mutating && mcpExposed` entries, so it cannot see that claim go false \u2014 the\n // guard is `src/commands/doctor/__tests__/doctor-mcp-surface.test.ts`, not this list.\n for (const tool of getExposedMcpTools()) {\n server.registerTool(\n tool.name,\n {\n // Display label; hosts fall back to `name` when absent. TOP-LEVEL `title` only \u2014\n // `annotations.title` is deliberately left unset, because some hosts prefer the latter when\n // present and two titles are a divergence waiting to happen.\n title: tool.title,\n description: tool.description,\n // Wrapped HERE, at the single `registerTool` call site, rather than in the 27\n // `defineMcpTool` definitions or in `CatalogMcpTool`. SDK v2 offers a deprecated\n // raw-shape overload that would auto-wrap, but it types the shape as its own\n // `ZodRawShape` (`Record<string, ZodType>`) while zod 4's `z.ZodRawShape` is the\n // looser `Readonly<{ [k: string]: $ZodType }>` \u2014 `$ZodType` lacks `def`/`type`/`_def`\n // and ~42 other members, so the raw shape is NOT assignable and the overload is\n // unreachable for us. `z.object()` closes that gap and lands on v2's PREFERRED\n // (non-deprecated) Standard Schema overload, since a `ZodObject` carries `~standard`.\n //\n // Wrapping here keeps the authoring shape in all 27 `src/commands/**` definitions\n // unchanged and leaves `defineMcpTool`'s `z.infer<z.ZodObject<TIn>>` handler typing\n // untouched \u2014 the migration needs zero edits under `src/commands/`.\n //\n // Gated tools additionally accept `confirmToken` here, at the boundary: `z.object` strips\n // undeclared keys, so without this the round-2 token the gate hands out would never reach\n // `createToolHandler`'s verification and every confirmation would be refused as absent.\n inputSchema: z.object(\n tool.requiresHumanConfirm === true ? withConfirmToken(tool.inputSchema) : tool.inputSchema,\n ),\n outputSchema: z.object(tool.outputSchema),\n // ADVISORY protocol hints, derived in the catalog from `mutating`. The authority for\n // destructive operations remains `requiresHumanConfirm` + `lib/tool-handler`'s confirm gate;\n // nothing in that gate reads these, and the spec forbids a client treating them as security.\n annotations: tool.annotations,\n // `anthropic/requiresUserInteraction` rides here on `setup`, the one tool that carries it. It is the\n // only human gate on the MCP path: the host prompts on EVERY call, in `bypassPermissions` too,\n // and an allow rule cannot skip it. Undefined for every other tool, which is the same as absent.\n _meta: tool.meta,\n },\n // The SDK's second callback argument (session, request state, signal) is threaded through so\n // the handler signature lands once; the gate binds tokens by tool name and needs none of it.\n wrapForRegistration(\n createToolHandler({\n toolName: tool.name,\n handler: tool.handler,\n requiresHumanConfirm: tool.requiresHumanConfirm,\n formProvider: tool.formProvider,\n // `server.server` is the underlying protocol instance the McpServer facade wraps; the\n // integration test in `mcp/__tests__/server.test.ts` reaches through it the same way.\n //\n // Envelope FIRST, accessor second. On a 2026-era connection the capabilities ride in each\n // request's envelope, and over stdio nothing ever seeds the server-level accessor from it\n // (the SDK does that only in its HTTP entry) \u2014 so the accessor alone read `undefined` on\n // every modern-era stdio call and no form was ever offered there. The accessor still answers\n // for a 2025-era connection, whose requests carry no envelope.\n getClientCapabilities: (ctx) => {\n const fromEnvelope = ctx?.mcpReq?.envelope?.[CLIENT_CAPABILITIES_META_KEY] as ClientCapabilities | undefined\n\n return fromEnvelope ?? server.server.getClientCapabilities()\n },\n }),\n ),\n )\n }\n}\n\nconst withConfirmToken = (shape: z.ZodRawShape): z.ZodRawShape => {\n return {\n ...shape,\n confirmToken: z\n .string()\n .optional()\n .describe('Round-2 only: the token returned by the round-1 gate, proving the arguments are unchanged.'),\n }\n}\n\nconst wrapForRegistration = (handle: ReturnType<typeof createToolHandler>) => {\n return (params: unknown, ctx: ToolCallContext) => {\n return handle(params, ctx)\n }\n}\n", "import { createRequestStateCodec } from '@modelcontextprotocol/server'\nimport { randomBytes } from 'node:crypto'\n\n/**\n * @fileoverview\n *\n * Round-1 gate payload \u2192 round-2 binding for the destructive-op confirm gate.\n *\n * Round 1 mints a `confirmToken` over the tool name and the CANONICAL form of the\n * arguments; round 2 must present that token together with the same arguments, or\n * the tool does not run.\n *\n * Without the binding an agent could obtain a gate for `env:dev` and re-call with\n * `confirm:true` for `env:prod` \u2014 the substitution hole\n * `docs/infra-kit-slash-commands-plan.md` \u00A70.12 records.\n */\n\n/** Keys the gate itself owns; they are never part of the signed argument set. */\nconst GATE_KEYS = new Set(['confirm', 'confirmToken'])\n\n/** Default token lifetime. Long enough for a human to read the gate, short enough to bound replay. */\nexport const CONFIRM_TOKEN_TTL_SECONDS = 600\n\n/** What a round-1 gate signs. `args` is the canonical JSON of the tool arguments. */\nexport interface ConfirmPayload {\n args: string\n}\n\n/** The subset of the SDK codec the gate uses; `ctx` is the tool name the token is bound to. */\nexport interface ConfirmCodec {\n mint: (payload: ConfirmPayload, toolName: string) => Promise<string>\n verify: (token: string, toolName: string) => Promise<ConfirmPayload>\n}\n\nexport type ConfirmRefusal = 'absent' | 'malformed' | 'mac' | 'expired' | 'bind' | 'mismatch'\n\ninterface BindContext {\n toolName: string\n}\n\ntype SdkBindContext = NonNullable<Parameters<ReturnType<typeof createRequestStateCodec>['mint']>[1]>\n\nconst asBindContext = (toolName: string): SdkBindContext => {\n return { toolName } as unknown as SdkBindContext\n}\n\nexport type ConfirmVerdict = { ok: true } | { ok: false; reason: ConfirmRefusal }\n\nconst sortKeysDeep = (value: unknown): unknown => {\n if (Array.isArray(value)) {\n return value.map(sortKeysDeep)\n }\n\n if (value !== null && typeof value === 'object') {\n const record = value as Record<string, unknown>\n\n return Object.fromEntries(\n Object.keys(record)\n .sort()\n .map((key) => {\n return [key, sortKeysDeep(record[key])]\n }),\n )\n }\n\n return value\n}\n\n/**\n * Stable JSON for an argument object: keys sorted recursively, arrays kept in order.\n * `JSON.stringify` is key-order-sensitive and round-2 arguments are re-serialized by\n * the client in whatever order the host emits, so signing or verifying the raw form\n * would refuse legitimate confirmations intermittently, per client.\n *\n * @example\n * canonicalArgs({ b: 2, a: { d: 1, c: [2, 1] } }) // => '{\"a\":{\"c\":[2,1],\"d\":1},\"b\":2}'\n */\nexport const canonicalArgs = (args: unknown): string => {\n return JSON.stringify(sortKeysDeep(args))\n}\n\n/**\n * The tool's own arguments: the incoming params with the gate's keys removed. This is\n * the ONE filter both rounds share, so round 2 (which carries `confirm` and\n * `confirmToken` on top of round 1's arguments) verifies against exactly what round 1 signed.\n *\n * @example\n * stripGateKeys({ env: 'dev', confirm: true, confirmToken: 'v1.x.y' }) // => { env: 'dev' }\n */\nexport const stripGateKeys = (params: unknown): Record<string, unknown> => {\n if (typeof params !== 'object' || params === null) return {}\n\n return Object.fromEntries(\n Object.entries(params as Record<string, unknown>).filter(([key]) => {\n return !GATE_KEYS.has(key)\n }),\n )\n}\n\n/**\n * Build the gate's codec. The key is per-process and random by default: one stdio\n * server process serves both rounds of every confirmation, so a key that dies with\n * the process is sufficient \u2014 that stops being true on any HTTP transport, where the\n * key would have to be shared across workers. Tests inject a short `ttlSeconds` or a\n * second key to exercise the expiry and tamper refusals.\n *\n * @example\n * const codec = createConfirmCodec()\n * const token = await codec.mint({ args: '{\"env\":\"dev\"}' }, 'env-clear')\n * await codec.verify(token, 'env-clear') // => { args: '{\"env\":\"dev\"}' }\n */\nexport const createConfirmCodec = (options: { key?: Uint8Array; ttlSeconds?: number } = {}): ConfirmCodec => {\n // The SDK types the bind context as its own `ServerContext`; the gate only ever binds to the\n // tool name, so a one-field object stands in for it on both mint and verify.\n const codec = createRequestStateCodec<ConfirmPayload>({\n key: options.key ?? randomBytes(32),\n ttlSeconds: options.ttlSeconds ?? CONFIRM_TOKEN_TTL_SECONDS,\n bind: (ctx) => {\n return (ctx as unknown as BindContext).toolName\n },\n })\n\n return {\n mint: (payload, toolName) => {\n return codec.mint(payload, asBindContext(toolName))\n },\n verify: (token, toolName) => {\n return codec.verify(token, asBindContext(toolName))\n },\n }\n}\n\nlet defaultCodec: ConfirmCodec | undefined\n\n/** The process-wide codec every registered tool shares, created on first use. */\nexport const getDefaultConfirmCodec = (): ConfirmCodec => {\n defaultCodec ??= createConfirmCodec()\n\n return defaultCodec\n}\n\nconst readConfirmToken = (params: unknown): string | undefined => {\n if (typeof params !== 'object' || params === null) return undefined\n\n const { confirmToken } = params as { confirmToken?: unknown }\n\n return typeof confirmToken === 'string' ? confirmToken : undefined\n}\n\nconst REFUSALS_FROM_CODEC: ReadonlySet<ConfirmRefusal> = new Set(['malformed', 'mac', 'expired', 'bind'])\n\nconst codecRefusal = (error: unknown): ConfirmRefusal => {\n const message = error instanceof Error ? error.message : ''\n\n return REFUSALS_FROM_CODEC.has(message as ConfirmRefusal) ? (message as ConfirmRefusal) : 'malformed'\n}\n\n/**\n * Mint the round-1 token for `params` (gate keys excluded), bound to `toolName`.\n *\n * @example\n * await mintConfirmToken(codec, 'env-clear', { env: 'dev' }) // => 'v1.<payload>.<mac>'\n */\nexport const mintConfirmToken = async (codec: ConfirmCodec, toolName: string, params: unknown): Promise<string> => {\n return codec.mint({ args: canonicalArgs(stripGateKeys(params)) }, toolName)\n}\n\n/**\n * Decide whether a round-2 call may run. Absence is a refusal in its own right \u2014\n * \"verify only when a token is present\" would let an agent omit the token and\n * substitute arguments freely, reopening the hole this module closes.\n *\n * @example\n * await verifyConfirmToken(codec, 'env-clear', { env: 'prod', confirm: true, confirmToken })\n * // => { ok: false, reason: 'mismatch' } (the token was minted for env:dev)\n */\nexport const verifyConfirmToken = async (\n codec: ConfirmCodec,\n toolName: string,\n params: unknown,\n): Promise<ConfirmVerdict> => {\n const token = readConfirmToken(params)\n\n if (token === undefined) return { ok: false, reason: 'absent' }\n\n let payload: ConfirmPayload\n\n try {\n payload = await codec.verify(token, toolName)\n } catch (error) {\n return { ok: false, reason: codecRefusal(error) }\n }\n\n if (payload.args !== canonicalArgs(stripGateKeys(params))) return { ok: false, reason: 'mismatch' }\n\n return { ok: true }\n}\n", "import fs from 'node:fs'\nimport path from 'node:path'\nimport process from 'node:process'\n\nimport {\n ENV_CLEAR_FILE,\n ENV_LOAD_FILE,\n INFRA_KIT_ENV_CLEARED_VAR,\n INFRA_KIT_SESSION_VAR,\n getSessionCacheDir,\n parseUnsetNamesFromEnvFile,\n parseVarsFromEnvFile,\n} from 'src/lib/constants'\nimport { logger } from 'src/lib/logger'\nimport { PROTECTED_CHILD_ENV_NAMES } from 'src/lib/mcp-proxy/protected-env'\n\n/**\n * What the session dir says is loaded right now, read the way `~/.zshenv` reads it\n * (src/commands/init/init.ts): env-load.sh wins unless env-clear.sh is strictly newer.\n * `signature` identifies the file the state came from so an unchanged file is never\n * re-parsed; `no-session` carries none because nothing is ever applied for it.\n */\nexport type SessionEnvState =\n | { kind: 'load'; vars: Record<string, string>; unset: string[]; signature: string }\n | { kind: 'clear'; unset: string[]; signature: string }\n | { kind: 'none'; signature: 'none' }\n | { kind: 'no-session' }\n\nexport interface SessionEnvApplyResult {\n set: string[]\n unset: string[]\n changed: boolean\n}\n\n/**\n * Names the file may carry but the overlay must never touch, on assign or on unset.\n * The proxy's set is reused so the two overlays cannot disagree about what plumbing\n * is; the four additions decide WHICH session dir this process reads and where the\n * plugin lives \u2014 a Doppler config defining `INFRA_KIT_SESSION` would otherwise point\n * the next read at another session.\n */\nconst NEVER_OVERLAID: ReadonlySet<string> = new Set([\n ...PROTECTED_CHILD_ENV_NAMES,\n INFRA_KIT_SESSION_VAR,\n 'XDG_CACHE_HOME',\n 'CLAUDE_PROJECT_DIR',\n 'CLAUDE_PLUGIN_ROOT',\n])\n\n/**\n * Captured on the FIRST apply, not at import: unit tests build their environment\n * before calling, and the lazy stdio factory may run twice per connection, so an\n * import-time capture could freeze either the wrong env or a half-applied one.\n */\nlet baseline: NodeJS.ProcessEnv | null = null\n/** Every name the previous apply wrote or deleted \u2014 what the next apply restores first. */\nconst touched = new Set<string>()\nlet lastSignature: string | null = null\n/** Latched on the first throw of `getSessionCacheDir`: one log line per process, then silence. */\nlet noSession = false\n\nexport const resetSessionEnvForTests = (): void => {\n baseline = null\n touched.clear()\n lastSignature = null\n noSession = false\n}\n\nconst statOrNull = (file: string): fs.Stats | null => {\n try {\n return fs.statSync(file)\n } catch {\n return null\n }\n}\n\n/**\n * Unlike the proxy's `NO_SESSION` fallback this never reads a `no-session` dir:\n * `env-load` over MCP throws in the same situation, so nothing could have written one.\n */\nconst resolveSessionDir = (): string | null => {\n if (noSession) return null\n\n try {\n return getSessionCacheDir()\n } catch {\n noSession = true\n logger.warn(`session-env: ${INFRA_KIT_SESSION_VAR} unset \u2014 no overlay`)\n\n return null\n }\n}\n\n/**\n * Which file the shell would source, decided from the stat pair alone so the signature\n * exists BEFORE anything is parsed. `atomicWriteFileSync` renames a fresh temp file into\n * place, so every write lands a new inode and `ino` makes the signature exact.\n */\ntype SourcedFile =\n | { kind: 'load'; file: string; signature: string }\n | { kind: 'clear'; file: string; signature: string }\n | { kind: 'none'; signature: 'none' }\n\nconst chooseSourcedFile = (dir: string): SourcedFile => {\n const loadPath = path.join(dir, ENV_LOAD_FILE)\n const clearPath = path.join(dir, ENV_CLEAR_FILE)\n const load = statOrNull(loadPath)\n const clear = statOrNull(clearPath)\n\n // zshenv: `! clear -nt load` \u2014 a tie goes to the load file.\n if (load && !(clear && clear.mtimeMs > load.mtimeMs)) {\n return { kind: 'load', file: loadPath, signature: `load:${load.ino}:${load.mtimeMs}:${load.size}` }\n }\n\n if (clear) return { kind: 'clear', file: clearPath, signature: `clear:${clear.ino}:${clear.mtimeMs}` }\n\n return { kind: 'none', signature: 'none' }\n}\n\nconst parseSourcedFile = (sourced: SourcedFile): SessionEnvState => {\n if (sourced.kind === 'load') {\n return {\n kind: 'load',\n vars: parseVarsFromEnvFile(sourced.file),\n unset: parseUnsetNamesFromEnvFile(sourced.file),\n signature: sourced.signature,\n }\n }\n\n if (sourced.kind === 'clear') {\n return { kind: 'clear', unset: parseUnsetNamesFromEnvFile(sourced.file), signature: sourced.signature }\n }\n\n return sourced\n}\n\nexport const readSessionEnvState = (dir = resolveSessionDir()): SessionEnvState => {\n if (dir === null) return { kind: 'no-session' }\n\n return parseSourcedFile(chooseSourcedFile(dir))\n}\n\n/** `readSessionEnvState` minus the parse when the file is the one already applied. */\nconst readSessionEnvStateIfMoved = (): SessionEnvState | null => {\n const dir = resolveSessionDir()\n\n if (dir === null) return { kind: 'no-session' }\n\n const sourced = chooseSourcedFile(dir)\n\n return sourced.signature === lastSignature ? null : parseSourcedFile(sourced)\n}\n\nconst restoreTouched = (): void => {\n for (const name of touched) {\n const original = baseline![name]\n\n if (original === undefined) {\n delete process.env[name]\n } else {\n process.env[name] = original\n }\n }\n\n touched.clear()\n}\n\ninterface Overlay {\n set: string[]\n unset: string[]\n skipped: string[]\n}\n\nconst assign = (overlay: Overlay, name: string, value: string): void => {\n if (NEVER_OVERLAID.has(name)) {\n overlay.skipped.push(name)\n\n return\n }\n\n process.env[name] = value\n touched.add(name)\n overlay.set.push(name)\n}\n\nconst remove = (overlay: Overlay, name: string): void => {\n if (NEVER_OVERLAID.has(name)) {\n overlay.skipped.push(name)\n\n return\n }\n\n delete process.env[name]\n touched.add(name)\n overlay.unset.push(name)\n}\n\nconst overlayState = (state: Exclude<SessionEnvState, { kind: 'no-session' }>): Overlay => {\n const overlay: Overlay = { set: [], unset: [], skipped: [] }\n\n if (state.kind === 'load') {\n for (const [name, value] of Object.entries(state.vars)) assign(overlay, name, value)\n for (const name of state.unset) remove(overlay, name)\n }\n\n if (state.kind === 'clear') {\n for (const name of state.unset) remove(overlay, name)\n assign(overlay, INFRA_KIT_ENV_CLEARED_VAR, '1')\n }\n\n return overlay\n}\n\nconst describeState = (state: Exclude<SessionEnvState, { kind: 'no-session' }>): string => {\n return state.kind === 'load' ? `load, ${Object.keys(state.vars).length} vars` : state.kind\n}\n\n/**\n * Make `process.env` equal `baseline \u2295 state`. SYNCHRONOUS on purpose: the SDK\n * dispatches tool handlers concurrently, and a restore+overlay that cannot yield\n * cannot interleave \u2014 `process.env` is always one full result, never a mix of two.\n *\n * The stat pair is the whole per-call cost: the default state is read through the\n * signature check, so an unchanged file is never parsed. Never throws: a missing\n * session id or a vanished file degrades to \"nothing applied\", corrected on the\n * next call.\n */\nexport const applySessionEnv = (state = readSessionEnvStateIfMoved()): SessionEnvApplyResult => {\n baseline ??= { ...process.env }\n\n if (state === null || state.kind === 'no-session' || state.signature === lastSignature) {\n return { set: [], unset: [], changed: false }\n }\n\n restoreTouched()\n\n const { set, unset, skipped } = overlayState(state)\n\n lastSignature = state.signature\n\n if (skipped.length > 0) logger.warn(`session-env: skipped protected names [${skipped.join(', ')}]`)\n\n logger.info(`session-env applied: set [${set.join(', ')}] unset [${unset.join(', ')}] (${describeState(state)})`)\n\n return { set, unset, changed: true }\n}\n", "import { acceptedContent, inputRequired, inputResponse } from '@modelcontextprotocol/server'\nimport type { InputRequiredResult } from '@modelcontextprotocol/server'\nimport type { z } from 'zod'\n\nimport { withDeadline } from 'src/lib/deadline'\nimport type { ArgumentFormProvider } from 'src/types'\n\n/**\n * @fileoverview\n *\n * The argument-form half of the MCP chokepoint: build the form, read what came back, and refuse\n * anything that came back narrower than it went out.\n *\n * Every function here is TOTAL. The form path runs on two things the handler does not control \u2014 a\n * provider written by someone else, and content the client sent back \u2014 and the SDK validates\n * neither (\"treat them as untrusted input\", `\u00ABd.mts\u00BB:1406`, `:1500`, `:2114`). A throw escaping\n * from here would leave the chokepoint's catch returning a TOOL ERROR on a call that was owed a\n * confirm gate, which is the failure this module exists to prevent. So every provider call and\n * every SDK call below is wrapped, and every failure mode \u2014 rejection, deadline, `null`, a thrown\n * `TypeError` \u2014 reads as the same single answer: `null`, meaning \"no form; fall to the gate\".\n */\n\n/**\n * The identifier the form is registered under in `inputRequests`, and the key the client's reply\n * comes back under in `ctx.mcpReq.inputResponses`. One key, one form, one round trip.\n */\n// Module-private on purpose. Both sides of the round trip are in this file \u2014 `buildArgumentForm`\n// registers under it, `readFormAction` and `readAcceptedArgs` read back under it \u2014 so the key is an\n// internal convention, not a contract anyone outside gets to depend on. Exporting it would invite a\n// caller to reach into `inputResponses` themselves and re-derive an action without the totality the\n// functions below are built to guarantee.\nconst FORM_KEY = 'args'\n\n/**\n * How long the chokepoint waits for a provider to produce a schema.\n *\n * A provider may reach the network (the release provider runs `git ls-remote` and a Jira fetch),\n * and those are `Promise.allSettled`-wrapped inside it \u2014 so they never FAIL, they only take as long\n * as they take. Without a ceiling a slow remote or an SSH key prompt turns `tools/call` into a call\n * that returns nothing at all: no form, no gate, no error. The deadline converts that into today's\n * behaviour instead.\n */\nexport const FORM_DEADLINE_MS = 3_000\n\n/**\n * The action a retried request carried for the form, with every non-elicitation response kind\n * folded into `missing`.\n *\n * `inputResponse` is documented TOTAL \u2014 a malformed or absent entry reads as `{kind:'missing'}` \u2014\n * and that is also where a key the SDK DROPPED lands: a dropped entry is removed from\n * `inputResponses` and only its name is kept in `droppedInputResponseKeys`, so it reaches us as an\n * absent key. That is why nothing here reads `droppedInputResponseKeys`: the drop is already\n * covered, as a `missing` that is not an accept.\n */\nexport type FormAction = 'accept' | 'decline' | 'cancel' | 'missing'\n\n/**\n * Read the form's action out of a retried request's responses.\n *\n * `InputResponseView` is a union and `action` exists only on its `kind:'elicit'` member, so the\n * narrowing is load-bearing rather than cosmetic.\n *\n * @example\n * readFormAction({ args: { action: 'decline' } }) // => 'decline'\n * readFormAction(undefined) // => 'missing'\n */\nexport const readFormAction = (responses: Record<string, unknown> | undefined): FormAction => {\n const view = inputResponse(responses, FORM_KEY)\n\n return view.kind === 'elicit' ? view.action : 'missing'\n}\n\n/**\n * The provider's schema for these arguments, or `null` on ANY failure: a rejection, a deadline\n * overrun, a synchronous throw before the promise is even returned, or the provider's own `null`.\n *\n * Called on BOTH rounds. Round 1 needs it to build the form; round 2 needs a schema to validate what\n * came back (\u00ABd.mts\u00BB:1406), and the round-2 `params` are the round-1 arguments the client echoed\n * back, so re-asking the provider reproduces it.\n */\n// The REBUILD on round 2 is deliberate, and it is not free. The two rounds are two separate\n// `tools/call` requests \u2014 round 2 is a re-entry, discriminated by `responses !== undefined` \u2014 so this\n// runs at most once per request and never twice within one. Across an accepted form that is two\n// builds, and for a provider that reaches the network (the release provider runs `git ls-remote` and\n// a Jira fetch) four calls where a single build would cost two. Both alternatives are worse.\n//\n// Reusing round 1's schema is not \"threading a value through\": nothing in this chokepoint survives a\n// request. The gate is stateless BY CONSTRUCTION \u2014 round 2 is trusted because it returns an HMAC the\n// server minted, not because the server remembered anything \u2014 so a reused schema needs either a\n// keyed server-side cache with its own eviction, or transport through the client. The second hands\n// the client the validator that is the ONLY thing checking the content it just sent. The first\n// re-introduces on a long-lived process exactly the per-request state `confirm-token.ts` was built\n// to avoid, to save two network calls per human decision.\n//\n// The rebuild does mean round 2 validates against a MOVED world: a choice that was legal when the\n// human made it can fail now, and they lose it. That is the correct direction to fail. A provider\n// enumerates candidates precisely because the current world constrains them \u2014 the release provider\n// lists the versions that already exist so a cut cannot collide \u2014 so honouring a stale selection\n// executes the collision the enumeration was there to prevent. The loss is not silent either: it\n// lands on the `formDiscarded` gate, which names the discard in a field AND in prose. Telling a\n// drift-caused failure apart from ordinary invalid content would need round 1's schema to compare\n// against, which is the cross-request state above \u2014 bought to reword a message, not to change an\n// outcome. `f14` in the tests pins the drifted round trip.\nconst trySchema = async (\n provider: ArgumentFormProvider,\n params: unknown,\n deadlineMs: number,\n): Promise<z.ZodObject<z.ZodRawShape> | null> => {\n try {\n return await withDeadline(provider.buildRequestedSchema(params), deadlineMs)\n } catch {\n return null\n }\n}\n\n/**\n * The form to send, or `null` meaning \"offer no form; fall to the gate\".\n *\n * The `elicit()` call is wrapped for a documented reason, not defensively: a Standard Schema it\n * cannot express as the restricted wire shape throws a `TypeError` BEFORE anything is sent\n * (\u00ABd.mts\u00BB:1404-1407) \u2014 a nested object property is enough \u2014 and a provider can arm that without\n * writing anything the type system objects to.\n *\n * @example\n * await buildArgumentForm(provider, { releases: [] }, 3000)\n * // => { resultType: 'input_required', inputRequests: { args: { method: 'elicitation/create', \u2026 } } }\n */\nexport const buildArgumentForm = async (\n provider: ArgumentFormProvider,\n params: unknown,\n deadlineMs: number,\n): Promise<InputRequiredResult | null> => {\n const schema = await trySchema(provider, params, deadlineMs)\n\n if (schema === null) return null\n\n try {\n return inputRequired({\n inputRequests: {\n [FORM_KEY]: inputRequired.elicit({ message: provider.message, requestedSchema: schema }),\n },\n })\n } catch {\n return null\n }\n}\n\n/**\n * The accepted form content, VALIDATED and merged over the round-1 arguments \u2014 or `null` when it\n * cannot be trusted, which the caller reads as \"gate with the round-1 arguments\".\n *\n * Three untrusted steps, each wrapped: the provider's schema, the SDK's schema-aware validation of\n * the content, and the provider's own merge.\n */\n// `acceptedContent`'s schema-aware overload is used because nothing between the client and here has\n// checked the content. It throws a `TypeError` on an asynchronously validating schema\n// (\u00ABd.mts\u00BB:1468-1470), and `z.ZodObject<z.ZodRawShape>` freely permits `.refine(async \u2026)` \u2014 so the\n// wrap is the enforcement of a contract the types cannot express. It returns `undefined` both for\n// \"not an accept\" and for \"failed validation\"; the caller has already discriminated the action, so\n// here `undefined` means only the latter. `toArgs` is the one member called on client-supplied\n// input: its contract says it never throws, and the wrap is what makes that true rather than promised.\nexport const readAcceptedArgs = async (\n provider: ArgumentFormProvider,\n params: unknown,\n responses: Record<string, unknown> | undefined,\n deadlineMs: number,\n): Promise<Record<string, unknown> | null> => {\n const schema = await trySchema(provider, params, deadlineMs)\n\n if (schema === null) return null\n\n let content: Record<string, unknown> | undefined\n\n try {\n content = acceptedContent(responses, FORM_KEY, schema) as Record<string, unknown> | undefined\n } catch {\n return null\n }\n\n if (content === undefined) return null\n\n try {\n return provider.toArgs(content, params)\n } catch {\n return null\n }\n}\n\n/** A plain object \u2014 not an array, not `null`, not a primitive. */\nconst isRecord = (value: unknown): value is Record<string, unknown> => {\n return typeof value === 'object' && value !== null && !Array.isArray(value)\n}\n\n/**\n * True when `after` removed a top-level key of `before`, or CHANGED the length of one of its\n * arrays. The chokepoint refuses such a merge and gates on the round-1 arguments instead.\n *\n * Both array directions are violations, and the growth half is the silent one: an injected entry\n * the caller never supplied stays schema-valid and EXECUTES. Deliberately TOP-LEVEL ONLY \u2014 nested\n * removal and equal-shape substitution are OUT of scope and are guarded only by the provider's merge.\n *\n * @example\n * narrowsArgs({ releases: [1, 2] }, { releases: [1] }) // => true (truncated)\n * narrowsArgs({ releases: [1] }, { releases: [1, 2] }) // => true (grown)\n * narrowsArgs({ a: 1, b: 2 }, { a: 9, b: 2 }) // => false (substitution is the feature)\n */\n// A form narrows a CHOICE; it never withdraws or invents an item the caller listed. So the length\n// rule is a ceiling rather than a heuristic \u2014 no legitimate provider, present or future, needs it\n// relaxed, and one that trips it is wrong by construction. The depth limit is the opposite kind of\n// decision: a recursive walk would forbid a human blanking an optional field they no longer want,\n// and nothing generic separates \"the human cleared this\" from \"the provider dropped this\".\nexport const narrowsArgs = (before: unknown, after: unknown): boolean => {\n if (!isRecord(before)) return false\n // `before` is a record and `after` is not: EVERY argument was narrowed away.\n if (!isRecord(after)) return true\n\n for (const [key, value] of Object.entries(before)) {\n if (!(key in after)) return true\n\n const replacement = after[key]\n\n if (Array.isArray(value) && (!Array.isArray(replacement) || replacement.length !== value.length)) return true\n }\n\n return false\n}\n", "import type { ClientCapabilities, InputRequiredResult } from '@modelcontextprotocol/server'\n\nimport { assertNever } from 'src/lib/assert-never'\nimport { commandEcho } from 'src/lib/command-echo'\nimport { ensureUserProjectConfig } from 'src/lib/config-bootstrap'\nimport { StructuredRefusalError } from 'src/lib/errors/structured-refusal-error'\nimport { logger } from 'src/lib/logger'\nimport { applySessionEnv as applySessionEnvFromFile } from 'src/lib/session-env'\nimport { textContent } from 'src/types'\nimport type { ArgumentFormProvider, ToolsExecutionResult } from 'src/types'\n\nimport { FORM_DEADLINE_MS, buildArgumentForm, narrowsArgs, readAcceptedArgs, readFormAction } from './argument-form'\nimport type { FormAction } from './argument-form'\nimport { getDefaultConfirmCodec, mintConfirmToken, stripGateKeys, verifyConfirmToken } from './confirm-token'\nimport type { ConfirmCodec, ConfirmRefusal } from './confirm-token'\n\ninterface ToolHandlerArgs {\n toolName: string\n handler: (params: any) => Promise<ToolsExecutionResult>\n /**\n * When true, this tool is gated by the destructive-op confirm gate below. Sourced from the\n * catalog tool's {@link CatalogMcpTool.requiresHumanConfirm} at registration (`mcp/tools/index.ts`).\n */\n requiresHumanConfirm?: boolean\n /**\n * Optional per-tool argument-form seam, sourced from the catalog tool's\n * {@link CatalogMcpTool.formProvider} at registration. Wired on gated and ungated tools alike (the\n * ungated `env-load` carries one). Absent must always mean \"the chokepoint behaves exactly as it\n * always has\": no form on either path.\n */\n formProvider?: ArgumentFormProvider\n /**\n * Reads the connected client's declared capabilities, so the chokepoint can ask whether this client\n * can render a form at all before offering one. Injected as a CLOSURE rather than a value because\n * capabilities are only known after initialize, and this handler is built at registration time.\n *\n * It receives the per-call context because on a 2026-era connection the capabilities travel in the\n * request ENVELOPE (`ctx.mcpReq.envelope`), and the server-level accessor is seeded from it only by\n * the SDK's HTTP entry \u2014 over stdio it stays `undefined` for the life of the process, which read as\n * \"cannot form\" and silently refused every modern-era client a form (found by the E-M1 lane).\n */\n getClientCapabilities?: (ctx: ToolCallContext | undefined) => ClientCapabilities | undefined\n /** Token codec for the gate. Defaults to the process-wide one; tests inject short-TTL or foreign-key codecs. */\n confirmCodec?: ConfirmCodec\n /**\n * How long the form path waits for {@link ArgumentFormProvider.buildRequestedSchema}. Defaults to\n * {@link FORM_DEADLINE_MS}; tests inject a few milliseconds so the deadline lane asserts a bound\n * instead of hanging until the runner's own timeout.\n */\n formDeadlineMs?: number\n /**\n * Re-applies the session's `env-load` file to `process.env`. Defaults to the real overlay; tests\n * inject a spy for the ordering lanes, and the mutation build a no-op so it never reads a session dir.\n */\n applySessionEnv?: () => void\n}\n\n/**\n * The SDK hands every tool callback a second argument (session, request state, abort signal).\n * Threaded through untouched so the signature lands once.\n */\nexport interface ToolCallContext {\n sessionId?: string\n /**\n * The per-request MCP envelope. Only the multi-round-trip carrier is declared: `inputResponses`\n * is how a client's answer to the argument form comes BACK, and its presence \u2014 not its content \u2014\n * is what separates a first call from a re-entry.\n */\n mcpReq?: {\n inputResponses?: Record<string, unknown>\n /**\n * The 2026-era request envelope. Read for ONE key \u2014 the client capabilities the SDK stamps under\n * `CLIENT_CAPABILITIES_META_KEY` \u2014 because that is where a modern-era stdio connection carries\n * them; the injection site owns the lookup.\n */\n envelope?: Record<string, unknown>\n /**\n * Keys the SDK dropped because they were not bare response objects. Declared for completeness\n * and deliberately UNREAD: a dropped key is absent from `inputResponses`, so it already reads\n * as `{kind:'missing'}` \u2014 an action that is not an accept, which is the decline path.\n */\n droppedInputResponseKeys?: string[]\n }\n}\n\nexport type GateState = 'run' | 'form' | 'declined' | 'gate' | 'verify' | 'run-form'\n\n/** Everything `resolveGateState` needs, all of it knowable BEFORE any work is done. */\nexport interface GateInputs {\n /** `requiresHumanConfirm === true` \u2014 spelled at the call site, not re-derived here. */\n gated: boolean\n /** The round-2 execute signal. */\n confirmed: boolean\n /** Present iff the client came BACK with an answer to the form. Content is irrelevant here. */\n responses: Record<string, unknown> | undefined\n /** The client declared form-mode elicitation. */\n canForm: boolean\n hasProvider: boolean\n /** The provider wants a form for THESE arguments. */\n formable: boolean\n /** The client came back with an ACCEPT specifically \u2014 decline, cancel and absence are all `false`. */\n accepted: boolean\n}\n\nconst isConfirmed = (params: unknown): boolean => {\n return typeof params === 'object' && params !== null && (params as { confirm?: unknown }).confirm === true\n}\n\n/**\n * The CANDIDATE state for a call. First-match-wins, in the order written, **every condition stated\n * in full** \u2014 no row relies on an earlier row having excluded anything.\n *\n * It is a candidate, not a verdict: `form` is the one outcome that can decline itself, because\n * whether a form can actually be built is knowable only after the provider has run under a\n * deadline. Every other outcome is terminal in the caller's own body.\n *\n * \"Gate\" describes rows G1\u2013G4 only: they are today's gate, unchanged, and they consume every\n * `gated` input before the ungated rows are reached. Rows U1\u2013U4 are a CALL state, not a gate\n * state \u2014 nothing on that path withholds execution pending consent; the form only fills arguments.\n * The name is kept because the tests and the mutation build import it.\n */\n// `!confirmed` is spelled on G1, G2 AND G3 for one reason: a non-elicitation round 2 carries\n// `confirm:true` with no `inputResponses`, so without it G3 matches, the call re-gates forever\n// and `verify` is unreachable. `responses !== undefined` is likewise the ONLY first-call/came-back\n// discriminator \u2014 `acceptedContent(...)` being falsy reads the same for decline, cancel and absence\n// alike, which would send a decline back to the form and re-prompt forever.\n//\n// `confirmed` is unread on U1\u2013U4: no ungated tool declares `confirm`, and the SDK's zod parse strips\n// undeclared keys before the params reach here, so it is `false` by construction on that path.\n//\n// The `!gated` conjuncts on U1\u2013U3 are defence-in-depth, not load-bearing: G1\u2013G4 partition `gated`\n// exhaustively, so the U rows are reachable only under `!gated` by ORDER. The order is what the\n// gated lanes pin \u2014 moving the U rows above the G rows lets an accepted form run without a token.\n//\n// Exported for its OWN tests, and not re-exported from `index.ts`, so the package's public surface\n// is unchanged. Some conjuncts are backed a second time downstream \u2014 `hasProvider` is also enforced\n// by the provider narrowing in `resolveForm` \u2014 so an end-to-end assertion cannot tell which\n// guard held. Testing the predicate directly is what makes deleting one of them observable.\nexport const resolveGateState = (input: GateInputs): GateState => {\n if (\n input.gated &&\n !input.confirmed &&\n input.responses === undefined &&\n input.canForm &&\n input.hasProvider &&\n input.formable\n ) {\n return 'form'\n }\n\n if (input.gated && !input.confirmed && input.responses !== undefined && !input.accepted) return 'declined'\n\n if (input.gated && !input.confirmed && (input.responses === undefined || input.accepted)) return 'gate'\n\n if (input.gated && input.confirmed) return 'verify'\n\n if (!input.gated && input.responses === undefined && input.canForm && input.hasProvider && input.formable) {\n return 'form'\n }\n\n if (!input.gated && input.responses !== undefined && input.hasProvider && !input.accepted) return 'declined'\n\n if (!input.gated && input.responses !== undefined && input.hasProvider && input.accepted) return 'run-form'\n\n // U4, condition complete: `!gated \u2227 ((responses === undefined \u2227 !(canForm \u2227 hasProvider \u2227 formable))\n // \u2228 (responses !== undefined \u2227 !hasProvider))` \u2014 the exact complement of U1\u2013U3 under `!gated`.\n // Spelled in the exhaustive partition test rather than here, so the fall-through stays the\n // fall-through.\n return 'run'\n}\n\n/**\n * Every gate and refusal payload sets `isError: true`: the MCP SDK validates a result's\n * `structuredContent` against the tool's `outputSchema` UNLESS `isError` is set, and no gated\n * tool's schema matches these payloads. A soft stop, not a failure \u2014 the tool did NOT run.\n */\nconst softStop = (structuredContent: Record<string, unknown>): ToolsExecutionResult => {\n return {\n content: textContent(JSON.stringify(structuredContent, null, 2)),\n structuredContent,\n isError: true,\n }\n}\n\n/** What the gate must SAY beyond its standing instruction, and to whom the arguments belong. */\ninterface GateNotices {\n /**\n * The human answered the form and their answer was thrown away \u2014 it failed validation, or it\n * narrowed the arguments. The gate then presents the AGENT'S values for approval in place of the\n * ones the human just chose, so saying so is not decoration: an unlabelled swap of the whole\n * selection is a worse substitution than the single-field one the merge exists to prevent.\n */\n formDiscarded: boolean\n hasProvider: boolean\n}\n\nconst FORM_DISCARDED_CLAUSE =\n 'The values you submitted in the form could NOT be applied and were DISCARDED \u2014 they failed validation or narrowed the arguments \u2014 so the arguments shown above are the ORIGINAL ones, not your selection.'\n\n// PM-D: only one gated tool in eight carries a provider, so a user who learns \"infra-kit asks me\n// before it does this\" from the one that does will read the other seven's silence as safety. The\n// true rule is \"no dialog \u21D2 this tool has no provider\", and nothing else in the payload says so.\nconst NO_FORM_CLAUSE =\n 'This tool does not prompt for its arguments; you are being asked to approve the values shown above.'\n\n/**\n * The round-1 gate: names the tool, echoes the arguments it will bind to, and carries the\n * `confirmToken` round 2 must return alongside `confirm:true` and the SAME arguments.\n */\nconst buildConfirmGate = async (\n codec: ConfirmCodec,\n toolName: string,\n params: unknown,\n notices: GateNotices,\n): Promise<ToolsExecutionResult> => {\n const resolvedArgs = stripGateKeys(params)\n const confirmToken = await mintConfirmToken(codec, toolName, params)\n const message = [\n notices.formDiscarded ? FORM_DISCARDED_CLAUSE : undefined,\n `${toolName} mutates external state and is gated. It was NOT executed. Re-call ${toolName} with the same arguments plus \"confirm\": true and this \"confirmToken\" to execute.`,\n notices.hasProvider ? undefined : NO_FORM_CLAUSE,\n ]\n .filter((clause) => {\n return clause !== undefined\n })\n .join(' ')\n\n return softStop({\n status: 'confirmation_required',\n tool: toolName,\n resolvedArgs,\n confirmToken,\n // A field and not only prose: an agent reacts to `formDiscarded` programmatically, for the same\n // reason `status` and `reason` are fields here rather than sentences.\n formDiscarded: notices.formDiscarded,\n message,\n })\n}\n\n/**\n * The form was answered with anything other than an accept. Terminal, and deliberately NOT a second\n * form: re-issuing one on a decline is an infinite prompt loop with the human's \"no\" as its engine.\n */\nconst buildFormDeclined = (toolName: string, action: FormAction): ToolsExecutionResult => {\n return softStop({\n status: 'form_declined',\n tool: toolName,\n action,\n message: `${toolName} was NOT executed: the argument form came back as \"${action}\". No confirmation is pending \u2014 call ${toolName} again to start over.`,\n })\n}\n\n/** Why an accepted form's content was thrown away on the ungated path. */\ntype FormDiscardReason = 'validation' | 'narrowed'\n\nconst DISCARD_TEXT: Record<FormDiscardReason, string> = {\n validation: 'failed validation',\n narrowed: 'narrowed the arguments',\n}\n\n/**\n * The ungated counterpart of the gate's `formDiscarded` notice. On a gated tool a discard still has\n * somewhere to go \u2014 the gate, carrying the round-1 arguments for approval. On an ungated tool there\n * is no gate, and running the round-1 arguments would silently execute the values the human never\n * saw in place of the ones they chose. Terminal, and never a second form: the human already answered.\n */\nconst buildFormDiscarded = (toolName: string, reason: FormDiscardReason): ToolsExecutionResult => {\n return softStop({\n status: 'form_discarded',\n tool: toolName,\n reason,\n message: `${toolName} was NOT executed: the values you submitted in the form ${DISCARD_TEXT[reason]} and were DISCARDED. No confirmation is pending \u2014 call ${toolName} again to start over.`,\n })\n}\n\nconst REFUSAL_TEXT: Record<ConfirmRefusal, string> = {\n absent: 'no \"confirmToken\" was supplied',\n malformed: 'the \"confirmToken\" is malformed',\n mac: 'the \"confirmToken\" was not issued by this server',\n expired: 'the \"confirmToken\" has expired',\n bind: 'the \"confirmToken\" was issued for a different tool',\n mismatch: 'the arguments differ from the ones the \"confirmToken\" was issued for',\n}\n\n/** The round-2 refusal: a terminal stop, deliberately NOT a second gate. */\nconst buildConfirmRefusal = (toolName: string, reason: ConfirmRefusal): ToolsExecutionResult => {\n return softStop({\n status: 'confirmation_refused',\n tool: toolName,\n reason,\n message: `${toolName} was NOT executed: ${REFUSAL_TEXT[reason]}. Call ${toolName} again WITHOUT \"confirm\" to receive a fresh gate, then re-call with the same arguments plus \"confirm\": true and the returned \"confirmToken\".`,\n })\n}\n\n/** Everything the stop resolution below needs from the tool's registration. */\ninterface StopDeps {\n toolName: string\n codec: ConfirmCodec\n requiresHumanConfirm: boolean | undefined\n formProvider: ArgumentFormProvider | undefined\n getClientCapabilities: ((ctx: ToolCallContext | undefined) => ClientCapabilities | undefined) | undefined\n formDeadlineMs: number\n}\n\n/** The arguments the gate will bind its token to, and whether they replaced a discarded selection. */\ninterface GateArgs {\n params: unknown\n formDiscarded: boolean\n}\n\n/**\n * Runs one form PREDICATE, treating any throw as `false`.\n *\n * `isFormable` and the capability probe are the two calls that decide whether a form is even\n * offered, and both run OUTSIDE the wraps `argument-form.ts` puts around `buildRequestedSchema`\n * and `toArgs`. Unwrapped, a provider that throws here escapes through the handler's outer catch\n * as a TOOL ERROR on a call that was owed a GATE \u2014 the exact failure those wraps exist to prevent,\n * reached one step earlier.\n *\n * `types.ts` states a never-throws contract for `isFormable`, but a contract is not a mechanism:\n * it is enforced per provider, which is the residual class the chokepoint's non-narrowing check was\n * adopted to remove. Failing to `false` here means the worst a broken predicate can do is decline a\n * form the human would have seen \u2014 never skip the gate, never reach the handler.\n */\nconst tryPredicate = (read: () => boolean): boolean => {\n try {\n return read()\n } catch {\n return false\n }\n}\n\n/**\n * The arguments a gate reached from an ACCEPTED form should carry. The round-1 arguments on every\n * path except one: a validated, merged, non-narrowing result.\n *\n * A failure here never goes back to the form \u2014 that is the re-prompt loop through a second door \u2014\n * and it never proceeds with the client's values either. It gates on what the agent originally\n * asked for, and says so.\n */\nconst resolveGateArgs = async (\n deps: StopDeps,\n params: unknown,\n responses: Record<string, unknown> | undefined,\n formAction: FormAction,\n): Promise<GateArgs> => {\n const provider = deps.formProvider\n\n if (provider === undefined || formAction !== 'accept') return { params, formDiscarded: false }\n\n const merged = await readAcceptedArgs(provider, params, responses, deps.formDeadlineMs)\n\n if (merged === null) {\n logger.info({ msg: `Tool execution form discarded (validation): ${deps.toolName}` })\n\n return { params, formDiscarded: true }\n }\n\n // Compared against `stripGateKeys(params)` \u2014 the same normalization the token is minted over, so\n // the check and the signature are looking at exactly one thing.\n if (narrowsArgs(stripGateKeys(params), merged)) {\n logger.info({ msg: `Tool execution form discarded (narrowed): ${deps.toolName}` })\n\n return { params, formDiscarded: true }\n }\n\n return { params: merged, formDiscarded: false }\n}\n\n/**\n * The chokepoint's verdict: either a result that answers the call in the handler's place, or the\n * arguments the handler runs with. A union rather than `null`-means-run because the ungated form\n * path runs with MERGED arguments, not the ones the call arrived with.\n */\ntype StopResolution =\n { kind: 'stop'; result: ToolsExecutionResult | InputRequiredResult } | { kind: 'run'; params: unknown }\n\nconst stopWith = (result: ToolsExecutionResult | InputRequiredResult): StopResolution => {\n return { kind: 'stop', result }\n}\n\nconst runWith = (params: unknown): StopResolution => {\n return { kind: 'run', params }\n}\n\n/** State `gate`, and the landing for a gated `form` that could not be built. */\nconst buildGate = async (\n deps: StopDeps,\n params: unknown,\n responses: Record<string, unknown> | undefined,\n formAction: FormAction,\n): Promise<ToolsExecutionResult> => {\n const gateArgs = await resolveGateArgs(deps, params, responses, formAction)\n\n logger.info({ msg: `Tool execution gated (awaiting confirm): ${deps.toolName}` })\n\n return await buildConfirmGate(deps.codec, deps.toolName, gateArgs.params, {\n formDiscarded: gateArgs.formDiscarded,\n hasProvider: deps.formProvider !== undefined,\n })\n}\n\n/**\n * State `form` \u2014 the one candidate that can decline itself: a provider that rejects, outruns its\n * deadline, resolves `null`, or hands `elicit()` a shape it cannot express falls through here with\n * no thrown error \u2014 to the gate on a gated tool, to the handler with the round-1 arguments on an\n * ungated one (where the handler, not the chokepoint, decides whether those arguments are enough).\n */\nconst resolveForm = async (\n deps: StopDeps,\n params: unknown,\n responses: Record<string, unknown> | undefined,\n formAction: FormAction,\n gated: boolean,\n): Promise<StopResolution> => {\n const form =\n deps.formProvider === undefined ? null : await buildArgumentForm(deps.formProvider, params, deps.formDeadlineMs)\n\n if (form !== null) {\n logger.info({ msg: `Tool execution form requested: ${deps.toolName}` })\n\n return stopWith(form)\n }\n\n logger.info({ msg: `Tool execution form unavailable: ${deps.toolName}` })\n\n return gated ? stopWith(await buildGate(deps, params, responses, formAction)) : runWith(params)\n}\n\n/**\n * State `run-form`: an ungated tool's accepted form. What runs is the provider's merge of content\n * validated against a schema built on THIS request \u2014 never the round-1 arguments after a discard,\n * and never a second form. The non-narrowing check is a chokepoint invariant kept for future\n * providers; a merge that only ever ADDS a key cannot trip it.\n */\nconst resolveUngatedForm = async (\n deps: StopDeps,\n params: unknown,\n responses: Record<string, unknown> | undefined,\n): Promise<StopResolution> => {\n const provider = deps.formProvider\n\n // U3 spells `hasProvider`, so this narrows for the types only; `run` is what U4 answers without one.\n if (provider === undefined) return runWith(params)\n\n const merged = await readAcceptedArgs(provider, params, responses, deps.formDeadlineMs)\n\n if (merged === null) {\n logger.info({ msg: `Tool execution form discarded (validation): ${deps.toolName}` })\n\n return stopWith(buildFormDiscarded(deps.toolName, 'validation'))\n }\n\n if (narrowsArgs(stripGateKeys(params), merged)) {\n logger.info({ msg: `Tool execution form discarded (narrowed): ${deps.toolName}` })\n\n return stopWith(buildFormDiscarded(deps.toolName, 'narrowed'))\n }\n\n logger.info({ msg: `Tool execution form accepted: ${deps.toolName}` })\n\n return runWith(merged)\n}\n\n/** State `verify`: round 2 of the gate. */\nconst resolveVerify = async (deps: StopDeps, params: unknown): Promise<StopResolution> => {\n const verdict = await verifyConfirmToken(deps.codec, deps.toolName, params)\n\n if (verdict.ok) return runWith(params)\n\n logger.info({ msg: `Tool execution refused (${verdict.reason}): ${deps.toolName}` })\n\n return stopWith(buildConfirmRefusal(deps.toolName, verdict.reason))\n}\n\n/**\n * The chokepoint's answer for a call.\n *\n * @example\n * await resolveStop(deps, { env: 'prod' }, undefined) // => { kind: 'stop', result: <the round-1 confirm gate> }\n */\nconst resolveStop = async (\n deps: StopDeps,\n params: unknown,\n ctx: ToolCallContext | undefined,\n): Promise<StopResolution> => {\n const responses = ctx?.mcpReq?.inputResponses\n const formAction = readFormAction(responses)\n // Spelled `=== true` on purpose: `mcp-confirm-gate-mutation.test.ts` neuters exactly this\n // predicate at build time to prove the gate's e2e assertions are load-bearing.\n const gated = deps.requiresHumanConfirm === true\n const state = resolveGateState({\n gated,\n confirmed: isConfirmed(params),\n responses,\n // `caps?.elicitation?.form`, NEVER `caps?.elicitation`: the SDK normalizes a bare\n // `{elicitation:{}}` to `{elicitation:{form:{}}}`, so both spellings agree on every fixture\n // except a url-only client \u2014 which is exactly the client that must NOT be offered a form.\n canForm: tryPredicate(() => {\n return deps.getClientCapabilities?.(ctx)?.elicitation?.form !== undefined\n }),\n hasProvider: deps.formProvider !== undefined,\n formable: tryPredicate(() => {\n return deps.formProvider?.isFormable(params) === true\n }),\n accepted: formAction === 'accept',\n })\n\n switch (state) {\n case 'run': {\n return runWith(params)\n }\n case 'declined': {\n logger.info({ msg: `Tool execution form declined (${formAction}): ${deps.toolName}` })\n\n return stopWith(buildFormDeclined(deps.toolName, formAction))\n }\n case 'verify': {\n return await resolveVerify(deps, params)\n }\n case 'run-form': {\n return await resolveUngatedForm(deps, params, responses)\n }\n case 'form': {\n return await resolveForm(deps, params, responses, formAction, gated)\n }\n case 'gate': {\n return stopWith(await buildGate(deps, params, responses, formAction))\n }\n default: {\n return assertNever(state)\n }\n }\n}\n\n// The return is a union because the chokepoint can now answer a call with an `InputRequiredResult` \u2014\n// the SDK's \"I need input from the human before I can run\" reply \u2014 as well as with a tool result.\n// It typechecks at the registration site because `ToolCallback` already returns\n// `CallToolResult | InputRequiredResult`.\n//\n// Deliberately UNCONDITIONAL. Narrowing it back for callers that pass no `formProvider` would be\n// asserting \"no provider means a form is unreachable\" \u2014 true only while the state machine keeps\n// requiring a provider, and enforced by nothing if that ever changes. Every caller narrows the\n// union explicitly instead.\nexport const createToolHandler = ({\n toolName,\n handler,\n requiresHumanConfirm,\n formProvider,\n getClientCapabilities,\n confirmCodec,\n formDeadlineMs,\n applySessionEnv = applySessionEnvFromFile,\n}: ToolHandlerArgs): ((\n params: unknown,\n ctx?: ToolCallContext,\n) => Promise<ToolsExecutionResult | InputRequiredResult>) => {\n const deps: StopDeps = {\n toolName,\n codec: confirmCodec ?? getDefaultConfirmCodec(),\n requiresHumanConfirm,\n formProvider,\n getClientCapabilities,\n formDeadlineMs: formDeadlineMs ?? FORM_DEADLINE_MS,\n }\n\n // `ctx` is bound HERE, in the returned closure, and not merely declared on the exported type. The\n // SDK passes it on every call, so a closure that omits the parameter discards it silently while\n // the signature keeps advertising it \u2014 the gap survived PR 1 unnoticed for exactly that reason.\n return async (params: unknown, ctx?: ToolCallContext) => {\n logger.info({ msg: `Tool execution started: ${toolName}`, params, sessionId: ctx?.sessionId })\n try {\n // MCP entry-boundary seed. This wrapper is the sole chokepoint for every exposed tool\n // (`src/mcp/tools/index.ts` holds the only `registerTool` call), and the `mcp` command itself is\n // in program.ts's SEED_EXCLUDED \u2014 so an MCP server seeds lazily, on its FIRST tool invocation,\n // and a server that never receives one never writes to $HOME. The once-per-process guard inside\n // ensureUserProjectConfig makes calls 2..N free on this long-lived process.\n //\n // Deliberately INSIDE the try, not before it: defense-in-depth. ensureUserProjectConfig is\n // contractually never-throw, but if that contract ever broke, an unhandled rejection here would\n // take down the long-lived server. Inside the try it degrades to an ordinary tool error.\n await ensureUserProjectConfig()\n\n // The command handlers record their resolved flags into the `commandEcho` singleton. On the CLI,\n // Commander's `preAction` clears it before every command; this long-lived server never runs that\n // hook, so without a reset here one tool call's flags would leak into the next one's snapshot and\n // the options array would grow for the life of the process.\n commandEcho.reset()\n\n // The env-load file lands mid-session, and every read from here on is `process.env` at call\n // time (Jira, the INFRA_KIT_ENV_* markers, the env children inherit). Applied at the call's\n // ENTRY so the gate, the form and the handler all run under one environment; synchronous so\n // a concurrently dispatched call cannot see a half-applied one.\n applySessionEnv()\n\n // Orthogonal destructive-op confirm gate, and the argument form. Both sit BEFORE the handler and\n // are INDEPENDENT of the `confirmedCommand:true` injected below \u2014 that flag is a prompt-skip /\n // behavior discriminator (e.g. worktrees-remove keys `allowEditorRelaunch` off it) and MUST keep\n // being injected on the real call, or the non-TTY server would hang on an inquirer prompt and\n // the Zed relaunch would re-enable.\n const resolution = await resolveStop(deps, params, ctx)\n\n if (resolution.kind === 'stop') return resolution.result\n\n const payload = await handler({ ...(resolution.params as object), confirmedCommand: true })\n\n logger.info({ msg: `Tool execution successful: ${toolName}` })\n\n return payload\n } catch (error) {\n logger.error({\n err: error,\n params,\n msg: `Tool execution failed: ${toolName}`,\n })\n\n // The one refusal channel for both surfaces (lib/errors/structured-refusal-error): thrown by the\n // handler, RENDERED here \u2014 the SDK would otherwise flatten it to text-only `isError` content and\n // drop `structuredContent`, the payload the agent is meant to act on. Same shape as `softStop`\n // so a refusal from inside the handler is indistinguishable from one the gate produced.\n if (error instanceof StructuredRefusalError) {\n return { ...softStop(error.structuredContent), content: textContent(error.message) }\n }\n\n throw error\n }\n }\n}\n"],
5
+ "mappings": "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",
6
+ "names": ["serveStdio", "process", "delay", "process", "setupErrorHandlers", "logger", "process", "error", "LOG_FILE_PATH", "reason", "promise", "McpServer", "CONFIG_RESOURCE_URI", "DEV_CONTEXT_RESOURCE_URI", "SESSION_WORKFLOW_URI", "SESSION_WORKFLOW_STUB", "defaultDeps", "getInfraKitConfig", "readDevContext", "jsonResource", "uri", "value", "initializeResources", "server", "deps", "error", "CLIENT_CAPABILITIES_META_KEY", "z", "createRequestStateCodec", "randomBytes", "GATE_KEYS", "CONFIRM_TOKEN_TTL_SECONDS", "asBindContext", "toolName", "sortKeysDeep", "value", "record", "key", "canonicalArgs", "args", "stripGateKeys", "params", "createConfirmCodec", "options", "codec", "ctx", "payload", "token", "defaultCodec", "getDefaultConfirmCodec", "readConfirmToken", "confirmToken", "REFUSALS_FROM_CODEC", "codecRefusal", "error", "message", "mintConfirmToken", "verifyConfirmToken", "fs", "path", "process", "NEVER_OVERLAID", "PROTECTED_CHILD_ENV_NAMES", "INFRA_KIT_SESSION_VAR", "baseline", "touched", "lastSignature", "noSession", "statOrNull", "file", "fs", "resolveSessionDir", "noSession", "getSessionCacheDir", "logger", "INFRA_KIT_SESSION_VAR", "chooseSourcedFile", "dir", "loadPath", "path", "ENV_LOAD_FILE", "clearPath", "ENV_CLEAR_FILE", "load", "clear", "parseSourcedFile", "sourced", "parseVarsFromEnvFile", "parseUnsetNamesFromEnvFile", "readSessionEnvStateIfMoved", "dir", "resolveSessionDir", "sourced", "chooseSourcedFile", "lastSignature", "parseSourcedFile", "restoreTouched", "name", "touched", "original", "baseline", "process", "assign", "overlay", "value", "NEVER_OVERLAID", "remove", "overlayState", "state", "INFRA_KIT_ENV_CLEARED_VAR", "describeState", "applySessionEnv", "set", "unset", "skipped", "logger", "acceptedContent", "inputRequired", "inputResponse", "FORM_KEY", "FORM_DEADLINE_MS", "readFormAction", "responses", "view", "inputResponse", "trySchema", "provider", "params", "deadlineMs", "withDeadline", "buildArgumentForm", "schema", "inputRequired", "readAcceptedArgs", "content", "acceptedContent", "isRecord", "value", "narrowsArgs", "before", "after", "key", "replacement", "isConfirmed", "params", "resolveGateState", "input", "softStop", "structuredContent", "textContent", "FORM_DISCARDED_CLAUSE", "NO_FORM_CLAUSE", "buildConfirmGate", "codec", "toolName", "notices", "resolvedArgs", "stripGateKeys", "confirmToken", "mintConfirmToken", "message", "clause", "buildFormDeclined", "action", "DISCARD_TEXT", "buildFormDiscarded", "reason", "REFUSAL_TEXT", "buildConfirmRefusal", "tryPredicate", "read", "resolveGateArgs", "deps", "responses", "formAction", "provider", "merged", "readAcceptedArgs", "logger", "narrowsArgs", "stopWith", "result", "runWith", "buildGate", "gateArgs", "resolveForm", "gated", "form", "buildArgumentForm", "resolveUngatedForm", "resolveVerify", "verdict", "verifyConfirmToken", "resolveStop", "ctx", "readFormAction", "state", "assertNever", "createToolHandler", "handler", "requiresHumanConfirm", "formProvider", "getClientCapabilities", "confirmCodec", "formDeadlineMs", "applySessionEnv", "getDefaultConfirmCodec", "FORM_DEADLINE_MS", "ensureUserProjectConfig", "commandEcho", "resolution", "payload", "error", "StructuredRefusalError", "initializeTools", "server", "tool", "getExposedMcpTools", "z", "withConfirmToken", "wrapForRegistration", "createToolHandler", "ctx", "CLIENT_CAPABILITIES_META_KEY", "shape", "handle", "params", "createMcpServer", "agentMode", "server", "McpServer", "package_default", "initializeResources", "initializeTools", "suppressTypelessPackageJsonWarning", "logger", "initLoggerMcp", "buildOrDie", "createMcpServer", "error", "process", "setupErrorHandlers", "handle", "serveStdio", "isShuttingDown", "shutdown", "signal", "delay"]
7
7
  }
@@ -1,2 +1,2 @@
1
- import{l as t,q as i}from"./chunk-UNAV7SP4.js";import"./chunk-HHWP3YXW.js";import"./chunk-75URROMU.js";import a from"node:fs";import o from"node:process";import{fileURLToPath as s}from"node:url";var m=r=>{let n=r.indexOf("--parent-pid");if(n===-1)return;let e=Number(r[n+1]);return Number.isInteger(e)&&e>0?e:void 0},p=()=>a.realpathSync(s(new URL("./cli.js",import.meta.url))),d=async()=>{await i(t.version,{selfRealPath:p(),parentPid:m(o.argv)})};d().catch(()=>{o.exitCode=0});
1
+ import{l as t,q as i}from"./chunk-IXWQ7EQT.js";import"./chunk-HHWP3YXW.js";import"./chunk-75URROMU.js";import a from"node:fs";import o from"node:process";import{fileURLToPath as s}from"node:url";var m=r=>{let n=r.indexOf("--parent-pid");if(n===-1)return;let e=Number(r[n+1]);return Number.isInteger(e)&&e>0?e:void 0},p=()=>a.realpathSync(s(new URL("./cli.js",import.meta.url))),d=async()=>{await i(t.version,{selfRealPath:p(),parentPid:m(o.argv)})};d().catch(()=>{o.exitCode=0});
2
2
  //# sourceMappingURL=update-check.js.map
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "infra-kit",
3
3
  "type": "module",
4
- "version": "0.7.12",
4
+ "version": "0.9.0",
5
5
  "files": [
6
6
  "dist"
7
7
  ],
@@ -22,7 +22,7 @@
22
22
  "@inquirer/password": "^5.2.2",
23
23
  "@inquirer/select": "^5.2.5",
24
24
  "@modelcontextprotocol/server": "^2.0.0",
25
- "@slip-stream-kit/config": "^0.7.12",
25
+ "@slip-stream-kit/config": "^0.9.0",
26
26
  "chalk": "^6.0.0",
27
27
  "chokidar": "^5.0.0",
28
28
  "commander": "^15.0.0",
@@ -39,7 +39,7 @@
39
39
  "devDependencies": {
40
40
  "@modelcontextprotocol/client": "^2.0.0",
41
41
  "@modelcontextprotocol/sdk": "^1.30.0",
42
- "@slip-stream-kit/eslint-plugin": "0.7.12",
42
+ "@slip-stream-kit/eslint-plugin": "0.9.0",
43
43
  "@types/aws-lambda": "^8.10.163",
44
44
  "@types/react": "19.3.0",
45
45
  "@wl/eslint-config": "0.1.0",
package/readme.md CHANGED
@@ -66,7 +66,6 @@ Strict schema file that declares audit rules (`requiredScripts`, `requiredFiles`
66
66
  | | `worktrees list` | List all worktrees |
67
67
  | | `worktrees remove` | Remove a worktree (no `--force`; a branch git refuses is reported and the command exits non-zero; a leftover git already unregistered that holds only `.omc/state`, `.omc/sessions` or `.DS_Store` is swept automatically) |
68
68
  | | `worktrees sync` | Sync worktree state (same removal and failure reporting as `worktrees remove`) |
69
- | | `reopen` | Reopen a closed worktree |
70
69
  | **Environment** | `env-status` | Show Doppler env status |
71
70
  | | `env-list` | List secrets in current env |
72
71
  | | `env-load` | Load env secrets into shell |
@@ -111,7 +110,7 @@ When infra-kit runs as an MCP server (via `.mcp.json`), these commands are avail
111
110
  - `gh-merge-dev`, `gh-release-list`, `release-create`, `release-remove`, `release-desc-edit`, `gh-release-deploy-all`, `gh-release-deploy-selected`
112
111
  - `release-create` offers an argument form when `releases` is omitted (one release; batches are arguments-only).
113
112
  - `local-deploy-all`, `local-deploy-selected`
114
- - `worktrees-add`, `worktrees-list`, `reopen`, `worktrees-remove`, `worktrees-sync`
113
+ - `worktrees-add`, `worktrees-list`, `worktrees-remove`, `worktrees-sync`
115
114
  - `env-status`, `env-list`, `env-load`, `env-clear`, `env-token-list`
116
115
  - `config-get`, `vendor-check`
117
116
  - `audit`, `version`
@@ -1,8 +0,0 @@
1
- import{a as G}from"./chunk-MFAFQVTW.js";import{e as A}from"./chunk-75URROMU.js";import q from"node:process";import $ from"pino";import ke from"pino-pretty";var V="/tmp/mcp-infra-kit.log",B=["token","*.token","serviceToken","*.serviceToken","DOPPLER_TOKEN","*.DOPPLER_TOKEN","INFRA_KIT_ENV_TOKEN","*.INFRA_KIT_ENV_TOKEN"],Jt=()=>{let e=q.argv.includes("--debug")?"debug":"info",t=$({level:e,redact:B},$.destination({dest:V}));return t.info(`Logger initialized with level: ${e}. Logging to: ${V}`),t},xe=()=>{let e=q.argv.includes("--debug")?"debug":"info",t=["time","pid","hostname"];return e==="debug"&&t.push("level"),$({level:e,redact:B},ke({destination:2,ignore:t.join(","),colorize:!0}))},Yt=xe();import W from"node:fs/promises";import pe from"node:os";import R from"node:path";import He from"node:process";import{z as s}from"zod";import x from"node:path";import Q from"node:process";import{$ as f}from"zx";var w=e=>{let t=new Set,r=e;for(;r!==null&&typeof r=="object"&&!t.has(r);){t.add(r);let{stderr:n,stderrExcerpt:o}=r;if(typeof n=="string"&&n.length>0)return n;if(typeof o=="string"&&o.length>0)return o;r=r.cause}},we=(e,t)=>{let r=t.stderrExcerpt??w(e),n=[`failed to ${t.operation}`];return r&&n.push(`stderr: ${r.slice(0,200).trim()}`),t.remediation&&n.push(`try: ${t.remediation}`),n.join(" \u2014 ")},d=class extends Error{operation;remediation;stderrExcerpt;constructor(t,r){super(we(t,r),{cause:t}),this.name="OperationError",this.operation=r.operation,this.remediation=r.remediation,this.stderrExcerpt=r.stderrExcerpt??w(t)}};var U={enabled:!1},P=()=>U.enabled;var Pe=/^v?(\d+)\.(\d+)\.(\d+)$/,be=/^(\d+)\.(\d+)\.(\d+)$/,Re=/^[a-z0-9]+(?:-[a-z0-9]+)*$/,C="release/",D="release/v",H="refs/heads/",Ee="next";var X=new Set(["dev","main","next","hotfix","regular","release"]),v=class extends Error{constructor(t){super(t),this.name="InvalidReleaseNameError"}},k=class extends Error{constructor(t){super(t),this.name="InvalidReleaseRefError"}},Y=e=>e.startsWith(H)?e.slice(H.length):e,Z=(e,t,r)=>({kind:"version",semver:{major:e,minor:t,patch:r},raw:`${e}.${t}.${r}`}),T=e=>{if(e.length===0)throw new v('Release name is empty. Provide a kebab-case name like "checkout-redesign".');if(e.length>50)throw new v(`Release name "${e}" is ${e.length} characters; the maximum is 50.`);if(!Re.test(e))throw new v(`Release name "${e}" is not kebab-case. Use lowercase letters, digits, and single hyphens, e.g. "checkout-redesign".`);if(X.has(e))throw new v(`Release name "${e}" is reserved. Reserved names: ${[...X].join(", ")}.`)},N=e=>{let t=Y(e.trim());if(!t.startsWith(C))return null;if(t.startsWith(D)){let n=t.slice(D.length),o=be.exec(n);if(o)return Z(Number(o[1]),Number(o[2]),Number(o[3]))}let r=t.slice(C.length);try{T(r)}catch{return null}return{kind:"name",name:r,raw:r}},Se=e=>{let t=e.trim();if(Y(t).startsWith(C)){let o=N(t);if(!o)throw new k(`"${e}" looks like a release branch but is not a valid release/v<semver> or release/<name> ref.`);return o}let n=Pe.exec(t);if(n)return Z(Number(n[1]),Number(n[2]),Number(n[3]));if(t.toLowerCase()===Ee)throw new k('The "next" token must be resolved to a concrete version (via computeNextVersion) before parsing a release ref.');try{T(t)}catch(o){let i=o instanceof Error?o.message:String(o);throw new k(`Cannot parse "${e}" as a release ref: ${i}`)}return{kind:"name",name:t,raw:t}},Ce=e=>e.kind==="version"?`${D}${e.raw}`:`${C}${e.name}`,je=(e,t)=>{let r=t==="hotfix"?"Hotfix":"Release";return e.kind==="version"?`${r} v${e.raw}`:`${r} ${e.name}`},Ie=e=>e.kind==="version"?`Release v${e.raw} (RC)`:`Release ${e.name} (RC)`,Ae=e=>e.kind==="version"?`v${e.raw}`:e.name,$e=e=>e.raw,_=e=>e==null?!1:N(e)!==null,J=e=>{if(e===void 0)return null;let t=e instanceof Date?e.getTime():new Date(e).getTime();return Number.isNaN(t)?null:t},De=(e,t,r)=>{if(e.kind==="version"&&t.kind==="version")return e.semver.major!==t.semver.major?e.semver.major-t.semver.major:e.semver.minor!==t.semver.minor?e.semver.minor-t.semver.minor:e.semver.patch-t.semver.patch;if(e.kind==="version")return-1;if(t.kind==="version")return 1;let n=J(r?.a),o=J(r?.b);return n!==null&&o!==null&&n!==o?n-o:e.name<t.name?-1:e.name>t.name?1:0};var ee=async e=>{let t=await f({cwd:e})`git worktree list --porcelain`,r=[],n=null;for(let o of t.stdout.split(`
2
- `)){if(o.startsWith("worktree ")){n&&r.push(n),n={path:o.slice(9),branch:null,detached:!1,bare:!1,prunable:!1,locked:!1};continue}n&&(o.startsWith("branch ")?n.branch=o.slice(7).replace(/^refs\/heads\//,""):o==="detached"?n.detached=!0:o==="bare"?n.bare=!0:o==="locked"||o.startsWith("locked ")?n.locked=!0:(o==="prunable"||o.startsWith("prunable "))&&(n.prunable=!0))}return n&&r.push(n),r},Te=async e=>{let t=await ee(Q.cwd()),r=n=>e==="release"?_(n):n.startsWith("feature/");return t.map(n=>n.branch).filter(n=>n!==null&&r(n))},Ne=e=>{let t=w(e);return t&&/not a git repository/i.test(t)?"not a git repository":void 0},_e=({hasStderr:e})=>{if(P())return"the infra-kit MCP server resolves its project from the working directory it was launched in; the operator must relaunch the server with its working directory set to an infra-kit project repo";let t="run infra-kit from inside an infra-kit project repo (or one of its git worktrees)";return e?`${t}; if you are already in one, the stderr in this message is the real cause`:t},b=async()=>{try{return(await f({quiet:!0})`git rev-parse --show-toplevel`).stdout.trim()}catch(e){let t=Ne(e)??w(e);throw new d(e,{operation:`resolve the project root from ${Q.cwd()}`,stderrExcerpt:t,remediation:_e({hasStderr:t!==void 0})})}},L=async e=>{let t=e??await b(),r=(await f({cwd:t,quiet:!0})`git rev-parse --git-common-dir`).stdout.trim(),n=x.resolve(t,r);return n.includes(`${x.sep}.git${x.sep}modules${x.sep}`)?t:x.dirname(n)},te=async()=>(await f`git rev-parse --abbrev-ref HEAD`).stdout.trim(),re=async()=>(await f`git status --porcelain`).stdout.split(`
3
- `).map(t=>t.trimEnd()).filter(t=>t.trim().length>0),Le=async()=>(await re()).length===0,Me=async()=>{let e=await b(),[t,r]=await Promise.all([f({cwd:e})`git rev-parse --absolute-git-dir`,f({cwd:e})`git rev-parse --git-common-dir`]),n=t.stdout.trim(),o=x.resolve(e,r.stdout.trim());return n!==o};var Oe=async e=>(await f`git branch --list ${e}`).stdout.trim().length>0,Ke=async e=>{(await f`git branch --list ${e}`).stdout.trim().length!==0&&await te()!==e&&await f`git branch -D ${e}`},We=async e=>{(await f`git ls-remote --heads origin ${e}`).stdout.trim().length!==0&&await f`git push origin --delete ${e}`};import{$ as j}from"zx";var Fe=async(e,t)=>{let r=`${t}^{commit}`;try{return(await j({cwd:e,quiet:!0})`git rev-parse --verify --quiet ${r}`).stdout.trim()||null}catch{return null}},ze=async(e,t,r)=>{try{return await j({cwd:e,quiet:!0})`git merge-base --is-ancestor ${t} ${r}`,!0}catch(n){if(n.exitCode===1)return!1;throw new d(n,{operation:`test whether ${t} is contained in ${r}`,remediation:"both refs must exist \u2014 verify them with rev-parse first"})}},Ge=async(e,t)=>{let n=(await j({cwd:e,quiet:!0})`git ls-remote --heads origin ${t}`).stdout.trim().split(`
4
- `)[0];return n?n.split(/\s+/)[0]??null:null},Ve=async(e,t)=>{let r=t.map(n=>`${n.sha}:refs/heads/${n.branch}`);if(r.length===0)return{refspecs:r,pushed:!1};try{return await j({cwd:e,quiet:!0})`git push --atomic origin ${r}`,{refspecs:r,pushed:!0}}catch(n){return{refspecs:r,pushed:!1,stderr:n.stderr}}};import ne from"node:path";import M from"node:process";import{$ as m}from"zx";var oe=async e=>{let t=e??M.cwd(),r=(await m({cwd:t,quiet:!0})`git rev-parse --git-common-dir`).stdout.trim(),n=M.env.INFRA_KIT_SESSION||String(M.pid);return ne.join(ne.resolve(t,r),"infra-kit",`merge-dev-${n}`)},O=async e=>{try{return await m({cwd:e,quiet:!0})`git rev-parse -q --verify MERGE_HEAD`,!0}catch{return!1}},qe=async e=>{if(await O(e))throw new d(void 0,{operation:"reuse the scratch worktree",stderrExcerpt:"a merge is still in progress in the scratch worktree",remediation:"this is an infra-kit bug \u2014 the previous branch iteration did not clean up"});let t=(await m({cwd:e,quiet:!0})`git status --porcelain`).stdout.trim();if(t.length>0)throw new d(void 0,{operation:"reuse the scratch worktree",stderrExcerpt:`scratch worktree is dirty: ${t.split(`
5
- `)[0]}`,remediation:"this is an infra-kit bug \u2014 the previous branch iteration did not clean up"})},Be=async e=>{let t=async r=>{try{await r()}catch{}};await O(e)&&await t(()=>m({cwd:e,quiet:!0})`git merge --abort`),await t(()=>m({cwd:e,quiet:!0})`git reset --hard`),await t(()=>m({cwd:e,quiet:!0})`git clean -fd`)},Ue=async(e,t)=>{let{cwd:r,anchor:n="origin/dev"}=e,o=await oe(r),i=async()=>{await m({cwd:r,quiet:!0})`git worktree add --detach ${o} ${n}`};try{await i()}catch(u){try{await m({cwd:r,quiet:!0})`git worktree prune`,await i()}catch{throw new d(u,{operation:"create the scratch worktree for the merge",remediation:`remove any stale checkout at ${o}, then retry`})}}let c={path:o,remove:async()=>{try{await m({cwd:r,quiet:!0})`git worktree remove --force ${o}`}catch{}}};try{return await t(c)}finally{await c.remove()}};var se="infra-kit.json";var Xe="infra-kit.json",Je="projects",Ye=s.object({provider:s.literal("doppler"),config:s.object({name:s.string().min(1)})}),Ze=s.discriminatedUnion("provider",[Ye]),Qe=s.object({workspaceConfigPath:s.string().min(1)}),et=s.object({provider:s.literal("cursor"),config:Qe}),tt=s.object({}),rt=s.object({provider:s.literal("zed"),config:tt}),ie=s.discriminatedUnion("provider",[et,rt]),nt=s.union([ie,s.array(ie).min(1)]),ot=s.object({provider:s.literal("jira"),config:s.object({baseUrl:s.string().url(),projectId:s.number().int().positive()})}),st=s.discriminatedUnion("provider",[ot]),it=["two-columns","three-pane"],at=s.object({layout:s.enum(it).optional()}),ct=s.object({openInGithubDesktop:s.boolean().optional(),openInCmux:s.boolean().optional(),cmux:at.optional()}),pt=s.object({port:s.number().int().positive().optional(),prefixUrl:s.string().min(1).optional()}).strict(),ut=s.record(s.string().min(1),pt),lt=s.enum(["local","cloud"]),ft=s.object({watchDeps:s.boolean().optional(),proxy:s.record(s.string().min(1),lt).optional()}).strict(),dt=s.object({apps:s.record(s.string().min(1),ft).optional(),cmux:s.boolean().optional()}).strict(),mt=s.record(s.string().min(1),dt),gt=s.object({port:s.number().int().positive().optional()}).strict(),ht=s.object({trigger:s.enum(["shell-startup","cli-invocation"]),config:s.string().min(1)}).strict(),yt=s.enum(["disallow","allow","cli-only"]),ae=s.string().regex(/^[A-Z_]\w*$/i,"must be a POSIX environment-variable name"),vt=s.object({command:s.string().min(1),args:s.array(s.string()).default([]),env:s.array(ae).min(1),unset:s.array(ae).default([])}).strict().superRefine((e,t)=>{for(let r of e.env)G.has(r)&&t.addIssue({code:"custom",path:["env"],message:`"${r}" is process plumbing the proxy never strips or injects \u2014 it cannot be a credential name`})}),kt=s.record(s.string().regex(/^[a-z][a-z0-9-]*$/,"must be lowercase letters, digits and hyphens, starting with a letter").refine(e=>!e.includes("infra-kit"),{message:'"infra-kit" is reserved for the infra-kit MCP server itself'}),vt),F=s.object({envManagement:Ze,ide:nt.optional(),taskManager:st.optional(),worktrees:ct.optional(),envAutoLoad:ht.optional(),dev:ut.optional(),devServersPresets:mt.optional(),devProxy:gt.optional(),protectedEnvs:yt.optional(),mcp:kt.optional()}).strict(),xt=443,ue=F.superRefine((e,t)=>{if(!Array.isArray(e.ide))return;let r=new Set;for(let n of e.ide){if(r.has(n.provider)){t.addIssue({code:"custom",message:"each IDE provider may appear at most once",path:["ide"]});return}r.add(n.provider)}}),le=F.partial(),wt=e=>{let t=e.ide;return t?Array.isArray(t)?t:[t]:[]},Pt="two-columns",bt=e=>e.worktrees?.cmux?.layout??Pt,g=null,E=null,fe=()=>`${He.cwd()} ${pe.homedir()}`,de=async()=>{let e=fe();if(E&&E.key===e)return E.value;let t=await b(),r=await L(t),n=R.basename(r),o=R.join(pe.homedir(),A),i={main:R.join(t,se),userGlobal:R.join(o,Xe),userProject:R.join(o,Je,n,se),projectName:n};return E={key:e,value:i},i},Rt=async()=>{let e=fe(),t=await de(),r;try{r=await W.stat(t.main)}catch{g=null;let h=t.main.replace(/\.json$/,".yml");throw await K(h)?new Error(`infra-kit.json not found at ${t.main}. A legacy infra-kit.yml exists \u2014 run \`infra-kit setup --skip-tools\` to convert it.`):new Error(P()?`infra-kit.json not found at ${t.main} \u2014 the directory the infra-kit MCP server was launched in is not an infra-kit project. The operator must relaunch the server with its working directory set to an infra-kit project repo.`:`infra-kit.json not found at ${t.main} \u2014 this git repo is not an infra-kit project. Run \`infra-kit setup --skip-tools\` here to create it, cd into an infra-kit project repo, or check out a branch that has infra-kit.json.`)}let[n,o]=await Promise.all([K(t.userGlobal),K(t.userProject)]),i={main:Number(r.mtimeMs),userGlobal:n?Number(n.mtimeMs):null,userProject:o?Number(o.mtimeMs):null};if(g&&g.key===e&&jt(g.mtimes,i))return g.value;let c=[{label:"infra-kit.json",path:t.main,required:!0},{label:"~/.infra-kit/infra-kit.json",path:t.userGlobal,required:!1},{label:`~/.infra-kit/projects/${t.projectName}/infra-kit.json`,path:t.userProject,required:!1}],u={};for(let h of c){let y=await It(h);y!==null&&(u={...u,...y})}let l=ue.safeParse(u);if(!l.success)throw new Error(`Invalid merged infra-kit config: ${s.prettifyError(l.error)}`);return g={key:e,mtimes:i,value:l.data},l.data},Et=()=>{g=null},St=()=>{g=null,E=null},K=async e=>{try{return await W.stat(e)}catch{return null}},Ct=async e=>{try{return await W.readFile(e,"utf-8")}catch{return null}},jt=(e,t)=>{let r=Object.keys(e);return r.length!==Object.keys(t).length?!1:r.every(n=>e[n]===t[n])},It=async e=>{let t=await Ct(e.path);if(t===null){if(e.required)throw new Error(`${e.label} not found at ${e.path}`);return null}let r;try{r=t.trim()===""?{}:JSON.parse(t)}catch(o){throw new Error(`Invalid JSON in ${e.label} at ${e.path}: ${o.message}`)}if(ce(r)&&"envTokens"in r)throw new Error($t(e));if(!e.required&&ce(r)&&"mcp"in r)throw new Error(At(e));let n=le.safeParse(r);if(!n.success)throw new Error(`Invalid ${e.label} at ${e.path}: ${s.prettifyError(n.error)}`);return n.data},At=e=>[`"mcp" is not allowed in ${e.label} (${e.path}): it feeds the committed .mcp.json, so a per-machine copy would replace the project's servers for everyone.`,"Move the block to the project infra-kit.json and run `infra-kit setup`.","For a machine-only override of one server (e.g. a local fork), use Claude Code directly:"," claude mcp add --scope local <name> -- ik-mcp --name <name> --env <VAR> ... -- <command> <args>","(local scope shadows the project entry by name; `claude mcp list` will show a same-name conflict warning, which is expected)."].join(`
6
- `),ce=e=>typeof e=="object"&&e!==null&&!Array.isArray(e),$t=e=>[`Refusing to load ${e.label} \u2014 \`envTokens\` is not a config key.`,"A service token in a config file can be committed, backed up by your editor, or shared."," 1. REVOKE the token in Doppler now \u2014 treat it as compromised.",` 2. Remove the \`envTokens\` key from ${e.path}.`," 3. Re-add it privately: `infra-kit env-token-set <env>`"," (it is written to ~/.infra-kit/projects/<repo>/tokens.json, mode 0600, never to the repo)."].join(`
7
- `);import*as p from"node:fs";import*as a from"node:path";function Dt(e){let t=e;for(let r=0;r<10;r++){let n=a.join(t,"pnpm-workspace.yaml");if(p.existsSync(n))return t;t=a.dirname(t)}throw new Error("Could not find monorepo root (pnpm-workspace.yaml)")}function ge(e,t){let r=a.join(e,"package.json");if(!p.existsSync(r))return t;try{let n=JSON.parse(p.readFileSync(r,"utf-8"));return typeof n.name=="string"?n.name:t}catch{return t}}function Or(e){let t=a.join(e,"apps"),r=[];if(!p.existsSync(t))throw new Error(`Apps directory not found: ${t}`);let n=p.readdirSync(t,{withFileTypes:!0}).filter(o=>o.isDirectory()).map(o=>o.name);for(let o of n){let i=a.join(t,o,"api"),c=a.join(i,"serverless.yml");p.existsSync(c)&&r.push({name:o,packageName:ge(i,o),path:i})}return r}var he=["vite.config.ts","vite.config.mts","vite.config.cts","vite.config.js","vite.config.mjs","vite.config.cjs"],Tt=["@slip-stream-kit/vite","@slip-stream-kit/config/vite","infra-kit/vite"];function Nt(e){for(let t of he){let r=a.join(e,t);if(p.existsSync(r))try{let n=p.readFileSync(r,"utf-8");return Tt.some(o=>n.includes(o))}catch{return!1}}return!1}function Kr(e){for(let t of he){let r=a.join(e,t);if(p.existsSync(r))try{return p.readFileSync(r,"utf-8").includes("infraKit(")}catch{return!1}}return!1}function _t(e){let t=a.join(e,"package.json");if(!p.existsSync(t))return!1;try{let r=JSON.parse(p.readFileSync(t,"utf-8"));return typeof r.scripts?.dev=="string"&&r.scripts.dev.length>0}catch{return!1}}function Wr(e){let t=a.join(e,"apps"),r=[];if(!p.existsSync(t))return r;let n=p.readdirSync(t,{withFileTypes:!0}).filter(o=>o.isDirectory()).map(o=>o.name);for(let o of n){let i=a.join(t,o,"ui");p.existsSync(i)&&_t(i)&&r.push({name:o,packageName:ge(i,o),path:i,managedPort:Nt(i)})}return r}function Fr(e){let t=e?.filter(Boolean)??[];return t.length>0?t:null}function zr(e){let t=Dt(e),r=a.relative(a.join(t,"apps"),e),n=r.split(a.sep)[0];if(r===""||r.startsWith("..")||a.isAbsolute(r)||!n)throw new Error(`--self: not inside an apps/<app> directory (cwd: ${e}). Run from an app folder or use --app=<name>.`);return n}var Lt=new Set(["api","ui"]),me=e=>{let t=a.join(e,"dist");return p.existsSync(t)&&p.statSync(t).isDirectory()?t:void 0},z=e=>p.existsSync(e)?p.readdirSync(e,{withFileTypes:!0}).filter(t=>t.isDirectory()).map(t=>t.name):[];function Gr(e){let t=[];for(let n of z(a.join(e,"packages"))){let o=me(a.join(e,"packages",n));o!==void 0&&t.push(o)}let r=a.join(e,"apps");for(let n of z(r))for(let o of z(a.join(r,n))){if(Lt.has(o))continue;let i=a.join(r,n,o);if(!p.existsSync(a.join(i,"package.json")))continue;let c=me(i);c!==void 0&&t.push(c)}return t}function Vr(e){return e.map(t=>a.join(t.path,"dist")).filter(t=>p.existsSync(t))}function qr(e,t,r){let n=a.normalize(e),o=r.find(c=>n.startsWith(a.normalize(c)));return o?{kind:"package",packageDir:o}:{kind:"app",app:t.find(c=>n.startsWith(a.normalize(c)))}}var Mt={"*/api":{},"*/ui":{}},ve=e=>{let t=e.split("/"),[r,n]=t;return t.length!==2||!r?`devServersPresets: invalid target "${e}" (expected "<app>/api" or "<app>/ui")`:n!=="api"&&n!=="ui"?`devServersPresets: invalid target part "${n}" in "${e}" (expected "api" or "ui")`:null},Ot=e=>{let t=ve(e);if(t)throw new Error(t);let[r,n]=e.split("/");return{appGlob:r,part:n}},Ur=e=>{let t=[];for(let[r,n]of Object.entries(e))for(let o of Object.keys(n.apps??{})){let i=ve(o);i&&t.push({preset:r,key:o,message:`preset "${r}": ${i}`})}return t},Kt=(e,t)=>t==="api"?e.api:e.ui,Wt=e=>[...new Set([...e.api,...e.ui])].sort(),ye=(e,t)=>`${e}/${t}`,Ft=e=>e?0:1,zt=(e,t)=>t.watchDeps===void 0?e??{watchDeps:!0,rank:t.rank,explicitValue:!1}:!e||!e.explicitValue?{watchDeps:t.watchDeps,rank:t.rank,explicitValue:!0}:t.rank>e.rank?{watchDeps:t.watchDeps,rank:t.rank,explicitValue:!0}:e,Gt=(e,t,r)=>{let{app:n,part:o,watchDeps:i,isGlob:c}=r;if(!Kt(t,o).includes(n)){c||e.unmatched.push(ye(n,o));return}let u=ye(n,o),l=zt(e.targets.get(u),{watchDeps:i,rank:Ft(c)});e.targets.set(u,{app:n,part:o,...l})},Vt=(e,t)=>{let r=Object.entries(e.apps??Mt),n={targets:new Map,proxy:{},unmatched:[]};for(let[c,u]of r){let{appGlob:l,part:h}=Ot(c),y=l==="*",S=y?Wt(t):[l];for(let I of S)Gt(n,t,{app:I,part:h,watchDeps:u.watchDeps,isGlob:y}),u.proxy&&(n.proxy[I]={...n.proxy[I]??{},...u.proxy})}let o=[...n.targets.values()].map(({app:c,part:u,watchDeps:l})=>({app:c,part:u,watchDeps:l})),i=[...new Set(o.filter(c=>c.part==="api").map(c=>c.app))];return{targets:o,cmux:e.cmux??!1,proxy:n.proxy,localApps:i,unmatched:n.unmatched}},Hr=e=>{if(e.preset!=null)return e.preset;if(e.running.length===0)return"nothing";let t=new Set(e.running);return e.discovered.every(n=>t.has(n))?"*":[...t].sort().join(" + ")},qt=(e,t)=>Object.keys(e).find(r=>e[r]===t),Bt=({preset:e,app:t,route:r,launchedPkgs:n,ctx:o})=>{let i=o.routePkg(t,r);if(i===void 0)return{preset:e,app:t,route:r,kind:"unknown-route",message:`preset "${e}": proxy override "${r}" on "${t}" names a route not declared in ${t}'s infra-kit.config.ts dev.proxy.routes`};if(n.has(i))return null;let c=qt(o.apiPkgByApp,i),u=c?`add "${c}/api" to the preset`:`launch the api whose package is "${i}"`;return{preset:e,app:t,route:r,pkg:i,kind:"backend-not-launched",message:`preset "${e}": proxy override "${r}" \u2192 "local" requires backend "${i}" to run locally, but the preset does not launch it \u2014 ${u}, or set "${r}" to "cloud"`}},Xr=(e,t)=>{let r=[];for(let[n,o]of Object.entries(e)){let i=Vt(o,t.discovered),c=new Set(i.localApps.map(u=>t.apiPkgByApp[u]).filter(u=>u!==void 0));for(let[u,l]of Object.entries(i.proxy))for(let[h,y]of Object.entries(l)){if(y!=="local")continue;let S=Bt({preset:n,app:u,route:h,launchedPkgs:c,ctx:t});S&&r.push(S)}}return r};export{w as a,d as b,U as c,P as d,v as e,T as f,N as g,Se as h,Ce as i,je as j,Ie as k,Ae as l,$e as m,_ as n,De as o,ee as p,Te as q,b as r,L as s,te as t,re as u,Le as v,Me as w,Oe as x,Ke as y,We as z,Fe as A,ze as B,Ge as C,Ve as D,O as E,qe as F,Be as G,Ue as H,V as I,Jt as J,Yt as K,xt as L,ue as M,le as N,wt as O,bt as P,de as Q,Rt as R,Et as S,St as T,Dt as U,Or as V,Kr as W,Wr as X,Fr as Y,zr as Z,Gr as _,Vr as $,qr as aa,ve as ba,Ur as ca,Vt as da,Hr as ea,Xr as fa};
8
- //# sourceMappingURL=chunk-5FYZAIFC.js.map