@agentproto/driver-agent-cli
Version:
@agentproto/driver-agent-cli — AIP-45 AGENT-CLI.md reference implementation. Multi-protocol runner that loads an AGENT-CLI.md manifest, spawns the binary, and dispatches turns through an ACP / MCP / proprietary arm. ACP arm delegates to @agentproto/acp; e
2,932 lines • 117 kB
JavaScript
import { z } from 'zod';
import { Writable, Readable } from 'stream';
import { createAcpClient } from '@agentproto/acp/client';
import { execFile, spawn } from 'child_process';
import { existsSync, readFileSync, mkdirSync, writeFileSync, rmSync, rmdirSync, realpathSync, cpSync, mkdtempSync, lstatSync, unlinkSync, symlinkSync } from 'fs';
import { createInterface } from 'readline';
import { join, isAbsolute, resolve, relative, dirname, delimiter, basename } from 'path';
import { tmpdir, homedir } from 'os';
import { loadAdapterSpawnSandboxConfig, resolveCommandSandbox } from '@agentproto/command-sandbox';
import { pathToFileURL } from 'url';
import { randomUUID, createHash } from 'crypto';
import { createDoctype } from '@agentproto/define-doctype';
/**
* @agentproto/driver-agent-cli v0.1.0-alpha
* AIP-45 AGENT-CLI.md `defineAgentCli` reference implementation.
*/
var ID_PATTERN = /^[a-z0-9][a-z0-9-]{1,63}$/;
var SEMVER_PATTERN = /^\d+\.\d+\.\d+(?:[-+][\w.-]+)?$/;
var TAG_PATTERN = /^[a-z0-9][a-z0-9-]{0,63}$/;
var SEMVER_RANGE_PATTERN = /^[><=^~ \d.\-+\w*xX|, ]+$/;
var ADAPTER_PATTERN = /^@?[a-z0-9][a-z0-9-./]*$/;
var BIN_PATTERN = /^[A-Za-z0-9._\-/]+$/;
var MODE_ID_PATTERN = /^[a-z0-9][a-z0-9-]{0,63}$/;
var OPTION_ID_PATTERN = /^[a-z0-9][a-z0-9_]{0,63}$/;
var CONTINUATION_STRATEGY_IDS = [
"none",
"pinned-session",
"transcript",
"native-resume"
];
var CONTINUATION_KEY_SCOPES = [
"conversation",
"operator",
"user",
"guild",
"workspace"
];
var installMethodSchema = z.discriminatedUnion("method", [
z.object({ method: z.literal("brew"), package: z.string(), experimental: z.boolean().optional() }).strict(),
z.object({ method: z.literal("apt"), package: z.string(), experimental: z.boolean().optional() }).strict(),
z.object({ method: z.literal("dnf"), package: z.string(), experimental: z.boolean().optional() }).strict(),
z.object({ method: z.literal("pacman"), package: z.string(), experimental: z.boolean().optional() }).strict(),
z.object({ method: z.literal("choco"), package: z.string(), experimental: z.boolean().optional() }).strict(),
z.object({ method: z.literal("scoop"), package: z.string(), experimental: z.boolean().optional() }).strict(),
z.object({ method: z.literal("npm"), package: z.string(), global: z.boolean().optional(), experimental: z.boolean().optional() }).strict(),
z.object({ method: z.literal("pip"), package: z.string(), user: z.boolean().optional(), experimental: z.boolean().optional() }).strict(),
z.object({ method: z.literal("cargo"), package: z.string(), experimental: z.boolean().optional() }).strict(),
z.object({ method: z.literal("go"), package: z.string(), experimental: z.boolean().optional() }).strict(),
z.object({
method: z.literal("curl"),
url: z.string().url(),
verify_sha256: z.string().regex(/^[a-f0-9]{64}$/).optional(),
experimental: z.boolean().optional()
}).strict(),
z.object({
method: z.literal("download"),
url: z.string().url(),
extract_bin: z.string(),
verify_sha256: z.string().regex(/^[a-f0-9]{64}$/).optional(),
experimental: z.boolean().optional()
}).strict(),
z.object({ method: z.literal("vendored"), path: z.string(), experimental: z.boolean().optional() }).strict()
]);
var versionCheckSchema = z.object({
cmd: z.string(),
parse: z.string().describe("ECMAScript regex with at least one capture group."),
range: z.string().regex(SEMVER_RANGE_PATTERN).describe("npm-style semver range."),
timeout_ms: z.number().int().min(100).default(5e3)
}).strict();
var upgradeSchema = z.object({
latest: z.union([
z.object({ npm: z.string().min(1) }).strict(),
z.object({ githubRelease: z.string().regex(/^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/) }).strict()
]),
cmd: z.array(z.string()).min(1),
rollback: z.array(z.string()).min(1).optional()
}).strict();
var SETUP_STEP_ID_PATTERN = /^[a-z0-9][a-z0-9-]{0,63}$/;
var SECRET_SLUG_PATTERN = /^([a-z][a-z0-9-]*[a-z0-9]\/)?[a-z][a-z0-9-]*[a-z0-9]$/;
var skipIfSchema = z.object({
cmd: z.string(),
exit_code: z.number().int().default(0),
timeout_ms: z.number().int().min(100).default(5e3)
}).strict();
var persistEnvSchema = z.object({ env: z.string().regex(/^[A-Z][A-Z0-9_]*$/) }).strict();
var persistSecretSchema = z.object({ secret_slug: z.string().regex(SECRET_SLUG_PATTERN).min(2).max(80) }).strict();
var persistCmdSchema = z.object({ cmd: z.string() }).strict();
var setupPersistSchema = z.union([
persistEnvSchema,
persistSecretSchema,
persistCmdSchema
]);
var setupStepSchema = z.discriminatedUnion("kind", [
z.object({
id: z.string().regex(SETUP_STEP_ID_PATTERN),
kind: z.literal("cmd"),
cmd: z.string(),
description: z.string().max(2e3).optional(),
skip_if: skipIfSchema.optional(),
timeout_ms: z.number().int().min(100).default(6e4),
interactive: z.boolean().default(false),
persist: setupPersistSchema.optional()
}).strict(),
z.object({
id: z.string().regex(SETUP_STEP_ID_PATTERN),
kind: z.literal("prompt"),
prompt: z.string().min(1),
description: z.string().max(2e3).optional(),
type: z.enum(["text", "select", "boolean", "secret"]).default("text"),
default: z.string().optional(),
options: z.union([
z.array(z.string().min(1)).min(1),
z.object({
cmd: z.string(),
timeout_ms: z.number().int().min(100).default(3e4)
}).strict()
]).optional(),
skip_if: skipIfSchema.optional(),
persist: setupPersistSchema.optional()
}).strict(),
z.object({
id: z.string().regex(SETUP_STEP_ID_PATTERN),
kind: z.literal("oauth"),
secret_slug: z.string().regex(SECRET_SLUG_PATTERN).min(2).max(80),
description: z.string().max(2e3).optional(),
skip_if: skipIfSchema.optional()
}).strict(),
z.object({
id: z.string().regex(SETUP_STEP_ID_PATTERN),
kind: z.literal("external"),
url: z.string().url(),
description: z.string().max(2e3).optional(),
callback: z.object({
param: z.string().min(1).optional(),
timeout_ms: z.number().int().min(1e3).default(6e5)
}).strict().optional(),
skip_if: skipIfSchema.optional(),
persist: setupPersistSchema.optional()
}).strict()
]);
var authSchema = z.object({
ref: z.string().optional(),
state: z.object({
paths: z.array(z.string()).optional(),
env: z.array(z.string()).optional()
}).strict().optional(),
login: z.object({
cmd: z.string(),
interactive: z.boolean().optional(),
requires_callback_url: z.boolean().optional()
}).strict().optional(),
refresh: z.object({
cmd: z.string(),
interval_s: z.number().int().positive().optional()
}).strict().optional(),
expiry: z.object({
parse: z.string().optional(),
grace_s: z.number().int().nonnegative().optional()
}).strict().optional()
}).strict();
var boxLoginSchema = z.object({
// Provision-recipe id naming the login file (and the destination path,
// unless `file` overrides it home-relative).
recipe: z.string().min(1),
// Multi-surface recipe flavor (opencode: anthropic-oauth / openai-oauth).
methodId: z.string().min(1).optional(),
// Allowlist of dotted paths retained in the copy — must be non-empty: an
// empty one would copy nothing and the box's own verify would just fail.
keep: z.array(z.string().min(1)).min(1),
// Dotted path of the login's expiry inside the raw JSON (epoch s/ms/date).
expiresPath: z.string().min(1).optional(),
// Home-relative destination inside the box; absent ⇒ derived from the
// recipe's own `file` source. Absolute / `..` paths are rejected at copy
// time by the bridge, never written.
file: z.string().min(1).optional()
}).strict();
var authSubscriptionEntrySchema = z.object({
setEnv: z.string().min(1).optional(),
external: z.boolean().optional(),
conflictEnv: z.array(z.string()).optional(),
unsetEnvAdd: z.array(z.string()).optional(),
// Provider scope for a multi-provider adapter's bearer surface (pi's
// ANTHROPIC_OAUTH_TOKEN is anthropic-only) — see AgentCliAuthSubscription.
provider: z.string().min(1).optional(),
boxLogin: boxLoginSchema.optional()
}).strict().superRefine((v, ctx) => {
if (v.external) {
if (v.setEnv !== void 0) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "authSubscription.external: true must NOT declare setEnv \u2014 an external (file-based) subscription injects no bearer; the CLI reads its own login file.",
path: ["setEnv"]
});
}
} else if (v.setEnv === void 0) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "authSubscription requires setEnv unless external: true (the bearer must be injected into some env var).",
path: ["setEnv"]
});
}
if (v.boxLogin && v.external !== true) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "authSubscription.boxLogin requires external: true \u2014 only a file-based (external) login has a file to bridge into a box; a bearer surface crosses the boundary as an injected env var instead.",
path: ["boxLogin"]
});
}
});
var authSubscriptionSchema = z.union([authSubscriptionEntrySchema, z.array(authSubscriptionEntrySchema).min(1)]).superRefine((v, ctx) => {
if (!Array.isArray(v)) return;
const seen = /* @__PURE__ */ new Set();
for (const entry of v) {
const key = entry.provider ?? "\0unscoped";
if (seen.has(key)) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: entry.provider ? `authSubscription: duplicate provider scope "${entry.provider}" \u2014 two entries claim the same provider, which is ambiguous at spawn time.` : "authSubscription: more than one unscoped entry (no `provider`) \u2014 ambiguous which one applies at spawn time."
});
}
seen.add(key);
}
});
var sessionSchema = z.object({
mode: z.enum(["ephemeral", "persistent", "resumable"]).default("ephemeral"),
idle_timeout_ms: z.number().int().min(1e3).default(6e5),
// No default: undefined disables the per-turn watchdog entirely. Only
// adapters known to sometimes drop the final `prompt` response (e.g.
// hermes) should declare this — applying it broadly would risk
// false-positiving on another adapter's legitimately long turns.
turn_idle_timeout_ms: z.number().int().min(1e3).optional(),
max_turns: z.number().int().positive().optional(),
context_carryover: z.boolean().default(true)
}).strict();
var modelEntrySchema = z.union([
z.string(),
z.object({
id: z.string(),
provider: z.string().optional(),
mode: z.string().optional()
}).strict()
]);
var modelsSchema = z.object({
default: z.string().optional(),
allowed: z.array(modelEntrySchema).optional(),
// Model-id patterns the adapter must never route to (case-insensitive,
// trailing `*` = prefix match). Enforced at compose time — see
// AgentCliModels.deny. Reserves premium providers for dedicated adapters.
deny: z.array(z.string()).optional(),
env: z.record(z.string(), z.string()).optional(),
// How a model is selected at session start: "config" (ACP
// set_config_option, default) | "command" (a `/model <id>` control turn,
// for agents like hermes that ignore the ACP session model config) |
// "arg" (a CLI argument composed into bin_args at spawn).
apply: z.enum(["config", "command", "arg"]).optional(),
// Argv template for apply:"arg" — {model} interpolates the requested
// model id. See AgentCliModels.bin_args_template.
bin_args_template: z.array(z.string()).optional()
}).strict();
var capabilitiesSchema = z.object({
streaming: z.boolean().optional(),
tool_calls: z.boolean().optional(),
sub_agents: z.boolean().optional(),
file_io: z.boolean().optional(),
multimodal: z.boolean().optional(),
resumable: z.boolean().optional(),
bidirectional: z.boolean().optional(),
/**
* Adapter has a verified native CLI resume into a real terminal/TUI
* session (e.g. `claude --resume <id>` or `hermes --resume <id> --tui`).
* When true, the host MAY restart this adapter via its native resume argv
* as a PTY session. Distinct from ACP-level `resumable`.
*/
nativeTerminalResume: z.boolean().optional(),
/**
* Harness defers mounted MCP tools behind its own native search tool
* (Claude Code `ToolSearch`). Makes the daemon's `/mcp` self-mount default
* to eager for this adapter instead of double-deferring behind the
* daemon's `tool_search`. Defaults to false (conservative).
*/
nativeToolSearch: z.boolean().optional(),
/**
* Adapter can ingest a filesystem path that the host UI just placed
* on disk (host-side drag-drop into a terminal pastes the path
* here). Implies the adapter has a Read-file tool wired so the
* dropped path actually does something. Hosts gate drag-drop UI on
* this flag — defaults to false (conservative).
*/
file_attach: z.boolean().optional()
}).strict();
var modeSchema = z.object({
id: z.string().regex(MODE_ID_PATTERN),
description: z.string().optional(),
// Which orthogonal session-config axis this mode expresses (SPEC-tracked in
// @agentproto/runtime's session-config.ts). Narrowed to only "context" (SPEC
// §3.4a): route is derived from the model catalog (@route), not a manifest
// mode, and posture comes from the harness's own ACP mode registry
// (SessionModeState.availableModes) — neither is a `modes[]` entry anymore.
// The legacy "posture"/"route" classification still exists, but only inside
// the daemon's back-compat `decomposeMode` shim, not this manifest surface.
kind: z.enum(["context"]).optional(),
bin_args_prepend: z.array(z.string()).optional(),
bin_args_append: z.array(z.string()).optional(),
env: z.record(z.string(), z.string()).optional(),
// Env keys to DELETE from the spawn env (auth hygiene for gateway
// modes — see AgentCliMode.env_unset). Applied at the runtime merge
// point, not as a static set.
env_unset: z.array(z.string()).optional(),
// Honest support status surfaced to clients. Absent ⇒ "active".
status: z.enum(["active", "noop", "planned"]).optional(),
status_note: z.string().optional(),
// How the host activates this mode: "bin_args" (default, argv/env
// composed at spawn) | "config" (no CLI surface — forwarded to the ACP
// arm's connect({mode}) and applied via session/set_config_option
// configId:"mode", e.g. opencode).
apply: z.enum(["bin_args", "config"]).optional()
}).strict();
var optionSchema = z.object({
id: z.string().regex(OPTION_ID_PATTERN),
type: z.enum(["boolean", "integer", "string", "enum"]),
description: z.string().optional(),
enum: z.array(z.string()).min(1).optional(),
default: z.union([z.boolean(), z.number(), z.string()]).optional(),
min: z.number().int().optional(),
max: z.number().int().optional(),
bin_args_prepend: z.array(z.string()).optional(),
bin_args_template: z.array(z.string()).optional(),
bin_args_append_when_true: z.array(z.string()).optional(),
env: z.record(z.string(), z.string()).optional(),
// Env keys to DELETE from the spawn env when this option is active
// (auth hygiene for value-bearing gateway options — see
// AgentCliOption.env_unset). Symmetric with modeSchema.env_unset.
env_unset: z.array(z.string()).optional()
}).strict().refine(
// type === "enum" REQUIRES an `enum` array. The JSON Schema enforces
// this via `if/then`; the zod side mirrors with a refine so both
// validation paths reject the same shapes.
(o) => o.type !== "enum" || Array.isArray(o.enum) && o.enum.length > 0,
{ message: "option.type === 'enum' requires a non-empty `enum` array" }
);
var presetSchema = z.object({
id: z.string().min(1),
label: z.string().min(1),
description: z.string().optional(),
schemaFlavor: z.enum(["anthropic", "openai"]),
baseUrl: z.string().min(1),
keyEnv: z.string().min(1),
scrubEnv: z.array(z.string()).optional(),
defaultModel: z.string().optional(),
homepage: z.string().optional()
}).strict();
var continuationStrategyIdSchema = z.enum(CONTINUATION_STRATEGY_IDS);
var continuationSchema = z.object({
default: continuationStrategyIdSchema,
supported: z.array(continuationStrategyIdSchema).min(1),
pinned_session: z.object({
idle_timeout_ms: z.number().int().min(1e3).default(18e5),
key_scope: z.array(z.enum(CONTINUATION_KEY_SCOPES)).min(1).default(["conversation", "operator"])
}).strict().optional()
}).strict().refine(
// The chosen `default` must appear in `supported` — otherwise the
// host has no valid strategy to fall back to.
(c) => c.supported.includes(c.default),
{
message: "continuation.default must be a member of continuation.supported"
}
);
var mcpBlockSchema = z.object({
command: z.string().optional(),
args: z.array(z.string()).optional(),
transport: z.enum(["stdio", "http", "sse"]),
url: z.string().url().optional()
}).strict();
var printConfigSchema = z.object({
prompt_flag: z.string().optional(),
output_format: z.array(z.string()).optional(),
pre_prompt: z.array(z.string()).optional(),
resume: z.object({
flag: z.string(),
kind: z.enum(["value", "boolean"])
}).strict().optional(),
event_schema: z.enum(["claude-stream-json", "mastra-jsonl", "antigravity-stream-json", "jcode-ndjson"]).optional()
}).strict().optional();
var agentCliFrontmatterSchema = z.object({
name: z.string().min(1).max(80),
id: z.string().regex(ID_PATTERN),
description: z.string().min(1).max(2e3),
version: z.string().regex(SEMVER_PATTERN),
bin: z.string().regex(BIN_PATTERN),
bin_args: z.array(z.string()).optional(),
// Always-on spawn env (merged before mode/option env, which can
// override). See AgentCliDefinition.env — used by generic ACP agents.
env: z.record(z.string(), z.string()).optional(),
install: z.array(installMethodSchema).min(1),
version_check: versionCheckSchema,
// In-place harness upgrade recipe — see AgentCliUpgrade. Optional: a
// pinned-npx adapter (or a curl-only install) declares nothing here and
// `harness upgrade` refuses it rather than improvising an installer.
upgrade: upgradeSchema.optional(),
setup: z.array(setupStepSchema).optional(),
auth: authSchema.optional(),
// Billing-auth resolver inputs (DECISION 3). `provider` is a CatalogProvider
// id (validated against the catalog by the runtime resolver, not here, to
// keep this generic doctype decoupled from @agentproto/model-catalog).
provider: z.string().min(1).optional(),
authSubscription: authSubscriptionSchema.optional(),
// How this adapter receives a GATEWAY-routed bearer credential — see the
// doc on AgentCliDefinition.gatewayAuth. Distinct from a gateway preset's
// `keyEnv` (the providers-store lookup key) and from
// authSubscription.setEnv (this adapter's own native bearer).
gatewayAuth: z.object({
setEnv: z.string().min(1)
}).strict().optional(),
// Auth-derivation axis (DECISION 3): true when the adapter's api-key auth
// is derived from the requested model rather than a fixed `provider`
// (e.g. `pi`, `opencode`, `mastracode`). When set, the runtime resolver
// allows `"api-key"` mode on the model-derived direct endpoint and
// includes it in eligibility manifests for by-model routers. DISTINCT
// from `routeSelection` (the UI-facing route-choice axis).
modelDerivedApiKey: z.boolean().optional(),
// UI-facing route-choice axis (AIP-45 launch-menu drill-down, WP1).
// Absent ⇒ "free" (route is an independent choice); "derived-from-model"
// means the endpoint falls out of the model id's vendor prefix. DISTINCT
// from `modelDerivedApiKey` (the auth-derivation axis) — see the field
// doc on AgentCliDefinition.routeSelection.
routeSelection: z.enum(["free", "derived-from-model"]).optional(),
authEnforce: z.enum(["always", "when-configured"]).optional(),
sandbox: z.union([z.string(), z.record(z.string(), z.unknown())]),
stateHome: z.object({
env: z.string().regex(/^[A-Z][A-Z0-9_]*$/),
defaultDir: z.string().min(1).refine((d) => !d.startsWith("/") && !d.split("/").includes(".."), {
message: "defaultDir must be relative to $HOME and stay inside it"
}),
share: z.array(z.string().regex(/^[^/]+$/)).optional(),
seed: z.record(z.string().regex(/^[^/]+$/).refine((n) => n !== "." && n !== "..", { message: "seed names a file of the home" }), z.string()).optional()
}).strict().optional(),
credentialDataHome: z.object({ env: z.string().regex(/^[A-Z][A-Z0-9_]*$/) }).strict().optional(),
smallModel: z.object({
env: z.string().regex(/^[A-Z][A-Z0-9_]*$/),
key: z.string().min(1),
option: z.string().regex(OPTION_ID_PATTERN).optional(),
userConfig: z.object({
globalFiles: z.array(z.string().min(1)).optional(),
fileEnv: z.array(z.string().regex(/^[A-Z][A-Z0-9_]*$/)).optional(),
dirEnv: z.array(z.string().regex(/^[A-Z][A-Z0-9_]*$/)).optional(),
projectFiles: z.array(z.string().min(1)).optional(),
dirFiles: z.array(z.string().min(1)).optional(),
projectDisableEnv: z.string().regex(/^[A-Z][A-Z0-9_]*$/).optional()
}).strict().optional()
}).strict().optional(),
// How this CLI takes extra system-prompt text for a session (see
// AgentCliSystemPrompt in types.ts). Absent ⇒ no channel.
systemPrompt: z.union([
z.object({ via: z.literal("acp-meta") }).strict(),
z.object({
via: z.literal("inline-config"),
env: z.string().regex(/^[A-Z][A-Z0-9_]*$/),
key: z.string().min(1),
shape: z.enum(["file-list", "text"])
}).strict(),
z.object({ via: z.literal("argv"), flag: z.string().min(1) }).strict(),
z.object({
via: z.literal("env-file"),
env: z.string().regex(/^[A-Z][A-Z0-9_]*$/),
maxChars: z.number().int().positive().optional()
}).strict()
]).optional(),
runner: z.union([z.string(), z.record(z.string(), z.unknown())]).optional(),
protocol: z.enum(["acp", "mcp", "proprietary", "print"]),
acp: z.string().optional(),
mcp: mcpBlockSchema.optional(),
adapter: z.string().regex(ADAPTER_PATTERN).optional(),
print: printConfigSchema,
session: sessionSchema.optional(),
models: modelsSchema.optional(),
capabilities: capabilitiesSchema.optional(),
modes: z.array(modeSchema).optional(),
options: z.array(optionSchema).optional(),
// AIP-45 gateway presets this adapter can drive (merged into the
// provider-preset catalog; built-in wins on id collision).
presets: z.array(presetSchema).optional(),
continuation: continuationSchema.optional(),
requires: z.object({
os: z.array(z.enum(["darwin", "linux", "windows"])).optional(),
arch: z.array(z.enum(["x64", "arm64", "x86", "arm"])).optional(),
min_disk_mb: z.number().int().nonnegative().optional(),
min_memory_mb: z.number().int().nonnegative().optional()
}).strict().optional(),
examples: z.array(z.object({
goal: z.string(),
prompt: z.string(),
note: z.string().optional()
}).strict()).optional(),
tags: z.array(z.string().regex(TAG_PATTERN)).optional(),
metadata: z.record(z.string(), z.unknown()).optional()
}).strict().superRefine((m, ctx) => {
if (m.modes) {
const seen = /* @__PURE__ */ new Set();
for (const mode of m.modes) {
if (seen.has(mode.id)) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
path: ["modes"],
message: `Duplicate mode id '${mode.id}' \u2014 mode ids must be unique within a manifest.`
});
}
seen.add(mode.id);
}
}
if (m.setup) {
const seen = /* @__PURE__ */ new Set();
for (const step of m.setup) {
if (seen.has(step.id)) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
path: ["setup"],
message: `Duplicate setup step id '${step.id}' \u2014 step ids must be unique within a manifest.`
});
}
seen.add(step.id);
}
}
if (m.options) {
const seen = /* @__PURE__ */ new Set();
for (const opt of m.options) {
if (seen.has(opt.id)) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
path: ["options"],
message: `Duplicate option id '${opt.id}' \u2014 option ids must be unique within a manifest.`
});
}
seen.add(opt.id);
}
}
if (m.continuation?.default === "native-resume" && !m.capabilities?.resumable) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
path: ["continuation", "default"],
message: "continuation.default === 'native-resume' requires capabilities.resumable: true."
});
}
}).describe(
"Validates the YAML frontmatter portion of an AIP-45 AGENT-CLI.md manifest."
);
var runtimeConfigSchema = z.object({
mode: z.string().optional(),
options: z.record(z.string(), z.union([z.boolean(), z.number(), z.string()])).optional(),
continuation: continuationStrategyIdSchema.optional()
}).strict();
var autoAllowPermissionHandler = ({
options
}) => {
if (!options || options.length === 0) {
return { outcome: { outcome: "cancelled" } };
}
const allow = options.find((o) => typeof o.kind === "string" && o.kind.startsWith("allow_")) ?? options[0];
return { outcome: { outcome: "selected", optionId: allow.optionId } };
};
var PLAN_MODE_EXIT_TOOL_CALL_KIND = "switch_mode";
var planModePermissionHandler = (params) => {
const { options, toolCall } = params;
if (!options || options.length === 0) {
return { outcome: { outcome: "cancelled" } };
}
if (toolCall?.kind === PLAN_MODE_EXIT_TOOL_CALL_KIND) {
const keepPlanning = options.find((o) => o.kind === "reject_once");
return keepPlanning ? { outcome: { outcome: "selected", optionId: keepPlanning.optionId } } : { outcome: { outcome: "cancelled" } };
}
return autoAllowPermissionHandler(params);
};
function defaultPermissionHandlerForMode(requestedMode) {
return requestedMode === "plan" ? planModePermissionHandler : autoAllowPermissionHandler;
}
function createAcpProtocolArm(options) {
const { child, cwd } = options;
if (!child.stdin || !child.stdout) {
throw new Error(
"AcpProtocolArm: subprocess must be spawned with piped stdin + stdout"
);
}
const output = Writable.toWeb(child.stdin);
const input = Readable.toWeb(child.stdout);
let client = null;
let session = null;
const pendingByTurn = /* @__PURE__ */ new Map();
const earlyDisconnectListeners = [];
return {
get sessionId() {
return session?.sessionId;
},
get availableConfigOptions() {
return session?.availableConfigOptions ?? [];
},
get modelApplyRejection() {
return session?.modelApplyRejection;
},
get availableModes() {
return session?.availableModes ?? [];
},
get currentModeId() {
return session?.currentModeId;
},
isConnected() {
return client?.isConnected() ?? true;
},
onDisconnect(listener) {
if (client) client.onDisconnect(listener);
else earlyDisconnectListeners.push(listener);
},
async connect(opts) {
const permissionHandler = options.onPermissionRequest ?? defaultPermissionHandlerForMode(options.requestedMode);
client = await createAcpClient({
output,
input,
clientInfo: options.clientInfo,
capabilities: {
fs: { readTextFile: true, writeTextFile: true }
},
onActivity: opts.onActivity,
turnIdleTimeoutMs: opts.turnIdleTimeoutMs,
// Permission-hold mode: surface + park requests for the daemon inbox
// instead of auto-answering them in-arm (see AcpProtocolOptions).
...options.permissionHold ? { permissionHold: true } : {},
// Wire the permission handler so the agent's `session/request_permission`
// callbacks get a real answer instead of bubbling up as
// "AcpClient.requestPermission: no handler configured" → which
// surfaces in the chat as an opaque "Internal error" when the
// agent tries to Write / Bash anything gated.
// `params` is contextually typed as the SDK's `RequestPermissionRequest`
// (via `AcpClientOptions.handlers`), which is assignable to
// `AcpPermissionRequestParams` — so no cast is needed on either side.
handlers: {
requestPermission: async (params) => permissionHandler(params)
}
});
for (const listener of earlyDisconnectListeners.splice(0)) {
client.onDisconnect(listener);
}
if (opts.resumeSessionId) {
session = await client.loadSession({
sessionId: opts.resumeSessionId,
cwd,
mcpServers: opts.mcpServers,
// Re-assert the requested model/effort/mode on the RESUMED session:
// a cold-resume spawns a fresh subprocess whose in-memory config
// starts from the harness default, and opencode persists+restores its
// own session model. Omitting these let a restarted opencode session
// drift to its default `opencode/big-pickle` (a rate-limited free id)
// while agentproto still reported the requested model/route.
...opts.model ? { model: opts.model } : {},
...opts.effort ? { effort: opts.effort } : {},
...opts.mode ? { mode: opts.mode } : {},
// claude-agent-acp re-reads `_meta.systemPrompt` on loadSession too
// (native-resume cold-spawns every turn) — sent on BOTH calls or
// the text is lost after turn 1.
...opts.appendSystemPrompt ? { meta: { systemPrompt: { append: opts.appendSystemPrompt } } } : {}
});
} else {
session = await client.newSession({
cwd,
mcpServers: opts.mcpServers,
...opts.model ? { model: opts.model } : {},
...opts.effort ? { effort: opts.effort } : {},
...opts.mode ? { mode: opts.mode } : {},
...opts.appendSystemPrompt ? { meta: { systemPrompt: { append: opts.appendSystemPrompt } } } : {}
});
}
},
async send(turnId, message) {
if (!session) throw new Error("AcpProtocolArm.send: not connected");
const messages = Array.isArray(message) ? message : [message];
const stream = session.prompt({ messages });
pendingByTurn.set(turnId, stream);
},
async *events() {
const last = Array.from(pendingByTurn.values()).at(-1);
if (!last) return;
for await (const evt of last) yield evt;
},
async cancel(_turnId) {
if (!session) return;
await session.cancel();
},
async setConfigOption(configId, value) {
if (!session) return { applied: false, reason: "not-connected" };
return session.setConfigOption(configId, value);
},
async setSessionMode(modeId) {
if (!session) return { applied: false, reason: "not-connected" };
return session.setSessionMode(modeId);
},
onOutOfTurnEvent(listener) {
if (!session) return () => {
};
return session.onOutOfTurnEvent(listener);
},
get steeringSupported() {
return session?.steeringSupported ?? false;
},
async steer(content) {
if (!session) return "promptRequired";
return session.steer(content);
},
respondPermission(requestId, resolution) {
return client?.respondPermission(requestId, resolution) ?? false;
},
async close() {
if (session) await session.close();
if (client) await client.close();
}
};
}
// src/mcp-servers.ts
function toFileBasedMcpServers(servers) {
if (!servers || servers.length === 0) return void 0;
const out = {};
for (const s of servers) {
out[s.name] = s.transport === "stdio" ? {
command: s.ref ?? "",
...s.args !== void 0 ? { args: s.args } : {},
...s.env !== void 0 ? { env: s.env } : {}
} : { url: s.ref ?? "", ...s.headers !== void 0 ? { headers: s.headers } : {} };
}
return out;
}
var DEFAULT_GRACE_MS = 3e3;
var DEFAULT_POLL_MS = 100;
function descendantsOf(rootPid, rows) {
const byParent = /* @__PURE__ */ new Map();
for (const r of rows) {
const list = byParent.get(r.ppid);
if (list) list.push(r.pid);
else byParent.set(r.ppid, [r.pid]);
}
const out = [];
const seen = /* @__PURE__ */ new Set([rootPid]);
const queue = [rootPid];
while (queue.length > 0) {
const pid = queue.shift();
for (const kid of byParent.get(pid) ?? []) {
if (seen.has(kid)) continue;
seen.add(kid);
out.push(kid);
queue.push(kid);
}
}
return out;
}
async function terminateChildTree(child, opts = {}) {
const graceMs = opts.graceMs ?? DEFAULT_GRACE_MS;
const pollMs = opts.pollMs ?? DEFAULT_POLL_MS;
const list = opts.listProcesses ?? listProcesses;
const rootPid = child.pid;
const rootAlive = () => child.exitCode === null && child.signalCode === null;
if (typeof rootPid !== "number" || rootPid <= 0 || rootPid === process.pid || !rootAlive()) {
return { descendants: [], killed: [] };
}
const descendants = descendantsOf(rootPid, await list()).filter((p) => p !== process.pid);
const tracked = new Set(descendants);
try {
child.kill("SIGTERM");
} catch {
}
for (const pid of tracked) signal(pid, "SIGTERM");
const anyAlive = () => rootAlive() || [...tracked].some(isAlive);
const deadline = Date.now() + graceMs;
while (anyAlive() && Date.now() < deadline) {
await new Promise((r) => setTimeout(r, pollMs));
}
if (!anyAlive()) return { descendants, killed: [] };
const rows = await list();
const survivors = [...tracked].filter(isAlive);
for (const from of rootAlive() ? [rootPid, ...survivors] : survivors) {
for (const pid of descendantsOf(from, rows)) {
if (pid !== process.pid) tracked.add(pid);
}
}
const killed = [];
if (rootAlive()) {
try {
child.kill("SIGKILL");
killed.push(rootPid);
} catch {
}
}
for (const pid of tracked) {
if (isAlive(pid) && signal(pid, "SIGKILL")) killed.push(pid);
}
return { descendants, killed };
}
function signal(pid, sig) {
try {
process.kill(pid, sig);
return true;
} catch {
return false;
}
}
function isAlive(pid) {
try {
process.kill(pid, 0);
return true;
} catch (err) {
return err.code === "EPERM";
}
}
function listProcesses() {
return new Promise((resolve4) => {
execFile("ps", ["-axo", "pid=,ppid="], { maxBuffer: 32 * 1024 * 1024 }, (err, stdout) => {
if (err) return resolve4([]);
const out = [];
for (const line of stdout.split("\n")) {
const m = line.match(/^\s*(\d+)\s+(\d+)\s*$/);
if (m) out.push({ pid: Number(m[1]), ppid: Number(m[2]) });
}
resolve4(out);
});
});
}
function isWindowsBatchFile(exe, deps) {
if ((process.platform) !== "win32") return false;
return /\.(?:bat|cmd)$/i.test(exe);
}
function windowsBatchShellOption(exe, deps) {
if (isWindowsBatchFile(exe)) return { shell: true };
return {};
}
function resolveWindowsBatchSpawn(bin, args, deps) {
if ((process.platform) !== "win32") return void 0;
if (!isWindowsBatchFile(bin)) return void 0;
const stem = basename(bin).replace(/\.(?:bat|cmd)$/i, "");
if (stem !== "npx" && stem !== "npm") return void 0;
const script = join(dirname(bin), "node_modules", "npm", "bin", `${stem}-cli.js`);
if (!(existsSync)(script)) return void 0;
return { bin: process.execPath, args: [script, ...args] };
}
function defaultToolchainWritePaths() {
const home = homedir();
return [
join(home, "Library", "pnpm", ".tools"),
// macOS pnpm self-managed toolchain
join(home, ".local", "share", "pnpm"),
// Linux pnpm self-managed toolchain
join(home, ".npm"),
// npm cache — self-update / global installs
join(home, ".claude", "session-env")
// claude-code's own Bash-tool session bookkeeping
];
}
function defaultToolchainReadPaths() {
const home = homedir();
return [
join(home, ".npmrc"),
join(home, "Library", "Preferences", "pnpm", "rc"),
join(home, ".gitconfig"),
// git's own global config (credential.helper, user.*, …)
join(home, ".config", "git"),
// XDG git config fallback
join(home, ".config", "gh"),
// gh's config.yml + hosts.yml
join(home, "Library", "Keychains")
// macOS Keychain DB — git/gh credential retrieval (read-only; see doc above)
];
}
function effectiveSpawnSandboxMode(opts, configured) {
return opts.zones ? opts.mode ?? "workspace" : opts.mode ?? configured;
}
async function willConfineAgentCliSpawn(opts) {
const cfg = await loadAdapterSpawnSandboxConfig(opts.cwd);
const mode = effectiveSpawnSandboxMode(opts, cfg.mode);
return mode !== void 0 && mode !== "off";
}
async function wrapAgentCliSpawn(bin, args, opts) {
const cfg = await loadAdapterSpawnSandboxConfig(opts.cwd);
const mode = effectiveSpawnSandboxMode(opts, cfg.mode);
if (mode === void 0) return [bin, args];
if (mode === "off" && opts.zones) {
throw new Error(
`agent-cli '${opts.label}': filesystem zones (an app boundary) cannot be combined with commandSandbox "off" \u2014 the zones are only enforceable through the OS sandbox. Refusing to spawn.`
);
}
if (mode === "off") {
console.error(
`[agent-cli] \u26A0 spawning '${opts.label}' UNCONFINED \u2014 no OS-level sandbox is active (commandSandbox mode is "off").`
);
return [bin, args];
}
const backend = resolveCommandSandbox();
if (!backend) {
throw new Error(
`agent-cli '${opts.label}': commandSandbox mode="${mode}" is configured but no sandbox backend is available on ${process.platform} (macOS needs sandbox-exec, Linux needs bwrap installed). Refusing to spawn unconfined \u2014 pass commandSandbox: "off" to explicitly accept unconfined execution, or install the missing backend.`
);
}
const fileCfg = opts.zones ? void 0 : cfg;
const policy = {
workspace: opts.cwd,
extraReadPaths: [
...defaultToolchainReadPaths(),
...fileCfg?.extraReadPaths ?? [],
...opts.extraReadPaths ?? []
],
extraWritePaths: [
...defaultToolchainWritePaths(),
...fileCfg?.extraWritePaths ?? [],
...opts.extraWritePaths ?? []
],
...opts.zones ? { zones: opts.zones } : {},
network: mode === "strict" || cfg.network === "deny" ? "deny" : "allow"
};
const wrapped = backend.wrap([bin, ...args], policy);
return [wrapped[0] ?? bin, wrapped.slice(1)];
}
// src/protocol/print-arm.ts
function printModelMismatch(requested, reported) {
const basename3 = (id) => {
const noSuffix = id.split("@")[0] ?? id;
const segs = noSuffix.split("/");
return (segs[segs.length - 1] ?? noSuffix).trim().toLowerCase();
};
return basename3(requested) !== basename3(reported);
}
var DEFAULT_OUTPUT = ["--output-format", "stream-json"];
var DEFAULT_PRE_PROMPT = ["--no-interactive"];
var DEFAULT_RESUME = { flag: "--resume", kind: "value" };
function createPrintSession(opts) {
const config = opts.printConfig;
const outputFormat = config?.output_format ?? DEFAULT_OUTPUT;
const prePrompt = config?.pre_prompt ?? DEFAULT_PRE_PROMPT;
const promptFlag = config?.prompt_flag;
const resumeCfg = config?.resume ?? DEFAULT_RESUME;
const eventSchema = config?.event_schema ?? "claude-stream-json";
let sessionId = opts.resumeSessionId ?? "";
let activeChild = null;
const mcpRestore = setupMcpConfigFile(opts, eventSchema);
return {
get sessionId() {
return sessionId;
},
async *send(message) {
const prompt = extractPromptText(message);
const args = [
...opts.baseArgs,
...outputFormat,
...prePrompt
];
if (sessionId) {
args.push(resumeCfg.flag);
if (resumeCfg.kind === "value") args.push(sessionId);
}
if (promptFlag) {
args.push(promptFlag, prompt);
} else {
args.push(prompt);
}
const [execBin, execArgs] = await wrapAgentCliSpawn(opts.bin, args, {
mode: opts.commandSandbox,
cwd: opts.cwd,
...opts.extraWritePaths ? { extraWritePaths: opts.extraWritePaths } : {},
...opts.extraReadPaths ? { extraReadPaths: opts.extraReadPaths } : {},
...opts.fsZones ? { zones: opts.fsZones } : {},
label: "print-arm"
});
const child = spawn(execBin, execArgs, {
cwd: opts.cwd,
env: opts.env,
stdio: ["ignore", "pipe", "pipe"],
// Win32: only reached for a `.cmd`/`.bat` bin that
// `resolveWindowsBatchSpawn` couldn't rewrite to its node entry
// (see `win32-spawn.ts` — Node refuses direct batch spawning,
// CVE-2024-27980, the recap-10 `spawn EINVAL`). POSIX: `{}`.
...windowsBatchShellOption(execBin)
});
activeChild = child;
let spawnFailure;
await new Promise((resolve4) => {
child.once("spawn", () => resolve4());
child.once("error", (err) => {
spawnFailure = err;
resolve4();
});
});
if (spawnFailure) {
activeChild = null;
yield {
kind: "error",
error: { message: `failed to spawn '${execBin} ${execArgs.join(" ")}': ${spawnFailure.message}` }
};
return;
}
const stderrLines = [];
const STDERR_KEEP = 80;
child.stderr?.setEncoding("utf8");
child.stderr?.on("data", (chunk) => {
for (const line of chunk.split(/\r?\n/)) {
if (!line) continue;
stderrLines.push(line);
if (stderrLines.length > STDERR_KEEP) stderrLines.shift();
}
});
try {
if (!child.stdout)
throw new Error("print-arm: child has no stdout pipe");
let capturedSessionId = "";
const mastraState = eventSchema === "mastra-jsonl" ? createMastraMapperState() : void 0;
const jcodeState = eventSchema === "jcode-ndjson" ? createJcodeMapperState() : void 0;
const rl = createInterface({ input: child.stdout, crlfDelay: Infinity });
const queue = new EventQueue();
let modelMismatchAborted = false;
rl.on("line", (line) => {
if (!line.trim()) return;
if (modelMismatchAborted) return;
let evt;
try {
evt = JSON.parse(line);
} catch {
return;
}
if (opts.expectedModel && eventSchema === "jcode-ndjson" && evt.type === "start" && typeof evt.model === "string" && printModelMismatch(opts.expectedModel, evt.model)) {
modelMismatchAborted = true;
const provider = typeof evt.provider === "string" ? ` (provider ${evt.provider})` : "";
queue.push({
kind: "error",
error: {
message: `model mismatch: requested "${opts.expectedModel}" but the agent started on "${evt.model}"${provider} \u2014 its CLI silently fell back to a default it resolved itself. This adapter routes AND bills by the model id, so the turn was aborted instead of silently running (and billing) a model you didn't ask for. Check the id against the adapter's model menu.`
}
});
queue.end();
child.kill("SIGTERM");
return;
}
const csid = captureSessionId(evt, eventSchema);
if (csid) capturedSessionId = csid;
const sid = capturedSessionId || sessionId || "";
const mapped = mapEvent(evt, sid, stderrLines, eventSchema, mastraState, jcodeState);
if (Array.isArray(mapped)) {
for (const m of mapped) queue.push(m);
} else if (mapped) {
queue.push(mapped);
}
});
rl.once("close", () => queue.end());
for await (const mapped of queue) {
yield mapped;
}
const exitCode = await waitForExit(child);
if (capturedSessionId) sessionId = capturedSessionId;
if (exitCode !== 0 && exitCode !== null) {
const binLabel = eventSchema === "mastra-jsonl" ? "mastracode" : eventSchema === "antigravity-stream-json" ? "agy" : eventSchema === "jcode-ndjson" ? "jcode" : "claude";
const errEvt = {
kind: "error",
error: {
message: `${binLabel} exited with code ${exitCode}`,
...stderrLines.length ? { data: { stderr: stderrLines.join("\n") } } : {}
}
};
yield errEvt;
}
} finally {
activeChild = null;
}
},
async cancel() {
activeChild?.kill("SIGTERM");
},
// The print arm spawns a fresh subprocess PER TURN from `baseArgs`,
// which already has the model baked in (composed once, before this
// session object was even constructed — see `bin_args_template`
// handling in define-agent-cli.ts/compose.ts). There is no live
// connection to apply a mid-session config change to, and rebuilding
// `baseArgs` would mean a fresh session, not a live switch — so this
// is honestly `requires-restart`, same as the `arg` model-apply
// strategy for ACP arms.
async setModel() {
return { applied: false, reason: "requires-restart" };
},
// No ACP session — no wrapper-advertised capability read-surface and
// no `session/set_mode` RPC to switch live. Mirrors the "arm has no
// mid-session config surface" case `define-agent-cli.ts` reports for
// `setModel` on other arms.
availableConfigOptions: [],
availableModes: [],
currentModeId: void 0,
async setSessionMode() {
return { applied: false, reason: "not-supported" };
},
// Same as `setSessionMode`: a print arm has no live ACP session to apply
// `set_config_option(configId:"effort")` against — effort is baked into
// the spawn argv/env, so a live change is honestly `not-supported`.
async setEffort() {
return { applied: false, reason: "not-supported" };
},
async close() {
const closing = activeChild ? terminateChildTree(activeChild) : void 0;
mcpRestore?.();
await closing;
}
};
}
function setupMcpConfigFile(opts, eventSchema) {
if (eventSchema !== "mastra-jsonl") {
const credentialed = (opts.mcpServers ?? []).filter((s) => s.transport !== "stdio" && s.headers !== void 0 && Object.keys(s.headers).length > 0).map((s) => s.name);
if (credentialed.length > 0) {
console.error(
`[agentproto] WARNING: mcpServers ${credentialed.map((n) => `"${n}"`).join(", ")} declare headers but the "${eventSchema}" print adapter has no MCP mounting path \u2014 the mounts (and their credentials) will NOT reach the agent.`
);
}
return void 0;
}
if (!opts.mcpServers || opts.mcpServers.length === 0) return void 0;
const dir = join(opts.cwd, ".mastracode");
const file = join(dir, "mcp.json");
const injected = toFileBasedMcpServers(opts.mcpServers);
if (!injected) return void 0;
const fileExisted = existsSync(file);
let originalContent;
if (fileExisted) {
try {
originalContent = readFileSync(file, "utf8");
} catch {
originalContent = void 0;
}
}
const dirExisted = existsSync(dir);
if (!dirExisted) {
mkdirSync(dir, { recursive: true });
}
let existingServers = {};
if (originalContent !== void 0) {
try {
const parsed = JSON.parse(originalContent);
if (parsed && typeof parsed.mcpServers === "object" && parsed.mcpServers !== null) {
existingServers = parsed.mcpServers;
}
} catch {
existingServers = {};
}
}
const merged = { ...existingServers, ...injected };
const output = JSON.stringify({ mcpServers: merged }, null, 2);
writeFileSync(file, output, "utf8");
return () => {
try {
if (!fileExisted) {
rmSync(file, { force: true });
if (!dirExisted) {
try {
rmdirSync(dir);
} catch {
}
}
} else if (originalContent !== void 0) {
let current = {};
try {
const parsed = JSON.parse(readFileSync(file, "utf8"));
if (parsed && typeof parsed.mcpServers === "object" && parsed.mcpServers !== null) {
current = parsed.mcpServers;
}
} catch {
writeFileSync(file, originalContent, "utf8");
return;
}
for (const key of Object.keys(injected)) {
delete current[key];
}
if (Object.keys(current).length === 0) {
writeFileSync(file, originalContent, "utf8");
} else {
writeFileSync(file, JSON.stringify({ mcpServers: current }, null, 2), "utf8");
}
}
} catch {
}
};
}
function extractPromptText(message) {
if (typeof message === "string") return message;
const blocks = Array.isArray(message) ? message : [message];
const texts = [];
for (const block of blocks) {
if (block !== null && typeof block === "object") {
const b = block;
if (b.type === "text" && typeof b.text === "string") {
texts.push(b.text);
continue;
}
if (b.type === void 0 && typeof b.text === "string") {
texts.push(b.text);
continue;
}
}
const kind = block !== null && typeof block === "object" && typeof block.type === "string" ? block.type : typeof block;
throw new Error(
`this adapter has no multimodal input (capabilities.multimodal: false) \u2014 got a "${kind}" content block it can't send. Upload the file via POST /files/upload and reference its path in the prompt text instead.`
);
}
return texts.join("\n");
}
function captureSessionId(evt, schema) {
switch (schema) {
case "claude-stream-json":
if (evt.type === "result" && typeof evt.session_id === "string" && evt.session_id)
return evt.session_id;
return null;
case "antigravity-stream-json": {
if (evt.event === "init" && typeof evt.conversation_id === "string" && evt.conversation_id)
return evt.conversation_id;
if (evt.event === "result") {
const r = evt.result;
if (typeof r?.conversation_id === "string" && r.conversation_id)
return r.conversation_id;
}
return null;
}
case "mastra-jsonl":
if (evt.type === "result" && typeof evt.threadId === "string" && evt.threadId)
return evt.threadId;
return null;
case "jcode-ndjson":
if ((evt.type === "start" || evt.type === "done") && typeof evt.session_id === "string" && evt.session_id)
return evt.session_id;
return null;
}
}
function mapEvent(evt, sessionId, stderrLines, schema, mastraState, jcodeState) {
switch (schema) {
case "claude-stream-json":
return mapClaudeEvent(evt, sessionId, stderrLines);
case "antigravity-stream-json":
return mapAntigravityEvent(evt, sessionId, stderrLines);
case "mastra-jsonl": {
if (!mastraState) {
return mapClaudeEvent(evt, sessionId, stderrLines);
}
return mapMastraEvent(evt, sessionId, stderrLines, mastraState);
}
case "jcode-ndjson":
return mapJcodeEvent(evt, sessionId, jcodeState ?? createJcodeMapperState());
default:
return mapClaudeEvent(evt, sessionId, stderrLines);
}
}
function mapClaudeEvent(evt, sessionId, stderrLines) {
switch (evt.type) {
case "text":
return typeof evt.text === "string" ? { kind: "text-delta", sessionId, text: evt.text } : null;
case "thinking":
return typeof evt.thinking === "string" ? { kind: "thought", sessionId, text: evt.thinking } : null;
case "tool_use":
return {
kind: "tool-call",
sessionId,
toolCallId: typeof evt.id === "string" ? evt.id : "",
toolName: typeof evt.name === "string" ? evt.name : "?",
arguments: evt.input ?? {}
};
case "tool_result":
return {
kind: "tool-result",
sessionId,
toolCallId: typeof evt.tool_use_id === "string" ? evt.tool_use_id : "",
result: evt.content ?? null,
isError: evt.is_error === true
};
case "result":
if (evt.subtype === "error_during_execution" || evt.is_error === true) {
return {
kind: "error",
sessionId,
error: {
message: typeof evt.error === "string" ? evt.error : "Unknown error",
...stderrLines.length ? { data: { stderr: stderrLines.join("\n") } } : {}
}
};
}
if (evt.subtype === "success") {
return { kind: "turn-end", sessionId, reason: "completed" };
}
return null;
case "system":
case "assistant":
return null;
default:
return null;
}
}
function mapAntigravityEvent(evt, sessionId, stderrLines) {
switch (evt.event) {
case "step_update": {
const su = evt.step_update;
if (!su) return null;
const stepType = su.step_type;
if (stepType === "agent_response" && typeof su.text_delta === "string" && su.text_delta) {
return { kind: "text-delta", sessionId, text: su.text_delta };
}
if (stepType === "tool" && su.state === "DONE") {
const ti = su.tool_info;
const toolName = typeof su.tool_name === "string" && su.tool_name ? su.tool_name : typeof ti?.name === "string" && ti.name ? ti.name : "?";
const toolCallId = typeof su.step_index === "number" ? `step-${su.step_index}` : "";
const err = ti?.error;
const call = {
kind: "tool-call",
sessionId,
toolCallId,
toolName,
arguments: ti?.parameters ?? {}
};
const result = {
kind: "tool-result",
sessionId,
toolCallId,
result: err ? typeof err.message === "string" ? err.message : err : ti?.output ?? null,
isError: !!err
};
return [call, result];
}
return null;
}
case "result": {
const r = evt.result;
if (!r) return null;
const status = typeof r.status === "string" ? r.status : "";
const out = [];
const usage = mapAntigravityUsage(r.usage, sessionId);
if (usage) out.push(usage);
if (status === "ERROR" || status === "INVALID") {
out.push({
kind: "error",
sessionId,
error: {
message: typeof r.error === "string" && r.error ? r.error : `agy run ended with status ${status || "ERROR"}`,
...stderrLines.length ? { data: { stderr: stderrLines.join("\n") } } : {}
}
});
} else {
out.push({
kind: "turn-end",
sessionId,
reason: mapAntigravityStatus(status)
});
}
return out.length ? out : null;
}
// init and any unknown event carry no host-facing StreamEvent.
default:
return null;
}
}
function mapAntigravityStatus(raw) {
switch (raw) {
case "CANCELED":
case "INTERRUPTED":
return "cancelled";
// SUCCESS, WAITING, RUNNING (or anything else non-error): the turn ended.
default:
return "completed";
}
}
function mapAntigravityUsage(usage, sessionId) {
if (usage === null || typeof usage !== "object") return null;
const u = usage;
const tokensIn = typeof u.input_tokens === "number" ? u.input_tokens : void 0;
const tokensOut = typeof u.output_tokens === "number" ? u.output_tokens : void 0;
if (tokensIn === void 0 && tokensOut === void 0) return null;
return {
kind: "usage_update",
sessionId,
size: 0,
used: 0,
...tokensIn !== void 0 ? { tokensIn } : {},
...tokensOut !== void 0 ? { tokensOut } : {}
};
}
function createJcodeMapperState() {
return {};
}
function mapJcodeEvent(evt, sessionId, state) {
switch (evt.type) {
case "text_delta":
return typeof evt.text === "string" ? { kind: "text-delta", sessionId, text: evt.text } : null;
case "tool_start":
state.pending = {
id: typeof evt.id === "string" ? evt.id : "",
name: typeof evt.name === "string" ? evt.name : "?",
input: ""
};
return null;
case "tool_input":
if (state.pending && typeof evt.delta === "string")
state.pending.input += evt.delta;
return null;
case "tool_exec":
return flushPendingJcodeCall(state, sessionId);
case "tool_done": {
const call = flushPendingJcodeCall(state, sessionId);
const result = {
kind: "tool-result",
sessionId,
toolCallId: typeof evt.id === "string" ? evt.id : "",
result: evt.output ?? null,
isError: evt.error !== null && evt.error !== void 0
};
return call ? [call, result] : result;
}
case "tokens": {
const tokensIn = typeof evt.input === "number" ? evt.input : void 0;
const tokensOut = typeof evt.output === "number" ? evt.output : void 0;
if (tokensIn === void 0 && tokensOut === void 0) return null;
return {
kind: "usage_update",
sessionId,
size: 0,
used: 0,
...tokensIn !== void 0 ? { tokensIn } : {},
...tokensOut !== void 0 ? { tokensOut } : {}
};
}
case "done":
return { kind: "turn-end", sessionId, reason: "completed" };
default:
return null;
}
}
function flushPendingJcodeCall(state, sessionId) {
const pending = state.pending;
state.pending = void 0;
if (!pending) return null;
let args;
try {
args = JSON.parse(pending.input);
} catch {
args = { raw: pending.input };
}
return {
kind: "tool-call",
sessionId,
toolCallId: pending.id,
toolName: pending.name,
arguments: args
};
}
function createMastraMapperState() {
return { lastTextLength: 0 };
}
function extractTextFromBlocks(content) {
let blocks = content;
if (blocks !== null && typeof blocks === "object" && !Array.isArray(blocks) && Array.isArray(blocks.parts)) {
blocks = blocks.parts;
}
if (!Array.isArray(blocks)) return "";
return blocks.filter((c) => c.type === "text" && typeof c.text === "string").map((c) => c.text).join("");
}
function mapMastraEvent(evt, sessionId, stderrLines, state) {
switch (evt.type) {
// ── Text streaming ──────────────────────────────────────────
case "message_update": {
const content = evt.message?.content;
const fullText = extractTextFromBlocks(content);
if (fullText.length > state.lastTextLength) {
const delta = fullText.slice(state.lastTextLength);
state.lastTextLength = fullText.length;
return delta ? { kind: "text-delta", sessionId, text: delta } : null;
}
return null;
}
case "message_end": {
const msg = evt.message;
if (msg?.role !== "assistant") return null;
const fullText = extractTextFromBlocks(msg?.content);
if (fullText.length > state.lastTextLength) {
const delta = fullText.slice(state.lastTextLength);
state.lastTextLength = fullText.length;
return delta ? { kind: "text-delta", sessionId, text: delta } : null;
}
return null;
}
// ── Tool calls ──────────────────────────────────────────────
case "tool_start":
return {
kind: "tool-call",
sessionId,
toolCallId: typeof evt.toolCallId === "string" ? evt.toolCallId : "",
toolName: typeof evt.toolName === "string" ? evt.toolName : "?",
// Mastra's controller event carries the tool call payload under
// `args`, not `input` (that's the Claude Code stream-json field
// name used by the sibling mapClaudeEvent tool_use case below).
arguments: evt.args ?? {}
};
case "tool_end":
return {
kind: "tool-result",
sessionId,
toolCallId: typeof evt.toolCallId === "string" ? evt.toolCallId : "",
result: evt.result ?? null,
isError: evt.isError === true
};
// ── Turn end ────────────────────────────────────────────────
case "agent_end": {
const rawReason = typeof evt.reason === "string" ? evt.reason : "complete";
const reason = mapMastraFinishReason(rawReason);
return { kind: "turn-end", sessionId, reason };
}
// ── Final result line ────────────────────────────────────────
// Always written last, once, whether or not `agent_end` fired —
// pre-flight failures (bad model, unresolvable thread, missing API
// key) call straight into this without ever streaming an
// `agent_end` or `error` event. `turn-end` already came from
// `agent_end` on the success path, so this only needs to surface
// an error for the pre-flight-failure path.
case "result": {
const err = evt.error;
if (!err) return null;
return {
kind: "error",
sessionId,
error: {
message: typeof err.message === "string" ? err.message : "Unknown error",
...stderrLines.length ? { data: { stderr: stderrLines.join("\n") } } : {}
}
};
}
// ── Errors ─────────────────────────────────────────────────
case "error": {
const err = evt.error;
return {
kind: "error",
sessionId,
error: {
message: typeof err?.message === "string" ? err.message : "Unknown error",
...stderrLines.length ? { data: { stderr: stderrLines.join("\n") } } : {}
}
};
}
// ── Token usage ─────────────────────────────────────────────
// Mastra Code DOES expose usage: its `AgentController` emits a native
// `usage_update` event ({ type: "usage_update", usage: TokenUsage })
// on every model step-finish — verified against @mastra/core 1.48.0's
// AgentControllerEvent union and its emit site
// (`this.#session.emit({ type: "usage_update", usage: stepUsage })`).
// `TokenUsage` carries `{ promptTokens, completionTokens, totalTokens }`.
// Map it to this repo's `usage_update` StreamEvent (the SAME shape
// claude-code emits over ACP) so mastracode sessions — both the print
// arm and the in-process arm, which share this mapper — carry token
// telemetry in the transcript. mastracode reports these cumulatively
// (its own headless `runMC` result-usage is last-write-wins over these
// events), and last-write-wins on the descriptor matches that. It
// carries no context-window size/used or per-turn cost here, so those
// stay 0 / absent (projectEvent guards on >0, never clobbering a real
// size).
case "usage_update": {
const usage = evt.usage;
const promptTokens = typeof usage?.promptTokens === "number" ? usage.promptTokens : void 0;
const completionTokens = typeof usage?.completionTokens === "number" ? usage.completionTokens : void 0;
if (promptTokens === void 0 && completionTokens === void 0)
return null;
return {
kind: "usage_update",
sessionId,
size: 0,
used: 0,
...promptTokens !== void 0 ? { tokensIn: promptTokens } : {},
...completionTokens !== void 0 ? { tokensOut: completionTokens } : {}
};
}
// ── Shell output (tool-like) ────────────────────────────────
case "shell_output":
return typeof evt.output === "string" ? {
kind: "tool-result",
sessionId,
toolCallId: "",
result: evt.output,
isError: false
} : null;
// ── Skipped events ─────────────────────────────────────────
case "agent_start":
case "subagent_start":
case "subagent_end":
case "tool_approval_required":
case "tool_suspended":
return null;
default:
return null;
}
}
var QUEUE_HIGH_WATER_MARK = 1e4;
var EventQueue = class {
buffer = [];
resolveNext = null;
ended = false;
highWaterWarned = false;
push(evt) {
this.buffer.push(evt);
if (this.buffer.length > QUEUE_HIGH_WATER_MARK && !this.highWaterWarned) {
this.highWaterWarned = true;
console.warn(
`print-arm: event backlog exceeded ${QUEUE_HIGH_WATER_MARK} buffered events \u2014 downstream consumer is not keeping up. The child's stdout is still being drained (no ENOBUFS), but memory will grow until the consumer catches up.`
);
}
const resolve4 = this.resolveNext;
this.resolveNext = null;
resolve4?.();
}
/** Signal that no further events will be pushed. */
end() {
this.ended = true;
const resolve4 = this.resolveNext;
this.resolveNext = null;
resolve4?.();
}
async *[Symbol.asyncIterator]() {
for (; ; ) {
const evt = this.buffer.shift();
if (evt !== void 0) {
yield evt;
continue;
}
if (this.ended) return;
await new Promise((resolve4) => {
this.resolveNext = resolve4;
});
}
}
};
function mapMastraFinishReason(raw) {
switch (raw) {
case "complete":
return "completed";
case "aborted":
case "suspended":
return "cancelled";
case "error":
return "error";
default:
return "completed";
}
}
function waitForExit(child) {
return new Promise((resolve4) => {
child.once("exit", (code) => resolve4(code));
child.once("error", () => resolve4(null));
});
}
async function createProprietaryProtocolArm(options) {
let mod;
try {
const target = isAbsolute(options.adapter) ? pathToFileURL(options.adapter).href : options.adapter;
mod = await import(target);
} catch (err) {
const cause = err instanceof Error ? err.message : String(err);
throw new Error(
`createProprietaryProtocolArm: could not load adapter package '${options.adapter}'. Install it with: npm i ${options.adapter}
cause: ${cause}`
);
}
const candidate = "createAgentCliClient" in mod ? mod.createAgentCliClient : mod.default;
if (typeof candidate !== "function") {
throw new Error(
`createProprietaryProtocolArm: adapter package '${options.adapter}' does not export a 'createAgentCliClient' factory function (or a default export function). A protocol="proprietary" adapter must export createAgentCliClient(definition: AgentCliHandle): AgentCliClient | Promise<AgentCliClient>.`
);
}
return await candidate(options.definition);
}
// src/legacy-modes.ts
var LEGACY_GATEWAY_MODE_IDS = /* @__PURE__ */ new Set([
"moonshot",
"openrouter",
"requesty",
"deepseek"
]);
var LEGACY_POSTURE_MODE_IDS = /* @__PURE__ */ new Set([
"default",
"plan",
"accept-edits",
"bypass-permissions",
"read-only",
"full-access",
"build"
]);
function inferLegacyModeKind(modeId) {
if (LEGACY_GATEWAY_MODE_IDS.has(modeId)) return "route";
if (LEGACY_POSTURE_MODE_IDS.has(modeId)) return "posture";
return "context";
}
function isLegacyExtractedModeId(modeId) {
return LEGACY_GATEWAY_MODE_IDS.has(modeId) || LEGACY_POSTURE_MODE_IDS.has(modeId);
}
// src/manifest/compose.ts
var RuntimeConfigError = class extends Error {
code;
path;
constructor(code, path, message) {
super(`[${code} at ${path}] ${message}`);
this.code = code;
this.path = path;
this.name = "RuntimeConfigError";
}
};
function composeSpawn(handle, config, opts) {
if (!config)
return {
binArgs: [...handle.bin_args ?? []],
env: { ...handle.env ?? {} },
envUnset: []
};
const prepend = [];
const append = [];
const env = { ...handle.env ?? {} };
const envUnset = [];
if (config.mode !== void 0) {
const mode = (handle.modes ?? []).find((m) => m.id === config.mode);
if (!mode) {
if (!isLegacyExtractedModeId(config.mode)) {
const known = (handle.modes ?? []).map((m) => m.id).join(", ") || "(none)";
throw new RuntimeConfigError(
"unknown_mode",
"config.mode",
`Mode '${config.mode}' is not declared by manifest '${handle.id}'. Known modes: ${known}`
);
}
} else if ((mode.apply ?? "bin_args") === "bin_args") {
if (mode.bin_args_prepend) prepend.push(...mode.bin_args_prepend);
if (mode.bin_args_append) append.push(...mode.bin_args_append);
if (mode.env) Object.assign(env, mode.env);
if (mode.env_unset) envUnset.push(...mode.env_unset);
}
}
if (config.options && Object.keys(config.options).length > 0) {
const declared = handle.options ?? [];
const declaredById = new Map(declared.map((o) => [o.id, o]));
for (const id of Object.keys(config.options)) {
if (!declaredById.has(id)) {
const known = declared.map((o) => o.id).join(", ") || "(none)";
throw new RuntimeConfigError(
"unknown_option",
`config.options.${id}`,
`Option '${id}' is not declared by manifest '${handle.id}'. Known options: ${known}`
);
}
}
for (const option of declared) {
if (!(option.id in config.options)) continue;
const value = config.options[option.id];
validateOptionValue(option, value);
const patch = renderOptionPatch(option, value);
prepend.push(...patch.prepend);
append.push(...patch.append);
Object.assign(env, patch.env);
if (patch.envUnset.length) envUnset.push(...patch.envUnset);
}
}
const requestedModel = config.options?.model;
if (typeof requestedModel === "string" && handle.models?.deny?.length) {
const denyHit = handle.models.deny.find(
(pattern) => matchesModelPattern(pattern, requestedModel)
);
if (denyHit) {
throw new RuntimeConfigError(
"model_denied",
"config.options.model",
`Model '${requestedModel}' is denied by manifest '${handle.id}' (matches deny pattern '${denyHit}'). This adapter deliberately does not route to that provider \u2014 use a permitted model or a different adapter.`
);
}
}
if (typeof requestedModel === "string" && handle.models?.apply === "arg" && handle.models.bin_args_template && !opts?.skipModelArg) {
append.push(
...handle.models.bin_args_template.map(
(token) => token.replace("{model}", requestedModel)
)
);
}
if (config.continuation !== void 0 && handle.continuation) {
if (!handle.continuation.supported.includes(config.continuation)) {
throw new RuntimeConfigError(
"unsupported_continuation",
"config.continuation",
`Continuation strategy '${config.continuation}' is not in manifest '${handle.id}'.continuation.supported (${handle.continuation.supported.join(", ")}).`
);
}
}
return {
binArgs: [...prepend, ...handle.bin_args ?? [], ...append],
env,
envUnset
};
}
function resolveContinuationStrategy(handle, config) {
if (config?.continuation) return config.continuation;
return handle.continuation?.default ?? "none";
}
function matchesModelPattern(pattern, modelId) {
const p = pattern.toLowerCase();
const m = modelId.toLowerCase();
return p.endsWith("*") ? m.startsWith(p.slice(0, -1)) : m === p;
}
function validateOptionValue(option, value) {
const path = `config.options.${option.id}`;
switch (option.type) {
case "boolean":
if (typeof value !== "boolean") {
throw new RuntimeConfigError(
"option_type_mismatch",
path,
`Expected boolean, got ${typeof value} (${JSON.stringify(value)}).`
);
}
return;
case "integer":
if (typeof value !== "number" || !Number.isInteger(value)) {
throw new RuntimeConfigError(
"option_type_mismatch",
path,
`Expected integer, got ${typeof value === "number" ? "non-integer number" : typeof value} (${JSON.stringify(value)}).`
);
}
if (option.min !== void 0 && value < option.min) {
throw new RuntimeConfigError(
"option_bounds_violation",
path,
`Value ${value} is below the declared minimum ${option.min}.`
);
}
if (option.max !== void 0 && value > option.max) {
throw new RuntimeConfigError(
"option_bounds_violation",
path,
`Value ${value} is above the declared maximum ${option.max}.`
);
}
return;
case "string":
if (typeof value !== "string") {
throw new RuntimeConfigError(
"option_type_mismatch",
path,
`Expected string, got ${typeof value} (${JSON.stringify(value)}).`
);
}
return;
case "enum":
if (typeof value !== "string") {
throw new RuntimeConfigError(
"option_type_mismatch",
path,
`Expected string (enum value), got ${typeof value} (${JSON.stringify(value)}).`
);
}
if (!(option.enum ?? []).includes(value)) {
throw new RuntimeConfigError(
"option_enum_violation",
path,
`Value '${value}' is not in the declared enum (${(option.enum ?? []).join(", ")}).`
);
}
return;
}
}
function renderOptionPatch(option, value) {
const stringValue = String(value);
if (option.type === "boolean") {
if (value !== true) return { prepend: [], append: [], env: {}, envUnset: [] };
return {
prepend: [],
append: option.bin_args_append_when_true ? [...option.bin_args_append_when_true] : [],
env: option.env ? interpolateEnv(option.env, stringValue) : {},
envUnset: option.env_unset ? [...option.env_unset] : []
};
}
return {
prepend: option.bin_args_prepend ? option.bin_args_prepend.map(
(token) => token.replace(/\{value\}/g, stringValue)
) : [],
append: option.bin_args_template ? option.bin_args_template.map(
(token) => token.replace(/\{value\}/g, stringValue)
) : [],
env: option.env ? interpolateEnv(option.env, stringValue) : {},
envUnset: option.env_unset ? [...option.env_unset] : []
};
}
function interpolateEnv(env, value) {
const out = {};
for (const [k, v] of Object.entries(env)) {
out[k] = v.replace(/\{value\}/g, value);
}
return out;
}
function realStateHome(stateHome, env) {
const fromEnv = env[stateHome.env];
if (fromEnv && isAbsolute(fromEnv)) return fromEnv;
return resolve(homedir(), stateHome.defaultDir);
}
function prepareIsolatedStateHome(stateHome, opts) {
const real = realStateHome(stateHome, opts.env);
let dir;
if (opts.configDir) {
dir = opts.configDir;
mkdirSync(dir, { recursive: true });
} else {
dir = mkdtempSync(join(tmpdir(), `agentproto-${stateHome.env.toLowerCase()}-`));
}
const writePaths = [dir];
for (const name of stateHome.share ?? []) {
const target = join(real, name);
const link = join(dir, name);
try {
lstatSync(link);
unlinkSync(link);
} catch {
}
if (!existsSync(target)) continue;
symlinkSync(target, link);
writePaths.push(realpathSync(target));
}
for (const [name, content] of Object.entries(stateHome.seed ?? {})) {
const file = join(dir, name);
try {
unlinkSync(file);
} catch {
}
writeFileSync(file, content);
}
return { dir, writePaths };
}
var CREDENTIAL_DATA_SUBDIR = "auth-data";
function isolatedCredentialDataHome(configDir) {
const dir = configDir ? join(configDir, CREDENTIAL_DATA_SUBDIR) : mkdtempSync(join(tmpdir(), "agentproto-data-"));
mkdirSync(dir, { recursive: true });
return dir;
}
function agentCliSupportsHostContextIsolation(adapterId) {
return adapterId === "claude-code";
}
function escapeGlob(p) {
return p.replace(/[\\*?[\]{}()!+@]/g, "\\$&");
}
function ancestors(dir) {
const out = [];
let cur = dirname(dir);
for (; ; ) {
out.push(cur);
const parent = dirname(cur);
if (parent === cur) return out;
cur = parent;
}
}
function hostContextExcludes(cwd) {
const abs = resolve(cwd);
let real = abs;
try {
real = realpathSync(abs);
} catch {
}
const dirs = /* @__PURE__ */ new Set([...ancestors(abs), ...ancestors(real)]);
const out = [];
for (const d of dirs) {
const base = d === "/" ? "" : escapeGlob(d);
out.push(
`${base}/CLAUDE.md`,
`${base}/CLAUDE.local.md`,
`${base}/.claude/CLAUDE.md`,
`${base}/.claude/rules/**`
);
}
return out;
}
var SESSION_SKILLS_ABS_DIR = "<abs dir>";
function readSessionSkillsMetadata(metadata) {
const raw = metadata?.sessionSkills;
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return void 0;
const record = raw;
const dir = record.dir;
if (typeof dir !== "string" || dir.length === 0) return void 0;
const env = {};
const envRaw = record.env;
if (envRaw && typeof envRaw === "object" && !Array.isArray(envRaw)) {
for (const [name, value] of Object.entries(envRaw)) {
if (typeof value === "string") env[name] = value;
}
}
return { dir, ...Object.keys(env).length > 0 ? { env } : {} };
}
function applySessionSkills(args) {
const targetDir = join(args.baseDir, args.meta.dir);
rmSync(targetDir, { recursive: true, force: true });
mkdirSync(targetDir, { recursive: true });
const warnings = [];
for (const skill of args.skills) {
const dest = resolve(targetDir, skill.alias);
const rel = relative(targetDir, dest);
if (rel === "" || rel.startsWith("..") || isAbsolute(rel)) {
warnings.push(`skill "${skill.alias}" has an invalid alias \u2014 not delivered`);
continue;
}
try {
cpSync(skill.path, dest, { recursive: true });
} catch (err) {
warnings.push(
`skill "${skill.alias}" could not be delivered: ${err instanceof Error ? err.message : String(err)}`
);
}
}
if (args.meta.env) {
for (const [name, value] of Object.entries(args.meta.env)) {
args.env[name] = value.split(SESSION_SKILLS_ABS_DIR).join(args.baseDir);
}
}
return { targetDir, warnings };
}
function createArmSessionControls(arm, definition) {
const modelApply = definition.models?.apply ?? "config";
return {
// Capability read-surface (SPEC §3.9/§3.4a) — snapshotted once
// `arm.connect()` has resolved. Not live-updated by a later
// `setModel`/`setSessionMode` call.
availableConfigOptions: arm.availableConfigOptions ?? [],
availableModes: arm.availableModes ?? [],
currentModeId: arm.currentModeId,
/**
* Mid-session model switch — the runtime counterpart to the
* spawn-time model apply. Dispatches on the same
* `definition.models.apply` strategy so a live switch behaves
* exactly like the spawn-time apply would have, just against an
* already-running session.
*/
async setModel(modelId) {
if (modelApply === "arg") {
return { applied: false, reason: "requires-restart" };
}
if (modelApply === "command") {
return applyModelCommand(arm, modelId);
}
if (!arm.setConfigOption) {
return { applied: false, reason: "not-supported" };
}
const result = await arm.setConfigOption("model", modelId);
return result.applied ? { applied: true, model: modelId } : { applied: false, ...result.reason ? { reason: result.reason } : {} };
},
/**
* Mid-session posture switch — the native-mode counterpart to
* `setModel`, wired directly to the arm's `setSessionMode` (only the
* ACP arm implements it).
*/
async setSessionMode(modeId) {
if (!arm.setSessionMode) {
return { applied: false, reason: "not-supported" };
}
const result = await arm.setSessionMode(modeId);
return result.applied ? { applied: true, modeId } : { applied: false, ...result.reason ? { reason: result.reason } : {} };
},
/**
* Mid-session effort switch — the effort-axis counterpart to
* `setModel`'s `"config"` strategy, wired to the same
* `arm.setConfigOption` surface with `configId:"effort"`. Best-effort:
* an effort label the current model rejects resolves
* `{applied:false, reason}` (SPEC risk R7).
*/
async setEffort(effort) {
if (!arm.setConfigOption) {
return { applied: false, reason: "not-supported" };
}
const result = await arm.setConfigOption("effort", effort);
return result.applied ? { applied: true, effort } : { applied: false, ...result.reason ? { reason: result.reason } : {} };
}
};
}
var MODEL_SWITCH_ACK_RE = /switch|model\s+set|now using/i;
function isModelSwitchAcknowledgement(evt) {
return MODEL_SWITCH_ACK_RE.test(JSON.stringify(evt));
}
function parseModelSwitchCommand(text) {
return /^\s*\/model\s+(\S+)/i.exec(text)?.[1];
}
async function applyModelCommand(arm, modelId) {
const turnId = randomUUID();
let acked = false;
try {
for await (const evt of promptTurn(arm, turnId, {
type: "text",
text: `/model ${modelId}`
})) {
if (isModelSwitchAcknowledgement(evt)) acked = true;
}
} catch (err) {
const reason = err instanceof Error ? err.message : String(err);
console.warn(
`[agent-cli] /model ${modelId} control turn failed (continuing on default):`,
err instanceof Error ? err.message : err
);
return { applied: false, reason };
}
if (!acked) {
const reason = "no switch acknowledgement \u2014 agent may be on its default model";
console.warn(`[agent-cli] /model ${modelId}: ${reason}`);
return { applied: false, reason };
}
return { applied: true, model: modelId };
}
function parseLogfmtLine(line) {
const out = {};
const re = /([A-Za-z0-9_.-]+)=("(?:[^"\\]|\\.)*"|\S+)/g;
let m;
while ((m = re.exec(line)) !== null) {
const key = m[1];
let value = m[2];
if (value.startsWith('"')) {
value = value.slice(1, -1).replace(/\\(.)/g, "$1");
}
out[key] = value;
}
return out;
}
function providerMessageFromError(raw) {
const m = raw.match(/^[A-Za-z_][\w.]*(?:Error|Exception)\s*:\s*([\s\S]+)$/);
return (m ? m[1] : raw).trim();
}
function parseStderrStreamError(line) {
if (!line.includes("stream error")) return void 0;
const fields = parseLogfmtLine(line);
if ((fields.level ?? "").toUpperCase() !== "ERROR") return void 0;
if (fields.message !== "stream error") return void 0;
if (fields.small === "true") return void 0;
if (fields.agent === "title") return void 0;
const raw = fields["error.error"] ?? fields.error;
if (!raw) return void 0;
return providerMessageFromError(raw);
}
async function* promptTurn(arm, turnId, message, stderrTurnError) {
const stderrTail = arm._stderrTail;
const pendingErrors = [];
let wake;
const unsubscribe = stderrTurnError && arm._onStderrLine ? arm._onStderrLine((line) => {
const parsed = stderrTurnError(line);
if (parsed === void 0) return;
pendingErrors.push(parsed);
wake?.();
wake = void 0;
}) : void 0;
let iterator;
try {
await arm.send(turnId, message);
iterator = arm.events()[Symbol.asyncIterator]();
while (true) {
if (pendingErrors.length > 0) {
await arm.cancel(turnId).catch(() => {
});
yield {
kind: "error",
sessionId: arm.sessionId,
error: {
message: pendingErrors.join("\n"),
data: { stderr: stderrTail?.() ?? "" }
}
};
yield {
kind: "turn-end",
sessionId: arm.sessionId ?? "",
reason: "error"
};
return;
}
const nextEvent = iterator.next();
const stderrArrived = new Promise((resolve4) => {
wake = resolve4;
});
const winner = await Promise.race([
nextEvent.then((result) => ({ source: "event", result })),
stderrArrived.then(() => ({ source: "stderr" }))
]);
if (winner.source === "stderr") continue;
if (winner.result.done) return;
const evt = winner.result.value;
if (evt.kind === "error" && typeof stderrTail === "function") {
const tail = stderrTail();
if (tail) {
const existing = evt.error.data ?? {};
evt.error.data = { ...existing, stderr: tail };
}
}
yield evt;
}
} finally {
unsubscribe?.();
void iterator?.return?.()?.catch?.(() => {
});
}
}
function isPlainObject(v) {
return typeof v === "object" && v !== null && !Array.isArray(v);
}
function ancestors2(cwd) {
const out = [];
let dir = resolve(cwd);
for (; ; ) {
out.push(dir);
const parent = dirname(dir);
if (parent === dir) return out;
dir = parent;
}
}
function userConfigFiles(decl, cwd, env) {
const home = env.HOME || homedir();
const files = [];
const xdgConfig = env.XDG_CONFIG_HOME || join(home, ".config");
for (const f of decl.globalFiles ?? []) files.push(join(xdgConfig, f));
for (const name of decl.fileEnv ?? []) {
if (env[name]) files.push(env[name]);
}
for (const name of decl.dirEnv ?? []) {
if (env[name]) for (const f of decl.dirFiles ?? []) files.push(join(env[name], f));
}
const dirs = [...ancestors2(cwd), home];
const projectDisabled = !!(decl.projectDisableEnv && env[decl.projectDisableEnv]);
for (const dir of dirs) {
if (!projectDisabled) for (const f of decl.projectFiles ?? []) files.push(join(dir, f));
for (const f of decl.dirFiles ?? []) files.push(join(dir, f));
}
return files;
}
function fileSetsKey(file, key) {
try {
if (!existsSync(file)) return false;
return readFileSync(file, "utf8").includes(`"${key}"`);
} catch {
return false;
}
}
function resolveSmallModelEnv(decl, input) {
if (!decl) return void 0;
const explicit = decl.option ? input.options?.[decl.option] : void 0;
const value = typeof explicit === "string" && explicit ? explicit : typeof input.model === "string" && input.model ? input.model : void 0;
if (!value) return void 0;
const prior = input.env[decl.env];
let inline = {};
if (prior !== void 0 && prior !== "") {
let parsed;
try {
parsed = JSON.parse(prior);
} catch {
return void 0;
}
if (!isPlainObject(parsed)) return void 0;
if (parsed[decl.key] !== void 0) return void 0;
inline = parsed;
}
if (!explicit && decl.userConfig) {
const files = userConfigFiles(decl.userConfig, input.cwd, input.env);
if (files.some((f) => fileSetsKey(f, decl.key))) return void 0;
}
return { [decl.env]: JSON.stringify({ ...inline, [decl.key]: value }) };
}
// src/opencode-permission.ts
var OPENCODE_PERMISSION_KEYS = [
"read",
"edit",
"glob",
"grep",
"list",
"bash",
"task",
"external_directory",
"todowrite",
"question",
"webfetch",
"websearch",
"lsp",
"doom_loop",
"skill"
];
var OPENCODE_POSTURE_PERMISSIONS = {
bypass: {
"*": "allow",
...Object.fromEntries(OPENCODE_PERMISSION_KEYS.map((key) => [key, "allow"]))
},
"accept-edits": { edit: "allow" },
"read-only": { edit: "deny", bash: "deny" }
};
var OPENCODE_SPAWN_ENFORCED_POSTURES = Object.keys(
OPENCODE_POSTURE_PERMISSIONS
);
function opencodePermissionForPosture(posture) {
if (!posture) return void 0;
return OPENCODE_POSTURE_PERMISSIONS[posture];
}
function mergeOpencodePermission(prior, permission, envName) {
let inline = {};
if (prior !== void 0 && prior !== "") {
let parsed;
try {
parsed = JSON.parse(prior);
} catch {
parsed = void 0;
}
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
throw new Error(
`opencode posture: ${envName} is not a JSON object, so the requested permission posture cannot be enforced \u2014 refusing to spawn with an advisory-only posture.`
);
}
inline = parsed;
}
const existing = inline.permission;
const merged = typeof existing === "string" ? { "*": existing } : existing && typeof existing === "object" && !Array.isArray(existing) ? { ...existing } : {};
for (const [key, action] of Object.entries(permission)) {
delete merged[key];
merged[key] = action;
}
return JSON.stringify({ ...inline, permission: merged });
}
function npxCacheKey(packages) {
return createHash("sha512").update([...packages].sort((a, b) => a.localeCompare(b, "en")).join("\n")).digest("hex").slice(0, 16);
}
function parseExactSpec(spec) {
const m = /^((?:@[a-z0-9][\w.-]*\/)?[a-z0-9][\w.-]*)@(\d+\.\d+\.\d+(?:-[\w.-]+)?(?:\+[\w.-]+)?)$/i.exec(spec);
return m ? { name: m[1], version: m[2] } : void 0;
}
function binNameFromManifest(name, bin) {
const unscoped = name.replace(/^@[^/]+\//, "");
if (typeof bin === "string") return unscoped;
if (!bin || typeof bin !== "object") return void 0;
const names = Object.keys(bin);
if (names.length === 1) return names[0];
return names.includes(unscoped) ? unscoped : void 0;
}
function resolveNpxFastPath(bin, args, env, deps) {
if ((process.platform) === "win32") return void 0;
if (basename(bin) !== "npx") return void 0;
const [yes, spec, ...rest] = args;
if (yes !== "-y" && yes !== "--yes") return void 0;
if (spec === void 0) return void 0;
const parsed = parseExactSpec(spec);
if (!parsed) return void 0;
const exists = existsSync;
const readFile = ((p) => readFileSync(p, "utf8"));
const cacheRoot = env["npm_config_cache"] ?? env["NPM_CONFIG_CACHE"] ?? join(homedir(), ".npm");
const installDir = join(cacheRoot, "_npx", npxCacheKey([spec]));
try {
const pkg = JSON.parse(readFile(join(installDir, "node_modules", parsed.name, "package.json")));
if (pkg.version !== parsed.version) return void 0;
const binName = binNameFromManifest(parsed.name, pkg.bin);
if (!binName) return void 0;
const binDir = join(installDir, "node_modules", ".bin");
const binPath = join(binDir, binName);
if (!exists(binPath)) return void 0;
return { bin: binPath, args: rest, binDir };
} catch {
return void 0;
}
}
// src/define-agent-cli.ts
var defineAgentCli = createDoctype(
{
aip: 45,
name: "agent-cli",
readIdentity: (def) => def.id,
validate(def) {
const result = agentCliFrontmatterSchema.safeParse(def);
if (!result.success) {
throw new Error(
`defineAgentCli (AIP-45): ${result.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`).join("; ")}`
);
}
const data = result.data;
if (data.protocol === "acp" && !data.acp) {
throw new Error(
"defineAgentCli (AIP-45): `acp` ref is required when protocol=acp"
);
}
if (data.protocol === "mcp" && !data.mcp) {
throw new Error(
"defineAgentCli (AIP-45): `mcp` block is required when protocol=mcp"
);
}
if (data.protocol === "proprietary" && !data.adapter) {
throw new Error(
"defineAgentCli (AIP-45): `adapter` package is required when protocol=proprietary"
);
}
if (data.session?.mode === "resumable" && data.capabilities?.resumable !== true) {
throw new Error(
"defineAgentCli (AIP-45): session.mode=resumable requires capabilities.resumable: true"
);
}
if (data.models?.apply === "arg" && !data.models.bin_args_template?.length) {
throw new Error(
'defineAgentCli (AIP-45): models.apply="arg" requires models.bin_args_template'
);
}
},
build(def) {
return { ...def };
}
}
);
function resolveSpawnBin(bin, deps) {
const execPath = deps?.execPath ?? process.execPath;
if (bin === "node") return execPath;
if (bin === "npx" || bin === "npm") {
const exists = deps?.exists ?? existsSync;
const sibling = join(
dirname(execPath),
process.platform === "win32" ? `${bin}.cmd` : bin
);
if (exists(sibling)) return sibling;
}
return bin;
}
function ensureExecDirOnPath(env, execPath = process.execPath) {
const execDir = dirname(execPath);
const parts = (env.PATH ?? "").split(delimiter).filter(Boolean);
if (parts.includes(execDir)) return;
env.PATH = [...parts, execDir].join(delimiter);
}
function isPlainObject2(v) {
return typeof v === "object" && v !== null && !Array.isArray(v);
}
function deepMergeJson(base, over) {
const out = { ...base };
for (const [k, v] of Object.entries(over)) {
const prev = out[k];
out[k] = isPlainObject2(prev) && isPlainObject2(v) ? deepMergeJson(prev, v) : v;
}
return out;
}
function mergeJsonEnvValue(base, over) {
try {
const a = JSON.parse(base);
const b = JSON.parse(over);
if (isPlainObject2(a) && isPlainObject2(b)) return JSON.stringify(deepMergeJson(a, b));
} catch {
}
return over;
}
function createAgentCliRuntime(definition) {
return {
definition,
async start(opts) {
const cwd = opts?.cwd ?? process.cwd();
const resolvedBin = resolveSpawnBin(definition.bin);
const contextMode = opts?.contextProfile ? (definition.modes ?? []).find(
(mode) => mode.id === opts.contextProfile && mode.kind === "context"
) : void 0;
const config = opts?.config?.mode || !contextMode ? opts?.config : { ...opts?.config, mode: contextMode.id };
const composed = composeSpawn(definition, config);
const env = {
...filterStringEnv(process.env),
...composed.env
};
for (const k of composed.envUnset) delete env[k];
const authSpec = opts?.auth;
let credentialDataDir;
const hasGatewayAuthToken = "ANTHROPIC_AUTH_TOKEN" in composed.env;
const hasGatewayBaseUrl = "ANTHROPIC_BASE_URL" in composed.env;
const resolverCoupledGateway = authSpec?.baseUrl !== void 0;
const engageAuth = !!authSpec && !hasGatewayAuthToken && (resolverCoupledGateway || !hasGatewayBaseUrl) && (authSpec.enforce === "always" || authSpec.explicit === true);
if (authSpec && engageAuth) {
for (const key of authSpec.unsetEnv) {
if (!(key in composed.env)) delete env[key];
}
if (!authSpec.externalCredential) {
if (authSpec.credential === void 0 && !authSpec.loginFile) {
const slug = definition.id;
const providerLabel = definition.provider ?? "<provider>";
const configBlock = `{"defaults":{"adapters":{"${slug}":{"auth":{"mode":"api-key"}}}}} in ~/.agentproto/config.json`;
const storeHint = authSpec.ignoredApiKeyInStore ? ` (providers.json already has a stored key for ${providerLabel} \u2014 ignored until that block exists)` : "";
const supportsSub = !!definition.authSubscription;
const message = authSpec.neitherConfigured && supportsSub ? `agent-cli '${slug}': no billing auth. Subscription \u2192 \`claude setup-token\`. Api-key \u2192 \`agentproto auth provider set ${providerLabel} sk-\u2026\`${storeHint}. Add ${configBlock}. Never inherited from the shell.` : authSpec.mode === "subscription" ? `agent-cli '${slug}': run \`claude setup-token\` and configure the result \u2014 bills the Max/Pro subscription, not API credits. Never inherited from the shell.` : `agent-cli '${slug}': run \`agentproto auth provider set ${providerLabel} sk-\u2026\`${storeHint}, then add ${configBlock}. Never inherited from the shell.`;
throw new RuntimeConfigError("missing_auth_credential", "opts.auth.credential", message);
}
if (authSpec.credential !== void 0) {
env[authSpec.setEnv] = authSpec.credential;
if (authSpec.extraEnv) {
for (const [key, value] of Object.entries(authSpec.extraEnv)) {
env[key] = key in composed.env ? mergeJsonEnvValue(composed.env[key], value) : value;
}
}
if (definition.credentialDataHome && authSpec.isolateDataHome !== false) {
credentialDataDir = isolatedCredentialDataHome(opts?.configDir);
env[definition.credentialDataHome.env] = credentialDataDir;
}
}
}
}
Object.assign(env, opts?.env ?? {});
const smallModelEnv = resolveSmallModelEnv(definition.smallModel, {
model: config?.options?.model,
...config?.options ? { options: config.options } : {},
env,
cwd
});
if (smallModelEnv) Object.assign(env, smallModelEnv);
const systemPromptDecl = definition.systemPrompt;
let systemPromptTmpDir;
const systemPromptText = opts?.appendSystemPrompt;
const writeSystemPromptFile = (text) => {
const dir = opts?.configDir ?? (systemPromptTmpDir = mkdtempSync(join(tmpdir(), "agentproto-system-prompt-")));
mkdirSync(dir, { recursive: true });
const file = join(dir, "agentproto-system-prompt.md");
writeFileSync(file, text);
return file;
};
if (systemPromptText && systemPromptDecl?.via === "inline-config") {
const isFileList = systemPromptDecl.shape === "file-list";
let payload;
if (isFileList) {
payload = writeSystemPromptFile(systemPromptText);
} else {
payload = systemPromptText;
}
if (payload !== void 0) {
const prior = env[systemPromptDecl.env];
let inline;
if (prior === void 0 || prior === "") {
inline = {};
} else {
try {
const parsed = JSON.parse(prior);
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
inline = parsed;
}
} catch {
}
if (!inline) {
console.warn(
`[agent-cli] system prompt: ${systemPromptDecl.env} is not a JSON object \u2014 the AGENT.md system-prompt text is not delivered to this session.`
);
}
}
if (inline) {
const existing = inline[systemPromptDecl.key];
inline[systemPromptDecl.key] = isFileList ? Array.isArray(existing) ? [...existing, payload] : [payload] : typeof existing === "string" && existing.length > 0 ? `${existing}
${payload}` : payload;
env[systemPromptDecl.env] = JSON.stringify(inline);
}
}
}
if (systemPromptText && systemPromptDecl?.via === "env-file") {
env[systemPromptDecl.env] = writeSystemPromptFile(systemPromptText);
}
const opencodePermission = definition.id === "opencode" ? opencodePermissionForPosture(opts?.posture) : void 0;
if (opencodePermission) {
env.OPENCODE_CONFIG_CONTENT = mergeOpencodePermission(
env.OPENCODE_CONFIG_CONTENT,
opencodePermission,
"OPENCODE_CONFIG_CONTENT"
);
}
ensureExecDirOnPath(env);
const npxFast = resolveNpxFastPath(resolvedBin, composed.binArgs, env);
if (npxFast) {
env.PATH = [npxFast.binDir, ...(env.PATH ?? "").split(delimiter).filter(Boolean)].join(delimiter);
}
const spawnBin = npxFast?.bin ?? resolvedBin;
let spawnArgs = npxFast?.args ?? composed.binArgs;
if (systemPromptText && systemPromptDecl?.via === "argv" && definition.protocol !== "proprietary") {
spawnArgs = [...spawnArgs, systemPromptDecl.flag, systemPromptText];
}
const batchRewrite = resolveWindowsBatchSpawn(spawnBin, spawnArgs);
const finalBin = batchRewrite?.bin ?? spawnBin;
const finalArgs = batchRewrite?.args ?? spawnArgs;
if (opts?.additionalReadPaths?.length) {
env.AGENTPROTO_ADDITIONAL_READ_PATHS = JSON.stringify(opts.additionalReadPaths);
}
const permissionMode = resolveClaudeCodePermissionMode(
definition,
opts?.posture,
config?.mode
);
const isolateClaudeCodeConfig = definition.id === "claude-code";
let claudeConfigDir;
if (isolateClaudeCodeConfig) {
if (opts?.configDir) {
claudeConfigDir = opts.configDir;
mkdirSync(claudeConfigDir, { recursive: true });
} else {
claudeConfigDir = mkdtempSync(
join(tmpdir(), "agentproto-claude-config-")
);
}
const globalConfigPath = join(claudeConfigDir, ".claude.json");
let priorGlobalConfig = {};
try {
const parsed = JSON.parse(readFileSync(globalConfigPath, "utf8"));
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
priorGlobalConfig = parsed;
}
} catch {
}
writeFileSync(
globalConfigPath,
JSON.stringify({ ...priorGlobalConfig, mcpServers: {} })
);
const isolatedSettings = {
attribution: { commit: "", pr: "", sessionUrl: false }
};
const isolatedPermissions = {};
if (permissionMode) isolatedPermissions.defaultMode = permissionMode;
if (opts?.fsZones?.writable.length) {
isolatedPermissions.additionalDirectories = opts.fsZones.writable;
}
if (Object.keys(isolatedPermissions).length > 0) {
isolatedSettings.permissions = isolatedPermissions;
}
if (opts?.isolateHostContext) {
isolatedSettings.claudeMdExcludes = hostContextExcludes(cwd);
}
writeFileSync(
join(claudeConfigDir, "settings.json"),
JSON.stringify(isolatedSettings)
);
env.CLAUDE_CONFIG_DIR = claudeConfigDir;
}
const sessionSkillsMeta = readSessionSkillsMetadata(definition.metadata);
const sessionSkillsBase = claudeConfigDir ?? opts?.configDir;
let sessionSkillsTargetDir;
if (sessionSkillsMeta && opts?.sessionSkills?.length && sessionSkillsBase) {
const applied = applySessionSkills({
skills: opts.sessionSkills,
meta: sessionSkillsMeta,
baseDir: sessionSkillsBase,
env
});
sessionSkillsTargetDir = applied.targetDir;
for (const warning of applied.warnings) {
console.warn(`[agent-cli] session skills: ${warning}`);
}
}
const extraWritePaths = claudeConfigDir ? [claudeConfigDir] : [];
if (credentialDataDir) extraWritePaths.push(credentialDataDir);
if (sessionSkillsTargetDir) extraWritePaths.push(sessionSkillsTargetDir);
if (definition.stateHome && definition.protocol !== "proprietary" && await willConfineAgentCliSpawn({ mode: opts?.commandSandbox, cwd, zones: opts?.fsZones })) {
const isolated = prepareIsolatedStateHome(definition.stateHome, {
...opts?.configDir ? { configDir: opts.configDir } : {},
env
});
env[definition.stateHome.env] = isolated.dir;
extraWritePaths.push(...isolated.writePaths);
}
if (definition.protocol === "print") {
const printModel = config?.options?.model;
return createPrintSession({
bin: finalBin,
baseArgs: finalArgs,
cwd,
env,
...opts?.resumeSessionId ? { resumeSessionId: opts.resumeSessionId } : {},
...opts?.mcpServers ? { mcpServers: opts.mcpServers } : {},
printConfig: definition.print,
commandSandbox: opts?.commandSandbox,
...opts?.fsZones ? { fsZones: opts.fsZones } : {},
...extraWritePaths.length ? { extraWritePaths } : {},
// Exact-file read grant — see the ACP arm's wrapAgentCliSpawn call.
...opts?.additionalReadPaths?.length ? { extraReadPaths: opts.additionalReadPaths } : {},
...definition.routeSelection === "derived-from-model" && printModel ? { expectedModel: String(printModel) } : {}
});
}
let child;
const stderrBuf = [];
const stderrLineListeners = /* @__PURE__ */ new Set();
if (definition.protocol !== "proprietary") {
const [execBin, execArgs] = await wrapAgentCliSpawn(
finalBin,
finalArgs,
{
mode: opts?.commandSandbox,
cwd,
...opts?.fsZones ? { zones: opts.fsZones } : {},
...extraWritePaths.length ? { extraWritePaths } : {},
// Exact-file read grant (see additionalReadPaths above):
// READ-only exceptions to the confinement boundary, never writes.
...opts?.additionalReadPaths?.length ? { extraReadPaths: opts.additionalReadPaths } : {},
label: definition.id
}
);
const spawned = spawn(execBin, execArgs, {
cwd,
env,
stdio: ["pipe", "pipe", "pipe"],
signal: opts?.signal,
// Win32: a `.cmd`/`.bat` bin that `resolveWindowsBatchSpawn`
// couldn't rewrite to its node entry needs the shell — Node
// refuses to spawn batch files directly (CVE-2024-27980, the
// recap-10 `spawn EINVAL`). POSIX: `{}` — unchanged.
...windowsBatchShellOption(execBin)
});
child = spawned;
let spawnFailure;
await new Promise((resolve4) => {
spawned.once("spawn", () => resolve4());
spawned.once("error", (err) => {
spawnFailure = err;
resolve4();
});
});
if (spawnFailure) {
const isEnoent = spawnFailure.code === "ENOENT";
const cwdMissing = isEnoent && !existsSync(cwd);
const enoentHint = !isEnoent ? "" : cwdMissing ? `
cwd '${cwd}' does not exist \u2014 Node reports a missing working directory with this same ENOENT. Recreate the directory, or pass a valid cwd/workspace (is the workspace path stale, or its worktree removed?).` : `
'${execBin}' was not found on the daemon's PATH. If it works in your own terminal, the daemon's environment may be stale \u2014 restart the daemon from a shell where \`which ${definition.bin}\` resolves (\`agentproto daemon restart\`).`;
throw new Error(
`agent-cli '${definition.id}': failed to spawn '${execBin} ${execArgs.join(" ")}': ${spawnFailure.message}${enoentHint}`
);
}
spawned.on("error", (err) => {
stderrBuf.push(`[spawn error] ${err.message}`);
});
const STDERR_KEEP_LINES = 80;
child.stderr?.setEncoding("utf8");
child.stderr?.on("data", (chunk) => {
for (const line of chunk.split(/\r?\n/)) {
if (!line) continue;
stderrBuf.push(line);
if (stderrBuf.length > STDERR_KEEP_LINES) stderrBuf.shift();
for (const listener of stderrLineListeners) listener(line);
}
});
}
const arm = await buildProtocolArm(
definition,
child,
cwd,
config?.mode,
opts?.permissionHold ?? false
);
arm._stderrTail = () => stderrBuf.join("\n");
arm._onStderrLine = (listener) => {
stderrLineListeners.add(listener);
return () => stderrLineListeners.delete(listener);
};
const abortController = new AbortController();
if (opts?.signal) {
opts.signal.addEventListener("abort", () => abortController.abort(), {
once: true
});
}
const optModel = config?.options?.model;
const optEffort = config?.options?.effort;
const modelApply = definition.models?.apply ?? "config";
const configModel = optModel && modelApply === "config" ? String(optModel) : void 0;
const requestedModeId = config?.mode;
const requestedModeDecl = requestedModeId ? (definition.modes ?? []).find((m) => m.id === requestedModeId) : void 0;
const configMode = requestedModeDecl?.apply === "config" ? requestedModeId : void 0;
const turnIdleTimeoutMs = opts?.turnIdleTimeoutMs ?? definition.session?.turn_idle_timeout_ms;
const connectSystemPrompt = systemPromptText !== void 0 && (systemPromptDecl?.via === "acp-meta" || systemPromptDecl?.via === "argv" && definition.protocol === "proprietary") ? systemPromptText : void 0;
await arm.connect({
cwd,
env,
abortSignal: abortController.signal,
...opts?.resumeSessionId ? { resumeSessionId: opts.resumeSessionId } : {},
...opts?.mcpServers ? { mcpServers: opts.mcpServers } : {},
...configModel ? { model: configModel } : {},
...optEffort ? { effort: String(optEffort) } : {},
...configMode ? { mode: configMode } : {},
// Channel decision above: `acp-meta` (the arm's `_meta.systemPrompt`)
// or a `proprietary` arm's own `argv` push.
...connectSystemPrompt !== void 0 ? { appendSystemPrompt: connectSystemPrompt } : {},
...opts?.onActivity ? { onActivity: opts.onActivity } : {},
...turnIdleTimeoutMs !== void 0 ? { turnIdleTimeoutMs } : {},
...opts?.permissionHold ? { permissionHold: true } : {}
});
if (configModel && definition.routeSelection === "derived-from-model" && arm.modelApplyRejection) {
const { requested, reason } = arm.modelApplyRejection;
await arm.close().catch(() => {
});
if (child && !child.killed) child.kill("SIGTERM");
throw derivedModelRefusalError(definition, requested, reason);
}
if (optModel && modelApply === "command") {
const commandResult = await applyModelCommand(arm, String(optModel));
if (!commandResult.applied && definition.routeSelection === "derived-from-model") {
await arm.close().catch(() => {
});
if (child && !child.killed) child.kill("SIGTERM");
throw derivedModelRefusalError(
definition,
String(optModel),
commandResult.reason ?? "model switch not acknowledged"
);
}
}
const sessionId = arm.sessionId ?? randomUUID();
let currentTurnId;
return {
sessionId,
pid: child?.pid,
// Transport liveness, delegated to the arm (see the fields' docs on
// AgentCliRuntimeSession). An arm that models no long-lived
// connection reports nothing, which reads as "connected" — there is
// no stream for it to have lost, and nothing to subscribe to.
isConnected: () => arm.isConnected?.() ?? true,
...arm.onDisconnect ? { onDisconnect: (listener) => arm.onDisconnect(listener) } : {},
// Live model/mode/effort switches + the capability read-surface,
// all pure arm delegation — shared with every host that builds
// its own session over another transport. See session-controls.ts.
// Snapshotted after `arm.connect()` above resolved.
...createArmSessionControls(arm, definition),
...spawnEnforcedPostures(definition).length > 0 ? { spawnEnforcedPostures: spawnEnforcedPostures(definition) } : {},
send(message) {
const turnId = randomUUID();
currentTurnId = turnId;
const stderrTurnError = definition.id === "opencode" ? parseStderrStreamError : void 0;
return promptTurn(arm, turnId, message, stderrTurnError);
},
/**
* Cancel the in-flight turn — the wire equivalent of Ctrl-C at the
* agent's own TUI, and the primitive the host's interrupt path
* (`SessionsRegistry.interruptSession`, `{interrupt: true}`) is built
* on.
*
* This used to be `abortController.abort()` alone, which cancelled
* NOTHING: that controller is the CONNECTION-level signal, handed to
* `arm.connect({abortSignal})` at spawn, and no arm reads it. So the
* abort fired into the void while the agent kept working — a turn
* "cancelled" this way ran happily to completion, and the host's 60s
* settle-wait then timed out on a turn that was never asked to stop.
* It only ever LOOKED like it worked for turns that happened to end
* naturally inside that wait.
*
* Both arms have always implemented the real thing — the ACP arm
* sends `session/cancel`, the print arm SIGTERMs the child — it was
* simply never called. Call it.
*
* Connection-level abort deliberately stays out of this: aborting the
* connection would end the SESSION, and cancelling a turn must leave
* it alive and idle for the next prompt. That distinction is the whole
* point of interrupt-vs-kill.
*/
async cancel() {
if (currentTurnId === void 0) return;
await arm.cancel(currentTurnId);
},
...arm.respondPermission ? {
respondPermission(requestId, resolution) {
return arm.respondPermission(requestId, resolution);
}
} : {},
...arm.onOutOfTurnEvent ? {
onOutOfTurnEvent(listener) {
return arm.onOutOfTurnEvent(listener);
}
} : {},
...arm.steer ? {
// Snapshotted after `arm.connect()` resolved, like the
// config/mode read surfaces above.
steeringSupported: arm.steeringSupported === true,
steer(content) {
return arm.steer(content);
}
} : {},
async close() {
await arm.close();
if (child) await terminateChildTree(child);
if (systemPromptTmpDir) rmSync(systemPromptTmpDir, { recursive: true, force: true });
}
};
}
};
}
async function buildProtocolArm(def, child, cwd, requestedMode, permissionHold) {
switch (def.protocol) {
case "acp":
if (!child) {
throw new Error("createAgentCliRuntime: acp protocol requires a spawned subprocess");
}
return createAcpProtocolArm({
child,
cwd,
clientInfo: { name: def.id, version: def.version },
requestedMode,
...permissionHold ? { permissionHold: true } : {}
});
case "mcp":
throw new Error("createAgentCliRuntime: mcp protocol arm not yet implemented");
case "proprietary":
if (!def.adapter) {
throw new Error(
"createAgentCliRuntime: proprietary protocol requires manifest.adapter"
);
}
return createProprietaryProtocolArm({ adapter: def.adapter, definition: def });
case "print":
throw new Error("createAgentCliRuntime: print arm bypasses buildProtocolArm");
}
}
var CLAUDE_CODE_POSTURE_PERMISSION_MODES = {
plan: "plan",
"read-only": "plan",
"accept-edits": "acceptEdits",
bypass: "bypassPermissions"
};
function spawnEnforcedPostures(definition) {
if (definition.id === "claude-code") return Object.keys(CLAUDE_CODE_POSTURE_PERMISSION_MODES);
if (definition.id === "opencode") return OPENCODE_SPAWN_ENFORCED_POSTURES;
return [];
}
function resolveClaudeCodePermissionMode(definition, posture, configMode) {
if (definition.id !== "claude-code") return void 0;
if (posture && CLAUDE_CODE_POSTURE_PERMISSION_MODES[posture]) {
return CLAUDE_CODE_POSTURE_PERMISSION_MODES[posture];
}
if (!configMode) return void 0;
const mode = (definition.modes ?? []).find((m) => m.id === configMode);
const args = mode?.bin_args_append ?? [];
const flagIndex = args.indexOf("--permission-mode");
if (flagIndex === -1 || flagIndex + 1 >= args.length) return void 0;
return args[flagIndex + 1];
}
function filterStringEnv(env) {
const out = {};
for (const [k, v] of Object.entries(env)) {
if (typeof v === "string") out[k] = v;
}
return out;
}
function derivedModelRefusalError(definition, requested, reason) {
const first = definition.models?.allowed?.[0];
const example = (typeof first === "string" ? first : first?.id) ?? "openrouter/anthropic/claude-sonnet-4-6";
return new Error(
`${definition.bin}: refusing to start on the default model \u2014 the requested model "${requested}" was not applied (${reason}). This adapter routes AND bills by the model id's own provider prefix, so continuing would silently run (and bill) a model you didn't ask for. Use the adapter's "<provider>/<model>" form (e.g. "${example}"), or pick an id from its model menu.`
);
}
export { CREDENTIAL_DATA_SUBDIR, LEGACY_GATEWAY_MODE_IDS, LEGACY_POSTURE_MODE_IDS, RuntimeConfigError, SESSION_SKILLS_ABS_DIR, agentCliFrontmatterSchema, agentCliSupportsHostContextIsolation, applyModelCommand, applySessionSkills, autoAllowPermissionHandler, composeSpawn, createAcpProtocolArm, createAgentCliRuntime, createArmSessionControls, createJcodeMapperState, createMastraMapperState, createPrintSession, createProprietaryProtocolArm, defineAgentCli, descendantsOf, hostContextExcludes, inferLegacyModeKind, isLegacyExtractedModeId, isModelSwitchAcknowledgement, mapJcodeEvent, mapMastraEvent, parseModelSwitchCommand, planModePermissionHandler, promptTurn, readSessionSkillsMetadata, resolveContinuationStrategy, resolveSpawnBin, runtimeConfigSchema, spawnEnforcedPostures, terminateChildTree, toFileBasedMcpServers };
//# sourceMappingURL=chunk-YX33PB7P.mjs.map
//# sourceMappingURL=chunk-YX33PB7P.mjs.map