@agentconnect.md/daemon 1.0.0-rc.55 → 1.0.0-rc.57

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -11,8 +11,8 @@ import { fileURLToPath } from "node:url";
11
11
  import { createInterface } from "node:readline";
12
12
  import { cpus, freemem, homedir, hostname, tmpdir, totalmem, type } from "node:os";
13
13
  import { spawn as spawn$1 } from "child_process";
14
- import { Readable, Writable } from "node:stream";
15
14
  import { randomBytes, randomUUID } from "node:crypto";
15
+ import { Readable, Writable } from "node:stream";
16
16
  import { lstat, open, readdir, realpath, stat as stat$1 } from "node:fs/promises";
17
17
  import { DatabaseSync } from "node:sqlite";
18
18
  import net from "node:net";
@@ -12788,437 +12788,1194 @@ function gitInstanceFactory(baseDir, options) {
12788
12788
  init_git_response_error();
12789
12789
  var simpleGit = gitInstanceFactory;
12790
12790
  //#endregion
12791
- //#region src/workspace/workspace-manager.ts
12792
- const PULL_TIMEOUT_MS = 4500;
12793
- const cloneInFlight = /* @__PURE__ */ new Map();
12794
- async function prepareWorkspace(agent) {
12795
- const cwd = agent.workspace.path;
12796
- mkdirSync(cwd, { recursive: true });
12797
- if (agent.workspace.mode === "from-scratch") {
12798
- const mem = join(cwd, "memory.md");
12799
- if (!existsSync(mem)) writeFileSync(mem, `# ${agent.name} memory\n`);
12800
- return cwd;
12801
- }
12802
- if (!existsSync(join(cwd, ".git"))) {
12803
- await cloneRepo(agent);
12804
- return cwd;
12805
- }
12806
- if (agent.workspace.pullOnNewSession) try {
12807
- await Promise.race([simpleGit(cwd).pull(["--ff-only"]), new Promise((_, rej) => setTimeout(() => rej(/* @__PURE__ */ new Error("pull timeout")), PULL_TIMEOUT_MS))]);
12808
- } catch {}
12809
- return cwd;
12810
- }
12791
+ //#region ../protocol/dist/envelope.js
12811
12792
  /**
12812
- * Eagerly clone a git-repo workspace that has no checkout yet — for reconcile-time
12813
- * prefetch so the repo is warm before the first message arrives. No-op for
12814
- * from-scratch or an already-cloned checkout (the session-time `prepareWorkspace`
12815
- * stays authoritative and still owns pull + hard-fail). Reuses the single-flight
12816
- * lock so a prefetch and a concurrent session-start clone never race into the same
12817
- * dir. The caller invokes this fire-and-forget and logs any rejection — clone
12818
- * failure here is non-fatal because `prepareWorkspace` re-clones (and hard-fails)
12819
- * on the first session as the backstop.
12793
+ * The protocol envelope — every frame, both directions, is wrapped in this.
12794
+ * Mirrors daemon-cp-ws-protocol.md §1.1.
12795
+ *
12796
+ * `payload` is left as `unknown` here and validated by the per-type schema in
12797
+ * `frames/*` via `FRAME_SCHEMAS[type]` (see `frame.ts`). This two-step parse is
12798
+ * what lets the codec answer an unknown `type` with `UNKNOWN_FRAME` (a REP)
12799
+ * instead of a hard close — forward-compat, protocol §1.
12820
12800
  */
12821
- async function prefetchWorkspace(agent) {
12822
- if (agent.workspace.mode !== "git-repo") return;
12823
- const cwd = agent.workspace.path;
12824
- if (existsSync(join(cwd, ".git"))) return;
12825
- mkdirSync(cwd, { recursive: true });
12826
- await cloneRepo(agent);
12827
- }
12828
- /** Clone agent.workspace.gitRepo @ gitBranch into cwd, single-flight per cwd. */
12829
- async function cloneRepo(agent) {
12830
- const cwd = agent.workspace.path;
12831
- const inflight = cloneInFlight.get(cwd);
12832
- if (inflight) return inflight;
12833
- const gitRepo = agent.workspace.gitRepo;
12834
- if (!gitRepo) throw new Error(`workspace clone: agent "${agent.id}" has git-repo mode but no gitRepo configured`);
12835
- const branch = agent.workspace.gitBranch;
12836
- const p = (async () => {
12837
- await simpleGit().clone(gitRepo, cwd, [
12838
- "--branch",
12839
- branch,
12840
- "--single-branch"
12841
- ]);
12842
- })().finally(() => {
12843
- cloneInFlight.delete(cwd);
12844
- });
12845
- cloneInFlight.set(cwd, p);
12846
- return p;
12847
- }
12848
- //#endregion
12849
- //#region ../../node_modules/.pnpm/@agentclientprotocol+sdk@1.0.0_zod@4.4.3/node_modules/@agentclientprotocol/sdk/dist/schema/index.js
12850
- const AGENT_METHODS = {
12851
- initialize: "initialize",
12852
- authenticate: "authenticate",
12853
- providers_list: "providers/list",
12854
- providers_set: "providers/set",
12855
- providers_disable: "providers/disable",
12856
- session_new: "session/new",
12857
- session_load: "session/load",
12858
- session_set_mode: "session/set_mode",
12859
- session_set_config_option: "session/set_config_option",
12860
- session_prompt: "session/prompt",
12861
- session_cancel: "session/cancel",
12862
- mcp_message: "mcp/message",
12863
- session_list: "session/list",
12864
- session_delete: "session/delete",
12865
- session_fork: "session/fork",
12866
- session_resume: "session/resume",
12867
- session_close: "session/close",
12868
- logout: "logout",
12869
- nes_start: "nes/start",
12870
- nes_suggest: "nes/suggest",
12871
- nes_accept: "nes/accept",
12872
- nes_reject: "nes/reject",
12873
- nes_close: "nes/close",
12874
- document_did_open: "document/didOpen",
12875
- document_did_change: "document/didChange",
12876
- document_did_close: "document/didClose",
12877
- document_did_save: "document/didSave",
12878
- document_did_focus: "document/didFocus"
12879
- };
12880
- const CLIENT_METHODS = {
12881
- session_request_permission: "session/request_permission",
12882
- session_update: "session/update",
12883
- fs_write_text_file: "fs/write_text_file",
12884
- fs_read_text_file: "fs/read_text_file",
12885
- terminal_create: "terminal/create",
12886
- terminal_output: "terminal/output",
12887
- terminal_release: "terminal/release",
12888
- terminal_wait_for_exit: "terminal/wait_for_exit",
12889
- terminal_kill: "terminal/kill",
12890
- mcp_connect: "mcp/connect",
12891
- mcp_message: "mcp/message",
12892
- mcp_disconnect: "mcp/disconnect",
12893
- elicitation_create: "elicitation/create",
12894
- elicitation_complete: "elicitation/complete"
12895
- };
12896
- const PROTOCOL_METHODS = { cancel_request: "$/cancel_request" };
12897
- //#endregion
12898
- //#region ../../node_modules/.pnpm/@agentclientprotocol+sdk@1.0.0_zod@4.4.3/node_modules/@agentclientprotocol/sdk/dist/schema-deserialize.js
12899
- const skippedItem = Symbol("skippedItem");
12900
- function defaultOnError(schema, fallback) {
12901
- return schema.catch(fallback);
12902
- }
12903
- function requiredDefaultOnError(schema, fallback) {
12904
- const schemaWithCatch = schema.catch(fallback);
12905
- return unknown().transform((value, context) => {
12906
- if (value !== void 0) return schemaWithCatch.parse(value);
12907
- context.addIssue({
12908
- code: "custom",
12909
- message: "Required value is missing"
12910
- });
12911
- return NEVER;
12912
- });
12913
- }
12914
- function vecSkipError(itemSchema) {
12915
- return array(itemSchema.catch(skippedItem)).transform((items) => items.filter((item) => item !== skippedItem));
12916
- }
12801
+ const Envelope = object({
12802
+ v: literal(1),
12803
+ id: string().uuid(),
12804
+ ts: string().datetime(),
12805
+ type: string(),
12806
+ corr: string().uuid().optional(),
12807
+ payload: unknown()
12808
+ });
12809
+ object({
12810
+ epoch: number().int(),
12811
+ seq: number().int().optional(),
12812
+ agentId: string().uuid().optional(),
12813
+ launchId: string().uuid().optional()
12814
+ });
12815
+ /** The all-zero UUID used when a frame is malformed past the point of reading `id`. */
12816
+ const NIL_UUID = "00000000-0000-0000-0000-000000000000";
12917
12817
  //#endregion
12918
- //#region ../../node_modules/.pnpm/@agentclientprotocol+sdk@1.0.0_zod@4.4.3/node_modules/@agentclientprotocol/sdk/dist/schema/zod.gen.js
12818
+ //#region ../protocol/dist/frames/auth.js
12919
12819
  /**
12920
- * JSON RPC Request Id
12921
- *
12922
- * An identifier established by the Client that MUST contain a String, Number, or NULL value if included. If it is not included it is assumed to be a notification. The value SHOULD normally not be Null \[1\] and Numbers SHOULD NOT contain fractional parts \[2\]
12923
- *
12924
- * The Server MUST reply with the same value in the Response object if included. This member is used to correlate the context between the two objects.
12925
- *
12926
- * \[1\] The use of Null as a value for the id member in a Request object is discouraged, because this specification uses a value of Null for Responses with an unknown id. Also, because JSON-RPC 1.0 uses an id value of Null for Notifications this could cause confusion in handling.
12820
+ * Auth & identity — protocol §3.1 / §3.2.
12927
12821
  *
12928
- * \[2\] Fractional parts may be problematic, since many decimal fractions cannot be represented exactly as binary fractions.
12822
+ * `auth` is the first frame after the socket opens. `auth/ok` carries the
12823
+ * minted `sessionEpoch` (the global fencing token), heartbeat cadence, and the
12824
+ * resume verdict.
12929
12825
  */
12930
- const zRequestId = union([number(), string()]).nullable();
12826
+ const AuthReq = object({
12827
+ apiKey: string(),
12828
+ daemonId: string().uuid().optional(),
12829
+ machineId: string().uuid().optional(),
12830
+ attestation: string().optional(),
12831
+ agentVersion: string(),
12832
+ resume: object({
12833
+ lastEpoch: number().int(),
12834
+ lastRecvSeq: record(string(), number())
12835
+ }).optional()
12836
+ });
12837
+ const AuthOk = object({
12838
+ daemonId: string().uuid(),
12839
+ sessionEpoch: number().int(),
12840
+ heartbeatSec: number().int(),
12841
+ serverTime: string().datetime(),
12842
+ resume: object({
12843
+ accepted: boolean(),
12844
+ redeliverFromSeq: record(string(), number()).optional()
12845
+ }).optional()
12846
+ });
12847
+ //#endregion
12848
+ //#region ../protocol/dist/frames/route.js
12931
12849
  /**
12932
- * A unique identifier for a conversation session between a client and agent.
12933
- *
12934
- * Sessions maintain their own context, conversation history, and state,
12935
- * allowing multiple independent interactions with the same agent.
12850
+ * Routing & orchestration (C→D control) — protocol §5.
12936
12851
  *
12937
- * See protocol docs: [Session ID](https://agentclientprotocol.com/protocol/session-setup#session-id)
12852
+ * `SessionKey` is the canonical session primitive shared across route/*,
12853
+ * agent/*, and event/session. Its canonical string form is
12854
+ * `${platform}:${channel}:${thread ?? "-"}`.
12938
12855
  */
12939
- const zSessionId = string();
12856
+ const Platform = _enum(["slack", "telegram"]);
12857
+ const SessionKey = object({
12858
+ platform: Platform,
12859
+ channel: string(),
12860
+ thread: string().optional()
12861
+ });
12862
+ /** Trigger-matching rule for a binding (protocol §5.1). */
12863
+ const BindRule = object({ match: discriminatedUnion("kind", [
12864
+ object({ kind: literal("mention") }),
12865
+ object({ kind: literal("dm") }),
12866
+ object({
12867
+ kind: literal("keyword"),
12868
+ value: string()
12869
+ }),
12870
+ object({ kind: literal("auto") })
12871
+ ]) });
12872
+ const RouteAssign = object({
12873
+ sessionKey: SessionKey,
12874
+ agentId: string().uuid(),
12875
+ workspaceId: string().uuid(),
12876
+ bindRules: array(BindRule).default([])
12877
+ });
12878
+ const RouteAssignAck = object({
12879
+ ok: boolean(),
12880
+ sessionKey: SessionKey,
12881
+ reason: string().optional()
12882
+ });
12883
+ const RouteUpdate = object({
12884
+ routingEpoch: number().int(),
12885
+ rules: array(object({
12886
+ match: unknown(),
12887
+ agentId: string().uuid()
12888
+ }))
12889
+ });
12890
+ /** Graceful scale-down / rebalance — protocol §5.3. */
12891
+ const Drain = object({
12892
+ scope: union([
12893
+ object({
12894
+ kind: literal("agent"),
12895
+ agentId: string().uuid()
12896
+ }),
12897
+ object({ kind: literal("daemon") }),
12898
+ object({
12899
+ kind: literal("session"),
12900
+ sessionKey: SessionKey
12901
+ })
12902
+ ]),
12903
+ deadline: string().datetime()
12904
+ });
12905
+ const DrainProgress = object({
12906
+ remaining: number().int(),
12907
+ drained: array(SessionKey)
12908
+ });
12909
+ const DrainDone = object({ released: array(SessionKey) });
12910
+ //#endregion
12911
+ //#region ../protocol/dist/frames/cron.js
12940
12912
  /**
12941
- * Request to write content to a text file.
12913
+ * Cron sinks to the daemon (D5) — protocol §5.4.
12942
12914
  *
12943
- * Only available if the client supports the `fs.writeTextFile` capability.
12915
+ * The CP owns the definition; the daemon owns firing + last-run persistence so
12916
+ * crons fire even when the CP is down.
12944
12917
  */
12945
- const zWriteTextFileRequest = object({
12946
- sessionId: zSessionId,
12947
- path: string(),
12948
- content: string(),
12949
- _meta: record(string(), unknown()).nullish()
12918
+ const CronUpsert = object({
12919
+ cronId: string().uuid(),
12920
+ schedule: string(),
12921
+ target: object({ channel: string() }),
12922
+ trigger: string(),
12923
+ enabled: boolean().default(true)
12950
12924
  });
12925
+ const CronRemove = object({ cronId: string().uuid() });
12926
+ //#endregion
12927
+ //#region ../protocol/dist/frames/secrets.js
12951
12928
  /**
12952
- * Request to read content from a text file.
12929
+ * Secrets (C5 ↔ D10) — protocol §6.
12953
12930
  *
12954
- * Only available if the client supports the `fs.readTextFile` capability.
12931
+ * Lease-based, no plaintext on the wire or in PG. Every frame carries a
12932
+ * REFERENCE to a Vault/KMS path, never the secret material itself.
12955
12933
  */
12956
- const zReadTextFileRequest = object({
12957
- sessionId: zSessionId,
12958
- path: string(),
12959
- line: int().gte(0).max(4294967295, { error: "Invalid value: Expected uint32 to be <= 4294967295" }).nullish(),
12960
- limit: int().gte(0).max(4294967295, { error: "Invalid value: Expected uint32 to be <= 4294967295" }).nullish(),
12961
- _meta: record(string(), unknown()).nullish()
12934
+ const SecretsRequest = object({ scope: object({
12935
+ platform: Platform,
12936
+ workspaceId: string().uuid()
12937
+ }) });
12938
+ const SecretsGrant = object({
12939
+ leaseId: string().uuid(),
12940
+ scope: object({
12941
+ platform: string(),
12942
+ workspaceId: string().uuid()
12943
+ }),
12944
+ ref: string(),
12945
+ ttl: number().int(),
12946
+ renewBeforeSec: number().int()
12947
+ });
12948
+ const SecretsRenew = object({ leaseId: string().uuid() });
12949
+ const SecretsRevoke = object({
12950
+ leaseId: string().uuid(),
12951
+ reason: string()
12952
+ });
12953
+ /** 🅼 Direct-to-store upload/download grant — protocol §3.2 / frame #25. */
12954
+ const ScopeAttestation = object({
12955
+ machineId: string().uuid(),
12956
+ scope: _enum([
12957
+ "attachment.put",
12958
+ "attachment.get",
12959
+ "facts.put"
12960
+ ]),
12961
+ resourceRef: string(),
12962
+ jws: string(),
12963
+ exp: string().datetime()
12962
12964
  });
12965
+ //#endregion
12966
+ //#region ../protocol/dist/frames/agent.js
12963
12967
  /**
12964
- * Unique identifier for a tool call within a session.
12968
+ * Agent lifecycle + delivery (protocol §4.3, §4.4, §7.4, §8).
12969
+ *
12970
+ * Body-locality invariant (protocol §12): no frame here carries
12971
+ * `NormalizedMessage.text`. `agent/prompt` ships a `NormalizedMessageRef` — a
12972
+ * reference the daemon resolves against its local body store.
12965
12973
  */
12966
- const zToolCallId = string();
12967
12974
  /**
12968
- * Categories of tools that can be invoked.
12969
- *
12970
- * Tool kinds help clients choose appropriate icons and optimize how they
12971
- * display tool execution progress.
12972
- *
12973
- * See protocol docs: [Creating](https://agentclientprotocol.com/protocol/tool-calls#creating)
12975
+ * A REFERENCE/digest of a normalized message, NOT the body (protocol §4.3).
12976
+ * Enough for D4/D6 to fetch the local body and prompt.
12974
12977
  */
12975
- const zToolKind = union([
12976
- literal("read"),
12977
- literal("edit"),
12978
- literal("delete"),
12979
- literal("move"),
12980
- literal("search"),
12981
- literal("execute"),
12982
- literal("think"),
12983
- literal("fetch"),
12984
- literal("switch_mode"),
12985
- literal("other")
12986
- ]);
12978
+ const NormalizedMessageRef = object({
12979
+ sessionKey: SessionKey,
12980
+ platformMsgId: string(),
12981
+ seenUpToSeq: number().int()
12982
+ });
12987
12983
  /**
12988
- * Execution status of a tool call.
12989
- *
12990
- * Tool calls progress through different statuses during their lifecycle.
12984
+ * Where the agent runs. Two modes; the **path is always daemon-generated** —
12985
+ * never specified by the caller (UX picks the mode, the machine owns the dir).
12991
12986
  *
12992
- * See protocol docs: [Status](https://agentclientprotocol.com/protocol/tool-calls#status)
12987
+ * - `scratch`: a fresh empty working dir on the machine, no repo.
12988
+ * - `github`: the daemon clones `gitRepo` @ `branch` and runs the agent in
12989
+ * `agentDir` (a subdir of the repo, repo-root if omitted). **Multiple agents
12990
+ * may share one repo** — they differ by `agentDir`, so the repo is not an
12991
+ * owned entity, just shared config on each agent.
12993
12992
  */
12994
- const zToolCallStatus = union([
12995
- literal("pending"),
12996
- literal("in_progress"),
12997
- literal("completed"),
12998
- literal("failed")
12999
- ]);
12993
+ const AgentWorkspace = discriminatedUnion("mode", [object({ mode: literal("scratch") }), object({
12994
+ mode: literal("github"),
12995
+ gitRepo: string(),
12996
+ branch: string().default("main"),
12997
+ agentDir: string().optional()
12998
+ })]);
13000
12999
  /**
13001
- * Optional annotations for the client. The client can use annotations to inform how objects are used or displayed
13000
+ * The editable agent definition the CP owns and the daemon needs to run it:
13001
+ * prompt + runtime selection. Raft ships this in the launch config and the
13002
+ * daemon synthesizes the system prompt locally; `description` IS the prompt.
13002
13003
  */
13003
- const zAnnotations = object({
13004
- audience: defaultOnError(vecSkipError(union([literal("assistant"), literal("user")])).nullish(), () => void 0),
13005
- lastModified: string().nullish(),
13006
- priority: number().nullish(),
13007
- _meta: record(string(), unknown()).nullish()
13004
+ const AgentSpec = object({
13005
+ name: string(),
13006
+ description: string().optional(),
13007
+ runtime: string().optional(),
13008
+ model: string().optional(),
13009
+ reasoningEffort: string().optional(),
13010
+ executionMode: string().optional(),
13011
+ workspace: AgentWorkspace.optional(),
13012
+ env: record(string(), string()).optional()
13008
13013
  });
13009
- /**
13010
- * Text provided to or from an LLM.
13011
- */
13012
- const zTextContent = object({
13013
- annotations: defaultOnError(zAnnotations.nullish(), () => void 0),
13014
- text: string(),
13015
- _meta: record(string(), unknown()).nullish()
13014
+ const AgentLaunch = object({
13015
+ agentId: string().uuid(),
13016
+ runtime: string(),
13017
+ workspaceId: string().uuid(),
13018
+ capabilities: array(string()),
13019
+ spec: AgentSpec,
13020
+ mode: _enum(["long_lived", "per_turn"]).default("long_lived")
13016
13021
  });
13017
13022
  /**
13018
- * An image provided to or from an LLM.
13023
+ * Live agent CRUD (C→D): the console edited an agent's spec; push it so a
13024
+ * running daemon reloads without waiting for the next launch. `agent/remove`
13025
+ * tears the agent down. Deleting an agent never relaunches it.
13019
13026
  */
13020
- const zImageContent = object({
13021
- annotations: defaultOnError(zAnnotations.nullish(), () => void 0),
13022
- data: string(),
13023
- mimeType: string(),
13024
- uri: string().nullish(),
13025
- _meta: record(string(), unknown()).nullish()
13027
+ const AgentUpsert = object({
13028
+ agentId: string().uuid(),
13029
+ spec: AgentSpec
13026
13030
  });
13027
- /**
13028
- * Audio provided to or from an LLM.
13029
- */
13030
- const zAudioContent = object({
13031
- annotations: defaultOnError(zAnnotations.nullish(), () => void 0),
13032
- data: string(),
13033
- mimeType: string(),
13034
- _meta: record(string(), unknown()).nullish()
13031
+ const AgentRemove = object({ agentId: string().uuid() });
13032
+ const AgentLaunched = object({
13033
+ agentId: string().uuid(),
13034
+ launchId: string().uuid(),
13035
+ acpSessionId: string().optional(),
13036
+ startedAt: string().datetime(),
13037
+ runtime: string()
13035
13038
  });
13036
- /**
13037
- * A resource that the server is capable of reading, included in a prompt or tool call result.
13038
- */
13039
- const zResourceLink = object({
13040
- annotations: defaultOnError(zAnnotations.nullish(), () => void 0),
13041
- description: string().nullish(),
13042
- mimeType: string().nullish(),
13043
- name: string(),
13044
- size: number().nullish(),
13045
- title: string().nullish(),
13046
- uri: string(),
13047
- _meta: record(string(), unknown()).nullish()
13039
+ const AgentStop = object({
13040
+ agentId: string().uuid(),
13041
+ launchId: string().uuid(),
13042
+ reason: string()
13048
13043
  });
13049
- /**
13050
- * Resource content that can be embedded in a message.
13051
- */
13052
- const zEmbeddedResourceResource = union([object({
13053
- mimeType: string().nullish(),
13054
- text: string(),
13055
- uri: string(),
13056
- _meta: record(string(), unknown()).nullish()
13057
- }), object({
13058
- blob: string(),
13059
- mimeType: string().nullish(),
13060
- uri: string(),
13061
- _meta: record(string(), unknown()).nullish()
13062
- })]);
13063
- /**
13064
- * The contents of a resource, embedded into a prompt or tool call result.
13065
- */
13066
- const zEmbeddedResource = object({
13067
- annotations: defaultOnError(zAnnotations.nullish(), () => void 0),
13068
- resource: zEmbeddedResourceResource,
13069
- _meta: record(string(), unknown()).nullish()
13044
+ const AgentPrompt = object({
13045
+ sessionKey: SessionKey,
13046
+ agentId: string().uuid(),
13047
+ content: NormalizedMessageRef,
13048
+ seenUpToSeq: number().int()
13049
+ });
13050
+ const AgentPromptAck = object({
13051
+ accepted: boolean(),
13052
+ reason: _enum([
13053
+ "queued",
13054
+ "held",
13055
+ "scope_denied",
13056
+ "no_session",
13057
+ "stale"
13058
+ ]).optional(),
13059
+ seq: number().int()
13060
+ });
13061
+ const AgentActivity = object({
13062
+ agentId: string().uuid(),
13063
+ launchId: string().uuid(),
13064
+ state: _enum([
13065
+ "thinking",
13066
+ "tool_call",
13067
+ "awaiting_permission",
13068
+ "idle"
13069
+ ]),
13070
+ ts: string().datetime()
13071
+ });
13072
+ const AgentScopeDenied = object({
13073
+ agentId: string().uuid(),
13074
+ launchId: string().uuid(),
13075
+ capability: string()
13070
13076
  });
13077
+ //#endregion
13078
+ //#region ../protocol/dist/frames/integration.js
13071
13079
  /**
13072
- * Content blocks represent displayable information in the Agent Client Protocol.
13080
+ * Platform integration distribution (C→D) — the Slack "install" flow.
13073
13081
  *
13074
- * They provide a structured way to handle various types of user-facing content—whether
13075
- * it's text from language models, images for analysis, or embedded resources for context.
13082
+ * The Control Plane is the source of truth for platform integrations and pushes
13083
+ * them to the daemon that owns the integration's agent (`integration/upsert`, and
13084
+ * the reconcile snapshot `RegisterOk.integrations[]`). The daemon opens the Socket
13085
+ * Mode connection from the delivered config (see slack/connection.ts).
13076
13086
  *
13077
- * Content blocks appear in:
13078
- * - User prompts sent via `session/prompt`
13079
- * - Language model output streamed through `session/update` notifications
13080
- * - Progress updates and results from tool calls
13081
- *
13082
- * This structure is compatible with the Model Context Protocol (MCP), enabling
13083
- * agents to seamlessly forward content from MCP tool outputs without transformation.
13087
+ * SECURITY: `integration/upsert` and `RegisterOk.integrations[]` carry PLAINTEXT
13088
+ * platform tokens (botToken/appToken). These payloads MUST NEVER be logged — no
13089
+ * body dump on decode error, no register/ok snapshot debug dump. The daemon
13090
+ * persists them into the owning agent's local `agent.json` (same trust boundary
13091
+ * as hand-authored agents, which already keep tokens there) so integrations
13092
+ * survive a restart with the CP down.
13084
13093
  *
13085
- * See protocol docs: [Content](https://agentclientprotocol.com/protocol/content)
13094
+ * `signingSecret` is intentionally absent: Socket Mode authenticates with the
13095
+ * app-level token, so the daemon never needs a signing secret.
13086
13096
  */
13087
- const zContentBlock = union([
13088
- zTextContent.and(object({ type: literal("text") })),
13089
- zImageContent.and(object({ type: literal("image") })),
13090
- zAudioContent.and(object({ type: literal("audio") })),
13091
- zResourceLink.and(object({ type: literal("resource_link") })),
13092
- zEmbeddedResource.and(object({ type: literal("resource") }))
13097
+ /** Trigger match — mirrors the daemon BindRuleConfig.match (agents/agent-schema.ts). */
13098
+ const BindMatch = discriminatedUnion("kind", [
13099
+ object({ kind: literal("mention") }),
13100
+ object({ kind: literal("dm") }),
13101
+ object({
13102
+ kind: literal("keyword"),
13103
+ value: string()
13104
+ }),
13105
+ object({ kind: literal("auto") })
13093
13106
  ]);
13094
- /**
13095
- * Standard content block (text, images, resources).
13096
- */
13097
- const zContent = object({
13098
- content: zContentBlock,
13099
- _meta: record(string(), unknown()).nullish()
13107
+ /** One channel/thread trigger binding — mirrors the daemon BindRuleConfig. */
13108
+ const IntegrationBindRule = object({
13109
+ channel: string().optional(),
13110
+ thread: string().optional(),
13111
+ match: BindMatch
13100
13112
  });
13101
- /**
13102
- * A diff representing file modifications.
13103
- *
13104
- * Shows changes to files in a format suitable for display in the client UI.
13105
- *
13106
- * See protocol docs: [Content](https://agentclientprotocol.com/protocol/tool-calls#content)
13107
- */
13108
- const zDiff = object({
13109
- path: string(),
13110
- oldText: string().nullish(),
13111
- newText: string(),
13112
- _meta: record(string(), unknown()).nullish()
13113
+ /** The Slack config the daemon needs to open + route a socket (no signingSecret). */
13114
+ const IntegrationSlackConfig = object({
13115
+ botToken: string(),
13116
+ appToken: string(),
13117
+ allowedUserIds: array(string()).default([]),
13118
+ notificationChannelId: string().optional(),
13119
+ bindRules: array(IntegrationBindRule).default([])
13113
13120
  });
13114
13121
  /**
13115
- * Typed identifier used for terminal values on the wire.
13122
+ * One platform integration, owned by exactly one agent. Also the element type of
13123
+ * `RegisterOk.integrations[]` (the per-daemon reconcile set).
13116
13124
  */
13117
- const zTerminalId = string();
13125
+ const IntegrationSpec = object({
13126
+ integrationId: string().uuid(),
13127
+ agentId: string().uuid(),
13128
+ platform: literal("slack"),
13129
+ slack: IntegrationSlackConfig
13130
+ });
13131
+ /** C→D EVT — install/update an integration on the owning agent's daemon. */
13132
+ const IntegrationUpsert = IntegrationSpec;
13133
+ /** C→D EVT — remove an integration from the daemon. */
13134
+ const IntegrationRemove = object({ integrationId: string().uuid() });
13135
+ //#endregion
13136
+ //#region ../protocol/dist/frames/register.js
13118
13137
  /**
13119
- * Embed a terminal created with `terminal/create` by its id.
13120
- *
13121
- * The terminal must be added before calling `terminal/release`.
13138
+ * Capability upload + the reconcile snapshot — protocol §3.3.
13122
13139
  *
13123
- * See protocol docs: [Terminal](https://agentclientprotocol.com/protocol/terminals)
13140
+ * `register/ok` is the authoritative source of truth: the daemon converges its
13141
+ * local cache to it. CP wins all conflicts, so re-issuing the same snapshot is
13142
+ * idempotent.
13124
13143
  */
13125
- const zTerminal = object({
13126
- terminalId: zTerminalId,
13127
- _meta: record(string(), unknown()).nullish()
13144
+ const RegisterReq = object({
13145
+ host: string(),
13146
+ capabilities: object({
13147
+ platforms: array(Platform),
13148
+ runtimes: array(string()),
13149
+ acp: boolean(),
13150
+ features: array(string()).default([])
13151
+ }),
13152
+ maxAgents: number().int(),
13153
+ localState: object({
13154
+ assignments: array(string()),
13155
+ crons: array(string()),
13156
+ leases: array(string())
13157
+ })
13158
+ });
13159
+ const RegisterOk = object({
13160
+ routingEpoch: number().int(),
13161
+ assignments: array(RouteAssign),
13162
+ agents: array(AgentSpec.extend({ agentId: string().uuid() })).default([]),
13163
+ crons: array(CronUpsert),
13164
+ integrations: array(IntegrationSpec).default([]),
13165
+ leases: array(SecretsGrant),
13166
+ drop: object({
13167
+ assignments: array(string()),
13168
+ crons: array(string())
13169
+ })
13128
13170
  });
13171
+ //#endregion
13172
+ //#region ../protocol/dist/frames/session.js
13129
13173
  /**
13130
- * Content produced by a tool call.
13174
+ * Session read-back (C→D REQ → REP) — the console's on-demand pulls.
13131
13175
  *
13132
- * Tool calls can produce different types of content including
13133
- * standard content blocks (text, images) or file diffs.
13176
+ * The CP stores NO session data — neither the list nor the bodies. Sessions are
13177
+ * created on the Slack→daemon path and live solely in the daemon's local store
13178
+ * (body-locality, §1/§12). So both the session **list** and a session's chat
13179
+ * **history** are pulled live from the owning daemon(s) and proxied to the
13180
+ * console — never persisted on the CP, never on the orchestration hot path.
13134
13181
  *
13135
- * See protocol docs: [Content](https://agentclientprotocol.com/protocol/tool-calls#content)
13182
+ * - `session/list`: the daemon's live sessions (CP fans this out to all the org's
13183
+ * online daemons and merges, for "all sessions across the workspace").
13184
+ * - `session/history`: one cursor-paginated page of a session's transcript.
13136
13185
  */
13137
- const zToolCallContent = union([
13138
- zContent.and(object({ type: literal("content") })),
13139
- zDiff.and(object({ type: literal("diff") })),
13140
- zTerminal.and(object({ type: literal("terminal") }))
13141
- ]);
13142
13186
  /**
13143
- * A file location being accessed or modified by a tool.
13144
- *
13145
- * Enables clients to implement "follow-along" features that track
13146
- * which files the agent is working with in real-time.
13147
- *
13148
- * See protocol docs: [Following the Agent](https://agentclientprotocol.com/protocol/tool-calls#following-the-agent)
13187
+ * Per-session token accounting, metered by the daemon from the agent's ACP usage
13188
+ * stream. Token counts are CUMULATIVE across the session's turns (summed from each
13189
+ * turn's `PromptResponse.usage`); context/cost are the LATEST snapshot from the
13190
+ * agent's `usage_update` stream. Every field is optional — a runtime that reports
13191
+ * no usage yields an absent field, not a zero.
13149
13192
  */
13150
- const zToolCallLocation = object({
13151
- path: string(),
13152
- line: int().gte(0).max(4294967295, { error: "Invalid value: Expected uint32 to be <= 4294967295" }).nullish(),
13153
- _meta: record(string(), unknown()).nullish()
13193
+ const SessionUsage = object({
13194
+ totalTokens: number().int().optional(),
13195
+ inputTokens: number().int().optional(),
13196
+ outputTokens: number().int().optional(),
13197
+ thoughtTokens: number().int().optional(),
13198
+ cachedReadTokens: number().int().optional(),
13199
+ cachedWriteTokens: number().int().optional(),
13200
+ contextUsed: number().int().optional(),
13201
+ contextSize: number().int().optional(),
13202
+ costAmount: number().optional(),
13203
+ costCurrency: string().optional()
13204
+ });
13205
+ /** One row in the session list (metadata + console metrics; NOT the transcript). */
13206
+ const SessionListItem = object({
13207
+ sessionId: string(),
13208
+ sessionKey: SessionKey,
13209
+ agentId: string().uuid(),
13210
+ title: string().optional(),
13211
+ status: string().optional(),
13212
+ lastActivityAt: string().optional(),
13213
+ usage: SessionUsage.optional(),
13214
+ triggeredBy: string().optional()
13215
+ });
13216
+ /** C→D REQ: list the daemon's sessions (optionally just one agent's). */
13217
+ const SessionListReq = object({ agentId: string().uuid().optional() });
13218
+ /** D→C REP (corr = req id): the daemon's current sessions. */
13219
+ const SessionListPage = object({ sessions: array(SessionListItem) });
13220
+ /** One message in a session transcript page (a body — returned only for display). */
13221
+ const SessionMessage = object({
13222
+ seq: number().int(),
13223
+ sender: string(),
13224
+ ts: string(),
13225
+ kind: string(),
13226
+ text: string()
13227
+ });
13228
+ /** C→D REQ: fetch one page of a session's history from the owning daemon. */
13229
+ const SessionHistoryReq = object({
13230
+ sessionId: string(),
13231
+ cursor: string().optional(),
13232
+ limit: number().int().positive().max(200).default(50)
13233
+ });
13234
+ /** D→C REP (corr = the req id): a page of messages + the cursor for the next page. */
13235
+ const SessionHistoryPage = object({
13236
+ sessionId: string(),
13237
+ messages: array(SessionMessage),
13238
+ nextCursor: string().optional()
13154
13239
  });
13240
+ //#endregion
13241
+ //#region ../protocol/dist/frame.js
13155
13242
  /**
13156
- * An update to an existing tool call.
13243
+ * The single source of truth for the wire: `type` string → payload zod schema.
13157
13244
  *
13158
- * Used to report progress and results as tools execute. All fields except
13159
- * the tool call ID are optional - only changed fields need to be included.
13245
+ * Mirrors the frame index in daemon-cp-ws-protocol.md §10 (the 29 numbered
13246
+ * frames) plus the correlated REP types named in the "Reply" column
13247
+ * (`route/assign/ack`, `drain/done`, and the generic `ack` replies) that also
13248
+ * travel on the wire and must be decodable.
13160
13249
  *
13161
- * See protocol docs: [Updating](https://agentclientprotocol.com/protocol/tool-calls#updating)
13162
- */
13163
- const zToolCallUpdate = object({
13164
- toolCallId: zToolCallId,
13165
- kind: defaultOnError(zToolKind.nullish(), () => void 0),
13166
- status: defaultOnError(zToolCallStatus.nullish(), () => void 0),
13167
- title: string().nullish(),
13168
- content: defaultOnError(vecSkipError(zToolCallContent).nullish(), () => void 0),
13169
- locations: defaultOnError(vecSkipError(zToolCallLocation).nullish(), () => void 0),
13170
- rawInput: unknown().optional(),
13171
- rawOutput: unknown().optional(),
13172
- _meta: record(string(), unknown()).nullish()
13173
- });
13174
- /**
13175
- * Unique identifier for a permission option.
13250
+ * `ws/codec.ts` validates every inbound `payload` against `FRAME_SCHEMAS[type]`;
13251
+ * an unknown `type` → `ErrorFrame{code:"UNKNOWN_FRAME"}` (a REP, not a close).
13176
13252
  */
13177
- const zPermissionOptionId = string();
13253
+ const FRAME_SCHEMAS = {
13254
+ auth: AuthReq,
13255
+ "auth/ok": AuthOk,
13256
+ register: RegisterReq,
13257
+ "register/ok": RegisterOk,
13258
+ heartbeat: object({
13259
+ load: object({
13260
+ cpu: number(),
13261
+ mem: number(),
13262
+ agents: number().int()
13263
+ }),
13264
+ health: _enum(["ok", "degraded"]),
13265
+ activeSessions: number().int(),
13266
+ degradedScopes: array(string()).default([])
13267
+ }),
13268
+ "agent/launch": AgentLaunch,
13269
+ "agent/launched": AgentLaunched,
13270
+ "agent/stop": AgentStop,
13271
+ "agent/upsert": AgentUpsert,
13272
+ "agent/remove": AgentRemove,
13273
+ "agent/prompt": AgentPrompt,
13274
+ "agent/prompt/ack": AgentPromptAck,
13275
+ "agent/activity": AgentActivity,
13276
+ "agent/scope-denied": AgentScopeDenied,
13277
+ "route/assign": RouteAssign,
13278
+ "route/assign/ack": RouteAssignAck,
13279
+ "route/update": RouteUpdate,
13280
+ "daemon/drain": Drain,
13281
+ "drain/progress": DrainProgress,
13282
+ "drain/done": DrainDone,
13283
+ "cron/upsert": CronUpsert,
13284
+ "cron/remove": CronRemove,
13285
+ "integration/upsert": IntegrationUpsert,
13286
+ "integration/remove": IntegrationRemove,
13287
+ "secrets/request": SecretsRequest,
13288
+ "secrets/grant": SecretsGrant,
13289
+ "secrets/renew": SecretsRenew,
13290
+ "secrets/revoke": SecretsRevoke,
13291
+ "scope-attestation": ScopeAttestation,
13292
+ "event/session": object({
13293
+ sessionId: string().uuid(),
13294
+ agentId: string().uuid(),
13295
+ launchId: string().uuid(),
13296
+ phase: _enum([
13297
+ "start",
13298
+ "plan",
13299
+ "problem",
13300
+ "end"
13301
+ ]),
13302
+ link: string().optional(),
13303
+ summary: string().optional(),
13304
+ ts: string().datetime()
13305
+ }),
13306
+ "usage/report": object({
13307
+ sessionId: string(),
13308
+ agentId: string().uuid(),
13309
+ platform: string().optional(),
13310
+ channel: string().optional(),
13311
+ lastActivityAt: string(),
13312
+ usage: SessionUsage
13313
+ }),
13314
+ "facts/runtime-profile": object({
13315
+ runtime: string(),
13316
+ version: string(),
13317
+ models: array(string()),
13318
+ contextWindow: number().int().optional(),
13319
+ acpSupport: _enum([
13320
+ "full",
13321
+ "partial",
13322
+ "none"
13323
+ ]),
13324
+ toolCalling: boolean()
13325
+ }),
13326
+ "session/list": SessionListReq,
13327
+ "session/list/page": SessionListPage,
13328
+ "session/history": SessionHistoryReq,
13329
+ "session/history/page": SessionHistoryPage,
13330
+ "config/push": object({ keys: record(string(), unknown()) }),
13331
+ "daemon/restart": object({
13332
+ reason: string(),
13333
+ drainFirst: boolean().default(true)
13334
+ }),
13335
+ "daemon/upgrade": object({
13336
+ targetVersion: string(),
13337
+ drainFirst: boolean().default(true)
13338
+ }),
13339
+ "daemon/control/ack": object({
13340
+ accepted: boolean(),
13341
+ willDrainUntil: string().datetime().optional()
13342
+ }),
13343
+ ack: object({
13344
+ ok: boolean(),
13345
+ reason: string().optional()
13346
+ }),
13347
+ error: object({
13348
+ code: _enum([
13349
+ "UNKNOWN_FRAME",
13350
+ "FRAME_TOO_LARGE",
13351
+ "PROTOCOL_STATE",
13352
+ "BAD_PAYLOAD",
13353
+ "AUTH_FAILED",
13354
+ "ATTESTATION_INVALID",
13355
+ "STALE_EPOCH",
13356
+ "STALE_LAUNCH",
13357
+ "SEQ_GAP",
13358
+ "NO_SESSION",
13359
+ "HELD",
13360
+ "SCOPE_DENIED",
13361
+ "LEASE_EXPIRED",
13362
+ "LEASE_DENIED",
13363
+ "RATE_LIMITED",
13364
+ "INTERNAL"
13365
+ ]),
13366
+ message: string(),
13367
+ retryable: boolean(),
13368
+ details: record(string(), unknown()).optional()
13369
+ })
13370
+ };
13371
+ Object.keys(FRAME_SCHEMAS);
13178
13372
  /**
13179
- * The type of permission option being presented to the user.
13180
- *
13181
- * Helps clients choose appropriate icons and UI treatment.
13373
+ * Builds the envelope schema for one frame `type` with a `type` literal and the
13374
+ * typed payload, so the discriminated union infers `payload` precisely.
13182
13375
  */
13183
- const zPermissionOptionKind = union([
13184
- literal("allow_once"),
13185
- literal("allow_always"),
13186
- literal("reject_once"),
13187
- literal("reject_always")
13376
+ function frame(type, payload) {
13377
+ return object({
13378
+ v: literal(1),
13379
+ id: string().uuid(),
13380
+ ts: string().datetime(),
13381
+ type: literal(type),
13382
+ corr: string().uuid().optional(),
13383
+ payload
13384
+ });
13385
+ }
13386
+ discriminatedUnion("type", [
13387
+ frame("auth", FRAME_SCHEMAS["auth"]),
13388
+ frame("auth/ok", FRAME_SCHEMAS["auth/ok"]),
13389
+ frame("register", FRAME_SCHEMAS["register"]),
13390
+ frame("register/ok", FRAME_SCHEMAS["register/ok"]),
13391
+ frame("heartbeat", FRAME_SCHEMAS["heartbeat"]),
13392
+ frame("agent/launch", FRAME_SCHEMAS["agent/launch"]),
13393
+ frame("agent/launched", FRAME_SCHEMAS["agent/launched"]),
13394
+ frame("agent/stop", FRAME_SCHEMAS["agent/stop"]),
13395
+ frame("agent/upsert", FRAME_SCHEMAS["agent/upsert"]),
13396
+ frame("agent/remove", FRAME_SCHEMAS["agent/remove"]),
13397
+ frame("agent/prompt", FRAME_SCHEMAS["agent/prompt"]),
13398
+ frame("agent/prompt/ack", FRAME_SCHEMAS["agent/prompt/ack"]),
13399
+ frame("agent/activity", FRAME_SCHEMAS["agent/activity"]),
13400
+ frame("agent/scope-denied", FRAME_SCHEMAS["agent/scope-denied"]),
13401
+ frame("route/assign", FRAME_SCHEMAS["route/assign"]),
13402
+ frame("route/assign/ack", FRAME_SCHEMAS["route/assign/ack"]),
13403
+ frame("route/update", FRAME_SCHEMAS["route/update"]),
13404
+ frame("daemon/drain", FRAME_SCHEMAS["daemon/drain"]),
13405
+ frame("drain/progress", FRAME_SCHEMAS["drain/progress"]),
13406
+ frame("drain/done", FRAME_SCHEMAS["drain/done"]),
13407
+ frame("cron/upsert", FRAME_SCHEMAS["cron/upsert"]),
13408
+ frame("cron/remove", FRAME_SCHEMAS["cron/remove"]),
13409
+ frame("integration/upsert", FRAME_SCHEMAS["integration/upsert"]),
13410
+ frame("integration/remove", FRAME_SCHEMAS["integration/remove"]),
13411
+ frame("secrets/request", FRAME_SCHEMAS["secrets/request"]),
13412
+ frame("secrets/grant", FRAME_SCHEMAS["secrets/grant"]),
13413
+ frame("secrets/renew", FRAME_SCHEMAS["secrets/renew"]),
13414
+ frame("secrets/revoke", FRAME_SCHEMAS["secrets/revoke"]),
13415
+ frame("scope-attestation", FRAME_SCHEMAS["scope-attestation"]),
13416
+ frame("event/session", FRAME_SCHEMAS["event/session"]),
13417
+ frame("usage/report", FRAME_SCHEMAS["usage/report"]),
13418
+ frame("facts/runtime-profile", FRAME_SCHEMAS["facts/runtime-profile"]),
13419
+ frame("session/list", FRAME_SCHEMAS["session/list"]),
13420
+ frame("session/list/page", FRAME_SCHEMAS["session/list/page"]),
13421
+ frame("session/history", FRAME_SCHEMAS["session/history"]),
13422
+ frame("session/history/page", FRAME_SCHEMAS["session/history/page"]),
13423
+ frame("config/push", FRAME_SCHEMAS["config/push"]),
13424
+ frame("daemon/restart", FRAME_SCHEMAS["daemon/restart"]),
13425
+ frame("daemon/upgrade", FRAME_SCHEMAS["daemon/upgrade"]),
13426
+ frame("daemon/control/ack", FRAME_SCHEMAS["daemon/control/ack"]),
13427
+ frame("ack", FRAME_SCHEMAS["ack"]),
13428
+ frame("error", FRAME_SCHEMAS["error"])
13188
13429
  ]);
13430
+ //#endregion
13431
+ //#region ../protocol/dist/codec.js
13432
+ /** Soft cap per frame — 256 KiB (protocol §1). Over this → FRAME_TOO_LARGE. */
13433
+ const MAX_FRAME_BYTES = 256 * 1024;
13434
+ const textEncoder = new TextEncoder();
13435
+ function byteLength(text) {
13436
+ return textEncoder.encode(text).length;
13437
+ }
13438
+ function extractControlExt(json) {
13439
+ if (typeof json !== "object" || json === null) return void 0;
13440
+ const o = json;
13441
+ const ext = {};
13442
+ if (typeof o.epoch === "number") ext.epoch = o.epoch;
13443
+ if (typeof o.seq === "number") ext.seq = o.seq;
13444
+ if (typeof o.agentId === "string") ext.agentId = o.agentId;
13445
+ if (typeof o.launchId === "string") ext.launchId = o.launchId;
13446
+ return Object.keys(ext).length > 0 ? ext : void 0;
13447
+ }
13448
+ function decodeEnvelope(text) {
13449
+ if (byteLength(text) > 262144) return {
13450
+ ok: false,
13451
+ id: NIL_UUID,
13452
+ msg: "FRAME_TOO_LARGE"
13453
+ };
13454
+ let json;
13455
+ try {
13456
+ json = JSON.parse(text);
13457
+ } catch {
13458
+ return {
13459
+ ok: false,
13460
+ id: NIL_UUID,
13461
+ msg: "invalid json"
13462
+ };
13463
+ }
13464
+ const env = Envelope.safeParse(json);
13465
+ if (!env.success) return {
13466
+ ok: false,
13467
+ id: typeof json === "object" && json !== null && typeof json.id === "string" ? json.id : NIL_UUID,
13468
+ msg: env.error.message
13469
+ };
13470
+ const schema = FRAME_SCHEMAS[env.data.type];
13471
+ if (!schema) return {
13472
+ ok: false,
13473
+ id: env.data.id,
13474
+ msg: "UNKNOWN_FRAME"
13475
+ };
13476
+ const payload = schema.safeParse(env.data.payload);
13477
+ if (!payload.success) return {
13478
+ ok: false,
13479
+ id: env.data.id,
13480
+ msg: payload.error.message
13481
+ };
13482
+ const ext = extractControlExt(json);
13483
+ return {
13484
+ ok: true,
13485
+ frame: {
13486
+ ...env.data,
13487
+ payload: payload.data
13488
+ },
13489
+ ...ext ? { ext } : {}
13490
+ };
13491
+ }
13492
+ function buildEnvelope(type, payload, opts = {}) {
13493
+ return {
13494
+ v: 1,
13495
+ id: opts.id ?? randomUUID(),
13496
+ ts: opts.ts ?? (/* @__PURE__ */ new Date()).toISOString(),
13497
+ type,
13498
+ payload,
13499
+ ...opts.corr ? { corr: opts.corr } : {},
13500
+ ...opts.ext ?? {}
13501
+ };
13502
+ }
13503
+ function encode(frame) {
13504
+ return JSON.stringify(frame);
13505
+ }
13506
+ //#endregion
13507
+ //#region ../protocol/dist/git-url.js
13189
13508
  /**
13190
- * Request for user permission to execute a tool call.
13509
+ * Git repo address helpers, shared by the CP (normalize on write) and the
13510
+ * daemon (defensive normalize before clone).
13191
13511
  *
13192
- * Sent when the agent needs authorization before performing a sensitive operation.
13193
- *
13194
- * See protocol docs: [Requesting Permission](https://agentclientprotocol.com/protocol/tool-calls#requesting-permission)
13512
+ * STORAGE INVARIANT: `workspace.gitRepo` is persisted as a FULL cloneable git
13513
+ * address (e.g. `https://github.com/acme/infra`), never the `acme/infra`
13514
+ * shorthand a user may type. UIs shorten it back to `org/repo` for display
13515
+ * with `gitRepoLabel`.
13195
13516
  */
13196
- const zRequestPermissionRequest = object({
13197
- sessionId: zSessionId,
13198
- toolCall: zToolCallUpdate,
13199
- options: array(object({
13200
- optionId: zPermissionOptionId,
13201
- name: string(),
13202
- kind: zPermissionOptionKind,
13203
- _meta: record(string(), unknown()).nullish()
13204
- })),
13205
- _meta: record(string(), unknown()).nullish()
13206
- });
13517
+ /** Scheme-full address: https://, ssh://, git://, file://, … */
13518
+ const SCHEME_RE = /^[a-z][a-z0-9+.-]*:\/\//i;
13519
+ /** scp-like ssh address: git@github.com:acme/infra(.git) */
13520
+ const SCP_RE = /^[\w.-]+@[\w.-]+:(.+)$/;
13207
13521
  /**
13208
- * An environment variable to set when launching an MCP server.
13522
+ * Normalize a user-supplied repo reference into a full cloneable git address.
13523
+ * Idempotent — full addresses pass through unchanged.
13524
+ *
13525
+ * - `https://…` / `ssh://…` / `git://…` / `git@host:…` → as-is
13526
+ * - `github.com/acme/infra` (host-prefixed shorthand) → `https://github.com/acme/infra`
13527
+ * - `acme/infra` (bare org/repo) → `https://github.com/acme/infra`
13528
+ * - anything else (e.g. a local path) → as-is
13209
13529
  */
13210
- const zEnvVariable = object({
13211
- name: string(),
13212
- value: string(),
13213
- _meta: record(string(), unknown()).nullish()
13214
- });
13530
+ function normalizeGitUrl(input) {
13531
+ const s = input.trim().replace(/\/+$/, "");
13532
+ if (!s || SCHEME_RE.test(s) || SCP_RE.test(s)) return s;
13533
+ const segments = s.split("/");
13534
+ if (segments.length >= 3 && segments[0].includes(".")) return `https://${s}`;
13535
+ if (segments.length === 2 && segments.every((p) => p.length > 0)) return `https://github.com/${s}`;
13536
+ return s;
13537
+ }
13538
+ //#endregion
13539
+ //#region ../protocol/dist/index.js
13215
13540
  /**
13216
- * Request to create a new terminal and execute a command.
13541
+ * Narrowing guard factory: `isFrame("auth")(frame)` narrows a decoded
13542
+ * `AnyFrame` to the member whose `type` matches.
13217
13543
  */
13218
- const zCreateTerminalRequest = object({
13219
- sessionId: zSessionId,
13220
- command: string(),
13221
- args: array(string()).optional(),
13544
+ function isFrame(type) {
13545
+ return (frame) => frame.type === type;
13546
+ }
13547
+ //#endregion
13548
+ //#region src/workspace/workspace-manager.ts
13549
+ const PULL_TIMEOUT_MS = 4500;
13550
+ const cloneInFlight = /* @__PURE__ */ new Map();
13551
+ async function prepareWorkspace(agent) {
13552
+ const cwd = agent.workspace.path;
13553
+ mkdirSync(cwd, { recursive: true });
13554
+ if (agent.workspace.mode === "from-scratch") {
13555
+ const mem = join(cwd, "memory.md");
13556
+ if (!existsSync(mem)) writeFileSync(mem, `# ${agent.name} memory\n`);
13557
+ return cwd;
13558
+ }
13559
+ if (!existsSync(join(cwd, ".git"))) {
13560
+ await cloneRepo(agent);
13561
+ return cwd;
13562
+ }
13563
+ if (agent.workspace.pullOnNewSession) try {
13564
+ await Promise.race([simpleGit(cwd).pull(["--ff-only"]), new Promise((_, rej) => setTimeout(() => rej(/* @__PURE__ */ new Error("pull timeout")), PULL_TIMEOUT_MS))]);
13565
+ } catch {}
13566
+ return cwd;
13567
+ }
13568
+ /**
13569
+ * Eagerly clone a git-repo workspace that has no checkout yet — for reconcile-time
13570
+ * prefetch so the repo is warm before the first message arrives. No-op for
13571
+ * from-scratch or an already-cloned checkout (the session-time `prepareWorkspace`
13572
+ * stays authoritative and still owns pull + hard-fail). Reuses the single-flight
13573
+ * lock so a prefetch and a concurrent session-start clone never race into the same
13574
+ * dir. The caller invokes this fire-and-forget and logs any rejection — clone
13575
+ * failure here is non-fatal because `prepareWorkspace` re-clones (and hard-fails)
13576
+ * on the first session as the backstop.
13577
+ */
13578
+ async function prefetchWorkspace(agent) {
13579
+ if (agent.workspace.mode !== "git-repo") return;
13580
+ const cwd = agent.workspace.path;
13581
+ if (existsSync(join(cwd, ".git"))) return;
13582
+ mkdirSync(cwd, { recursive: true });
13583
+ await cloneRepo(agent);
13584
+ }
13585
+ /** Clone agent.workspace.gitRepo @ gitBranch into cwd, single-flight per cwd. */
13586
+ async function cloneRepo(agent) {
13587
+ const cwd = agent.workspace.path;
13588
+ const inflight = cloneInFlight.get(cwd);
13589
+ if (inflight) return inflight;
13590
+ const gitRepo = agent.workspace.gitRepo && normalizeGitUrl(agent.workspace.gitRepo);
13591
+ if (!gitRepo) throw new Error(`workspace clone: agent "${agent.id}" has git-repo mode but no gitRepo configured`);
13592
+ const branch = agent.workspace.gitBranch;
13593
+ const p = (async () => {
13594
+ await simpleGit().clone(gitRepo, cwd, [
13595
+ "--branch",
13596
+ branch,
13597
+ "--single-branch"
13598
+ ]);
13599
+ })().finally(() => {
13600
+ cloneInFlight.delete(cwd);
13601
+ });
13602
+ cloneInFlight.set(cwd, p);
13603
+ return p;
13604
+ }
13605
+ //#endregion
13606
+ //#region ../../node_modules/.pnpm/@agentclientprotocol+sdk@1.0.0_zod@4.4.3/node_modules/@agentclientprotocol/sdk/dist/schema/index.js
13607
+ const AGENT_METHODS = {
13608
+ initialize: "initialize",
13609
+ authenticate: "authenticate",
13610
+ providers_list: "providers/list",
13611
+ providers_set: "providers/set",
13612
+ providers_disable: "providers/disable",
13613
+ session_new: "session/new",
13614
+ session_load: "session/load",
13615
+ session_set_mode: "session/set_mode",
13616
+ session_set_config_option: "session/set_config_option",
13617
+ session_prompt: "session/prompt",
13618
+ session_cancel: "session/cancel",
13619
+ mcp_message: "mcp/message",
13620
+ session_list: "session/list",
13621
+ session_delete: "session/delete",
13622
+ session_fork: "session/fork",
13623
+ session_resume: "session/resume",
13624
+ session_close: "session/close",
13625
+ logout: "logout",
13626
+ nes_start: "nes/start",
13627
+ nes_suggest: "nes/suggest",
13628
+ nes_accept: "nes/accept",
13629
+ nes_reject: "nes/reject",
13630
+ nes_close: "nes/close",
13631
+ document_did_open: "document/didOpen",
13632
+ document_did_change: "document/didChange",
13633
+ document_did_close: "document/didClose",
13634
+ document_did_save: "document/didSave",
13635
+ document_did_focus: "document/didFocus"
13636
+ };
13637
+ const CLIENT_METHODS = {
13638
+ session_request_permission: "session/request_permission",
13639
+ session_update: "session/update",
13640
+ fs_write_text_file: "fs/write_text_file",
13641
+ fs_read_text_file: "fs/read_text_file",
13642
+ terminal_create: "terminal/create",
13643
+ terminal_output: "terminal/output",
13644
+ terminal_release: "terminal/release",
13645
+ terminal_wait_for_exit: "terminal/wait_for_exit",
13646
+ terminal_kill: "terminal/kill",
13647
+ mcp_connect: "mcp/connect",
13648
+ mcp_message: "mcp/message",
13649
+ mcp_disconnect: "mcp/disconnect",
13650
+ elicitation_create: "elicitation/create",
13651
+ elicitation_complete: "elicitation/complete"
13652
+ };
13653
+ const PROTOCOL_METHODS = { cancel_request: "$/cancel_request" };
13654
+ //#endregion
13655
+ //#region ../../node_modules/.pnpm/@agentclientprotocol+sdk@1.0.0_zod@4.4.3/node_modules/@agentclientprotocol/sdk/dist/schema-deserialize.js
13656
+ const skippedItem = Symbol("skippedItem");
13657
+ function defaultOnError(schema, fallback) {
13658
+ return schema.catch(fallback);
13659
+ }
13660
+ function requiredDefaultOnError(schema, fallback) {
13661
+ const schemaWithCatch = schema.catch(fallback);
13662
+ return unknown().transform((value, context) => {
13663
+ if (value !== void 0) return schemaWithCatch.parse(value);
13664
+ context.addIssue({
13665
+ code: "custom",
13666
+ message: "Required value is missing"
13667
+ });
13668
+ return NEVER;
13669
+ });
13670
+ }
13671
+ function vecSkipError(itemSchema) {
13672
+ return array(itemSchema.catch(skippedItem)).transform((items) => items.filter((item) => item !== skippedItem));
13673
+ }
13674
+ //#endregion
13675
+ //#region ../../node_modules/.pnpm/@agentclientprotocol+sdk@1.0.0_zod@4.4.3/node_modules/@agentclientprotocol/sdk/dist/schema/zod.gen.js
13676
+ /**
13677
+ * JSON RPC Request Id
13678
+ *
13679
+ * An identifier established by the Client that MUST contain a String, Number, or NULL value if included. If it is not included it is assumed to be a notification. The value SHOULD normally not be Null \[1\] and Numbers SHOULD NOT contain fractional parts \[2\]
13680
+ *
13681
+ * The Server MUST reply with the same value in the Response object if included. This member is used to correlate the context between the two objects.
13682
+ *
13683
+ * \[1\] The use of Null as a value for the id member in a Request object is discouraged, because this specification uses a value of Null for Responses with an unknown id. Also, because JSON-RPC 1.0 uses an id value of Null for Notifications this could cause confusion in handling.
13684
+ *
13685
+ * \[2\] Fractional parts may be problematic, since many decimal fractions cannot be represented exactly as binary fractions.
13686
+ */
13687
+ const zRequestId = union([number(), string()]).nullable();
13688
+ /**
13689
+ * A unique identifier for a conversation session between a client and agent.
13690
+ *
13691
+ * Sessions maintain their own context, conversation history, and state,
13692
+ * allowing multiple independent interactions with the same agent.
13693
+ *
13694
+ * See protocol docs: [Session ID](https://agentclientprotocol.com/protocol/session-setup#session-id)
13695
+ */
13696
+ const zSessionId = string();
13697
+ /**
13698
+ * Request to write content to a text file.
13699
+ *
13700
+ * Only available if the client supports the `fs.writeTextFile` capability.
13701
+ */
13702
+ const zWriteTextFileRequest = object({
13703
+ sessionId: zSessionId,
13704
+ path: string(),
13705
+ content: string(),
13706
+ _meta: record(string(), unknown()).nullish()
13707
+ });
13708
+ /**
13709
+ * Request to read content from a text file.
13710
+ *
13711
+ * Only available if the client supports the `fs.readTextFile` capability.
13712
+ */
13713
+ const zReadTextFileRequest = object({
13714
+ sessionId: zSessionId,
13715
+ path: string(),
13716
+ line: int().gte(0).max(4294967295, { error: "Invalid value: Expected uint32 to be <= 4294967295" }).nullish(),
13717
+ limit: int().gte(0).max(4294967295, { error: "Invalid value: Expected uint32 to be <= 4294967295" }).nullish(),
13718
+ _meta: record(string(), unknown()).nullish()
13719
+ });
13720
+ /**
13721
+ * Unique identifier for a tool call within a session.
13722
+ */
13723
+ const zToolCallId = string();
13724
+ /**
13725
+ * Categories of tools that can be invoked.
13726
+ *
13727
+ * Tool kinds help clients choose appropriate icons and optimize how they
13728
+ * display tool execution progress.
13729
+ *
13730
+ * See protocol docs: [Creating](https://agentclientprotocol.com/protocol/tool-calls#creating)
13731
+ */
13732
+ const zToolKind = union([
13733
+ literal("read"),
13734
+ literal("edit"),
13735
+ literal("delete"),
13736
+ literal("move"),
13737
+ literal("search"),
13738
+ literal("execute"),
13739
+ literal("think"),
13740
+ literal("fetch"),
13741
+ literal("switch_mode"),
13742
+ literal("other")
13743
+ ]);
13744
+ /**
13745
+ * Execution status of a tool call.
13746
+ *
13747
+ * Tool calls progress through different statuses during their lifecycle.
13748
+ *
13749
+ * See protocol docs: [Status](https://agentclientprotocol.com/protocol/tool-calls#status)
13750
+ */
13751
+ const zToolCallStatus = union([
13752
+ literal("pending"),
13753
+ literal("in_progress"),
13754
+ literal("completed"),
13755
+ literal("failed")
13756
+ ]);
13757
+ /**
13758
+ * Optional annotations for the client. The client can use annotations to inform how objects are used or displayed
13759
+ */
13760
+ const zAnnotations = object({
13761
+ audience: defaultOnError(vecSkipError(union([literal("assistant"), literal("user")])).nullish(), () => void 0),
13762
+ lastModified: string().nullish(),
13763
+ priority: number().nullish(),
13764
+ _meta: record(string(), unknown()).nullish()
13765
+ });
13766
+ /**
13767
+ * Text provided to or from an LLM.
13768
+ */
13769
+ const zTextContent = object({
13770
+ annotations: defaultOnError(zAnnotations.nullish(), () => void 0),
13771
+ text: string(),
13772
+ _meta: record(string(), unknown()).nullish()
13773
+ });
13774
+ /**
13775
+ * An image provided to or from an LLM.
13776
+ */
13777
+ const zImageContent = object({
13778
+ annotations: defaultOnError(zAnnotations.nullish(), () => void 0),
13779
+ data: string(),
13780
+ mimeType: string(),
13781
+ uri: string().nullish(),
13782
+ _meta: record(string(), unknown()).nullish()
13783
+ });
13784
+ /**
13785
+ * Audio provided to or from an LLM.
13786
+ */
13787
+ const zAudioContent = object({
13788
+ annotations: defaultOnError(zAnnotations.nullish(), () => void 0),
13789
+ data: string(),
13790
+ mimeType: string(),
13791
+ _meta: record(string(), unknown()).nullish()
13792
+ });
13793
+ /**
13794
+ * A resource that the server is capable of reading, included in a prompt or tool call result.
13795
+ */
13796
+ const zResourceLink = object({
13797
+ annotations: defaultOnError(zAnnotations.nullish(), () => void 0),
13798
+ description: string().nullish(),
13799
+ mimeType: string().nullish(),
13800
+ name: string(),
13801
+ size: number().nullish(),
13802
+ title: string().nullish(),
13803
+ uri: string(),
13804
+ _meta: record(string(), unknown()).nullish()
13805
+ });
13806
+ /**
13807
+ * Resource content that can be embedded in a message.
13808
+ */
13809
+ const zEmbeddedResourceResource = union([object({
13810
+ mimeType: string().nullish(),
13811
+ text: string(),
13812
+ uri: string(),
13813
+ _meta: record(string(), unknown()).nullish()
13814
+ }), object({
13815
+ blob: string(),
13816
+ mimeType: string().nullish(),
13817
+ uri: string(),
13818
+ _meta: record(string(), unknown()).nullish()
13819
+ })]);
13820
+ /**
13821
+ * The contents of a resource, embedded into a prompt or tool call result.
13822
+ */
13823
+ const zEmbeddedResource = object({
13824
+ annotations: defaultOnError(zAnnotations.nullish(), () => void 0),
13825
+ resource: zEmbeddedResourceResource,
13826
+ _meta: record(string(), unknown()).nullish()
13827
+ });
13828
+ /**
13829
+ * Content blocks represent displayable information in the Agent Client Protocol.
13830
+ *
13831
+ * They provide a structured way to handle various types of user-facing content—whether
13832
+ * it's text from language models, images for analysis, or embedded resources for context.
13833
+ *
13834
+ * Content blocks appear in:
13835
+ * - User prompts sent via `session/prompt`
13836
+ * - Language model output streamed through `session/update` notifications
13837
+ * - Progress updates and results from tool calls
13838
+ *
13839
+ * This structure is compatible with the Model Context Protocol (MCP), enabling
13840
+ * agents to seamlessly forward content from MCP tool outputs without transformation.
13841
+ *
13842
+ * See protocol docs: [Content](https://agentclientprotocol.com/protocol/content)
13843
+ */
13844
+ const zContentBlock = union([
13845
+ zTextContent.and(object({ type: literal("text") })),
13846
+ zImageContent.and(object({ type: literal("image") })),
13847
+ zAudioContent.and(object({ type: literal("audio") })),
13848
+ zResourceLink.and(object({ type: literal("resource_link") })),
13849
+ zEmbeddedResource.and(object({ type: literal("resource") }))
13850
+ ]);
13851
+ /**
13852
+ * Standard content block (text, images, resources).
13853
+ */
13854
+ const zContent = object({
13855
+ content: zContentBlock,
13856
+ _meta: record(string(), unknown()).nullish()
13857
+ });
13858
+ /**
13859
+ * A diff representing file modifications.
13860
+ *
13861
+ * Shows changes to files in a format suitable for display in the client UI.
13862
+ *
13863
+ * See protocol docs: [Content](https://agentclientprotocol.com/protocol/tool-calls#content)
13864
+ */
13865
+ const zDiff = object({
13866
+ path: string(),
13867
+ oldText: string().nullish(),
13868
+ newText: string(),
13869
+ _meta: record(string(), unknown()).nullish()
13870
+ });
13871
+ /**
13872
+ * Typed identifier used for terminal values on the wire.
13873
+ */
13874
+ const zTerminalId = string();
13875
+ /**
13876
+ * Embed a terminal created with `terminal/create` by its id.
13877
+ *
13878
+ * The terminal must be added before calling `terminal/release`.
13879
+ *
13880
+ * See protocol docs: [Terminal](https://agentclientprotocol.com/protocol/terminals)
13881
+ */
13882
+ const zTerminal = object({
13883
+ terminalId: zTerminalId,
13884
+ _meta: record(string(), unknown()).nullish()
13885
+ });
13886
+ /**
13887
+ * Content produced by a tool call.
13888
+ *
13889
+ * Tool calls can produce different types of content including
13890
+ * standard content blocks (text, images) or file diffs.
13891
+ *
13892
+ * See protocol docs: [Content](https://agentclientprotocol.com/protocol/tool-calls#content)
13893
+ */
13894
+ const zToolCallContent = union([
13895
+ zContent.and(object({ type: literal("content") })),
13896
+ zDiff.and(object({ type: literal("diff") })),
13897
+ zTerminal.and(object({ type: literal("terminal") }))
13898
+ ]);
13899
+ /**
13900
+ * A file location being accessed or modified by a tool.
13901
+ *
13902
+ * Enables clients to implement "follow-along" features that track
13903
+ * which files the agent is working with in real-time.
13904
+ *
13905
+ * See protocol docs: [Following the Agent](https://agentclientprotocol.com/protocol/tool-calls#following-the-agent)
13906
+ */
13907
+ const zToolCallLocation = object({
13908
+ path: string(),
13909
+ line: int().gte(0).max(4294967295, { error: "Invalid value: Expected uint32 to be <= 4294967295" }).nullish(),
13910
+ _meta: record(string(), unknown()).nullish()
13911
+ });
13912
+ /**
13913
+ * An update to an existing tool call.
13914
+ *
13915
+ * Used to report progress and results as tools execute. All fields except
13916
+ * the tool call ID are optional - only changed fields need to be included.
13917
+ *
13918
+ * See protocol docs: [Updating](https://agentclientprotocol.com/protocol/tool-calls#updating)
13919
+ */
13920
+ const zToolCallUpdate = object({
13921
+ toolCallId: zToolCallId,
13922
+ kind: defaultOnError(zToolKind.nullish(), () => void 0),
13923
+ status: defaultOnError(zToolCallStatus.nullish(), () => void 0),
13924
+ title: string().nullish(),
13925
+ content: defaultOnError(vecSkipError(zToolCallContent).nullish(), () => void 0),
13926
+ locations: defaultOnError(vecSkipError(zToolCallLocation).nullish(), () => void 0),
13927
+ rawInput: unknown().optional(),
13928
+ rawOutput: unknown().optional(),
13929
+ _meta: record(string(), unknown()).nullish()
13930
+ });
13931
+ /**
13932
+ * Unique identifier for a permission option.
13933
+ */
13934
+ const zPermissionOptionId = string();
13935
+ /**
13936
+ * The type of permission option being presented to the user.
13937
+ *
13938
+ * Helps clients choose appropriate icons and UI treatment.
13939
+ */
13940
+ const zPermissionOptionKind = union([
13941
+ literal("allow_once"),
13942
+ literal("allow_always"),
13943
+ literal("reject_once"),
13944
+ literal("reject_always")
13945
+ ]);
13946
+ /**
13947
+ * Request for user permission to execute a tool call.
13948
+ *
13949
+ * Sent when the agent needs authorization before performing a sensitive operation.
13950
+ *
13951
+ * See protocol docs: [Requesting Permission](https://agentclientprotocol.com/protocol/tool-calls#requesting-permission)
13952
+ */
13953
+ const zRequestPermissionRequest = object({
13954
+ sessionId: zSessionId,
13955
+ toolCall: zToolCallUpdate,
13956
+ options: array(object({
13957
+ optionId: zPermissionOptionId,
13958
+ name: string(),
13959
+ kind: zPermissionOptionKind,
13960
+ _meta: record(string(), unknown()).nullish()
13961
+ })),
13962
+ _meta: record(string(), unknown()).nullish()
13963
+ });
13964
+ /**
13965
+ * An environment variable to set when launching an MCP server.
13966
+ */
13967
+ const zEnvVariable = object({
13968
+ name: string(),
13969
+ value: string(),
13970
+ _meta: record(string(), unknown()).nullish()
13971
+ });
13972
+ /**
13973
+ * Request to create a new terminal and execute a command.
13974
+ */
13975
+ const zCreateTerminalRequest = object({
13976
+ sessionId: zSessionId,
13977
+ command: string(),
13978
+ args: array(string()).optional(),
13222
13979
  env: array(zEnvVariable).optional(),
13223
13980
  cwd: string().nullish(),
13224
13981
  outputByteLimit: number().nullish(),
@@ -16860,1123 +17617,398 @@ var ClientApp = class {
16860
17617
  this[runClientConnectHandlers](state.connection);
16861
17618
  return state;
16862
17619
  }
16863
- const [thisStream, peerStream] = memoryStreamPair();
16864
- const peerRawConnection = target[appBuilder]().connect(peerStream);
16865
- const peerConnection = agentConnection(peerRawConnection);
16866
- const state = this.openStreamConnection(thisStream);
16867
- state.rawConnection.closed.then(() => peerConnection.close());
16868
- peerRawConnection.closed.then(() => state.connection.close());
16869
- try {
16870
- target[runAgentConnectHandlers](peerConnection);
16871
- this[runClientConnectHandlers](state.connection);
16872
- } catch (error) {
16873
- peerConnection.close(error);
16874
- state.connection.close(error);
16875
- throw error;
16876
- }
16877
- return state;
16878
- }
16879
- openStreamConnection(stream) {
16880
- const rawConnection = this.builder.connect(stream);
16881
- return {
16882
- rawConnection,
16883
- connection: clientConnection(rawConnection, this.connectHandlers)
16884
- };
16885
- }
16886
- };
16887
- AGENT_METHODS.initialize, AGENT_METHODS.authenticate, AGENT_METHODS.providers_list, AGENT_METHODS.providers_set, AGENT_METHODS.providers_disable, AGENT_METHODS.session_new, AGENT_METHODS.session_load, AGENT_METHODS.session_set_mode, AGENT_METHODS.session_set_config_option, AGENT_METHODS.session_prompt, AGENT_METHODS.session_list, AGENT_METHODS.session_delete, AGENT_METHODS.session_fork, AGENT_METHODS.session_resume, AGENT_METHODS.session_close, AGENT_METHODS.logout, AGENT_METHODS.nes_start, AGENT_METHODS.nes_suggest, AGENT_METHODS.nes_close;
16888
- AGENT_METHODS.session_cancel, AGENT_METHODS.nes_accept, AGENT_METHODS.nes_reject, AGENT_METHODS.document_did_open, AGENT_METHODS.document_did_change, AGENT_METHODS.document_did_close, AGENT_METHODS.document_did_save, AGENT_METHODS.document_did_focus;
16889
- CLIENT_METHODS.session_request_permission, CLIENT_METHODS.fs_write_text_file, CLIENT_METHODS.fs_read_text_file, CLIENT_METHODS.terminal_create, CLIENT_METHODS.terminal_output, CLIENT_METHODS.terminal_release, CLIENT_METHODS.terminal_wait_for_exit, CLIENT_METHODS.terminal_kill, CLIENT_METHODS.elicitation_create;
16890
- CLIENT_METHODS.session_update, CLIENT_METHODS.elicitation_complete;
16891
- //#endregion
16892
- //#region src/runtimes/probe.ts
16893
- /**
16894
- * Host-availability probing for runtimes.
16895
- *
16896
- * `resolveRuntimes()` produces a runtime for every ACP-registry entry that has a
16897
- * distribution for this platform — but that says nothing about whether the tool
16898
- * is actually runnable *here*. This module answers that question so the daemon
16899
- * only advertises genuinely-installed runtimes in `RegisterReq.capabilities`.
16900
- *
16901
- * Two layers:
16902
- * 1. A generic launcher check — is the runtime's `command` resolvable? For a
16903
- * runtime distributed as a real binary (`command` is the tool's own
16904
- * executable) this is a meaningful signal: the binary on `$PATH` means it's
16905
- * installed.
16906
- * 2. For runtimes distributed via a package launcher (`npx`/`uvx`), (1) is
16907
- * meaningless — the launcher fetches on demand, so "npx exists" is true on
16908
- * virtually every dev host and tells us nothing about the wrapped agent. For
16909
- * those we REQUIRE a per-runtime custom probe (`CUSTOM_PROBES`) that looks for
16910
- * the wrapped CLI's own config/state dir, which appears once the user has
16911
- * installed and run/initialized it. A launcher-distributed runtime with no
16912
- * probe is treated as NOT installed — otherwise every npx/uvx agent in the
16913
- * ACP registry would be falsely advertised just because `npx` is on `$PATH`.
16914
- *
16915
- * The custom probes test for the config *directory* (the "installed & initialized
16916
- * on this host" signal). A stricter "is logged in" check would look for the auth
16917
- * file instead (e.g. `~/.codex/auth.json`), but that yields false negatives for
16918
- * users who authenticate via an API-key env var, so directory presence is the
16919
- * pragmatic signal. Paths verified against each tool's docs (June 2026).
16920
- */
16921
- const isWin = process.platform === "win32";
16922
- function isExecutableFile(p) {
16923
- try {
16924
- accessSync(p, isWin ? constants.F_OK : constants.X_OK);
16925
- return true;
16926
- } catch {
16927
- return false;
16928
- }
16929
- }
16930
- /**
16931
- * Resolve a launcher command the way a shell would. A command containing a path
16932
- * separator is treated as a literal path; a bare name is searched across `$PATH`
16933
- * (trying `$PATHEXT` extensions on Windows).
16934
- */
16935
- function isCommandAvailable(command, env = process.env) {
16936
- return resolveCommandPath(command, env) !== void 0;
16937
- }
16938
- /**
16939
- * Resolve a launcher/command to its absolute path the way a shell would — a path
16940
- * containing a separator is checked literally; a bare name is searched across
16941
- * `$PATH` (trying `$PATHEXT` extensions on Windows). Returns undefined if not found.
16942
- */
16943
- function resolveCommandPath(command, env = process.env) {
16944
- const exts = isWin ? ["", ...(env.PATHEXT ?? ".EXE;.CMD;.BAT;.COM").split(";")] : [""];
16945
- if (command.includes("/") || isWin && command.includes("\\")) return exts.map((ext) => command + ext).find(isExecutableFile);
16946
- const dirs = (env.PATH ?? "").split(delimiter).filter(Boolean);
16947
- for (const dir of dirs) for (const ext of exts) {
16948
- const p = join(dir, command) + ext;
16949
- if (isExecutableFile(p)) return p;
16950
- }
16951
- }
16952
- function home(env) {
16953
- return (isWin ? env.USERPROFILE : env.HOME) || homedir();
16954
- }
16955
- function xdgConfigHome(env) {
16956
- return env.XDG_CONFIG_HOME || join(home(env), ".config");
16957
- }
16958
- function xdgDataHome(env) {
16959
- return env.XDG_DATA_HOME || join(home(env), ".local", "share");
16960
- }
16961
- function pathExists(p) {
16962
- return !!p && existsSync(p);
16963
- }
16964
- /** True if any candidate path exists (undefined candidates are skipped). */
16965
- function anyExists(...candidates) {
16966
- return candidates.some(pathExists);
16967
- }
16968
- /**
16969
- * Custom probes keyed by ACP-registry runtime id. Add an entry here whenever the
16970
- * launcher-on-PATH check is too weak to mean "actually installed & set up".
16971
- * Each tests for the tool's home/XDG config dir (or, where the dir is ambiguous,
16972
- * a tool-specific file), honoring the documented env overrides.
16973
- */
16974
- const CUSTOM_PROBES = {
16975
- "claude-acp": (env) => anyExists(join(home(env), ".claude"), join(home(env), ".claude.json")),
16976
- "codex-acp": (env) => anyExists(env.CODEX_HOME, join(home(env), ".codex")),
16977
- gemini: (env) => pathExists(join(home(env), ".gemini")),
16978
- "qwen-code": (env) => pathExists(join(home(env), ".qwen")),
16979
- "github-copilot-cli": (env) => anyExists(env.COPILOT_HOME, join(home(env), ".copilot")),
16980
- cursor: (env) => anyExists(env.CURSOR_CONFIG_DIR ? join(env.CURSOR_CONFIG_DIR, "cli-config.json") : void 0, join(home(env), ".cursor", "cli-config.json")),
16981
- opencode: (env) => anyExists(join(xdgConfigHome(env), "opencode"), join(xdgDataHome(env), "opencode", "auth.json")),
16982
- goose: (env) => pathExists(join(xdgConfigHome(env), "goose")),
16983
- "amp-acp": (env) => anyExists(env.AMP_SETTINGS_FILE, join(xdgConfigHome(env), "amp")),
16984
- auggie: (env) => pathExists(join(home(env), ".augment")),
16985
- cline: (env) => anyExists(env.CLINE_DATA_DIR, join(home(env), ".cline")),
16986
- "grok-build": (env) => pathExists(join(home(env), ".grok")),
16987
- kimi: (env) => anyExists(env.KIMI_CODE_HOME, join(home(env), ".kimi"), join(home(env), ".kimi-code")),
16988
- "factory-droid": (env) => pathExists(join(home(env), ".factory")),
16989
- devin: (env) => anyExists(join(xdgConfigHome(env), "devin"), join(xdgDataHome(env), "devin"))
16990
- };
16991
- /**
16992
- * Package launchers that fetch-and-run on demand. When a runtime is launched via
16993
- * one of these, the launcher being on `$PATH` says nothing about whether the
16994
- * wrapped agent is installed, so such runtimes require a custom probe to count.
16995
- */
16996
- const PACKAGE_LAUNCHERS = /* @__PURE__ */ new Set(["npx", "uvx"]);
16997
- /** Is this runtime actually usable on this host? */
16998
- function isRuntimeAvailable(id, rt, env = process.env) {
16999
- if (!isCommandAvailable(rt.command, env)) return false;
17000
- const custom = CUSTOM_PROBES[id];
17001
- if (custom) return custom(env);
17002
- return !PACKAGE_LAUNCHERS.has(rt.command);
17003
- }
17004
- /**
17005
- * Filter a runtime map down to those installed on this host — what the daemon
17006
- * should report to the Control Plane and is able to launch.
17007
- */
17008
- function installedRuntimes(runtimes, env = process.env) {
17009
- const out = {};
17010
- for (const [id, rt] of Object.entries(runtimes)) if (isRuntimeAvailable(id, rt, env)) out[id] = rt;
17011
- return out;
17012
- }
17013
- //#endregion
17014
- //#region src/acp/acp-host.ts
17015
- const PROTOCOL_VERSION = 1;
17016
- /**
17017
- * Extract the model selector from a session's `configOptions`. ACP models are an
17018
- * (experimental) session config option tagged `category: "model"`, `type:
17019
- * "select"`; the selectable values (optionally grouped) are the model list.
17020
- * Returns null when the agent advertises no model selector.
17021
- */
17022
- function modelOptionsFrom(configOptions) {
17023
- const model = configOptions?.find((o) => o.category === "model" && o.type === "select");
17024
- if (!model || model.type !== "select") return null;
17025
- const models = model.options.flatMap((o) => "group" in o ? o.options : [o]).map((o) => o.value);
17026
- return {
17027
- current: model.currentValue,
17028
- models
17029
- };
17030
- }
17031
- var AcpHost = class {
17032
- runtime;
17033
- opts;
17034
- child;
17035
- conn;
17036
- live = /* @__PURE__ */ new Set();
17037
- loadingSessions = /* @__PURE__ */ new Set();
17038
- canLoad = false;
17039
- promptCaps = {};
17040
- lastModelOptions = null;
17041
- constructor(runtime, opts) {
17042
- this.runtime = runtime;
17043
- this.opts = opts;
17044
- }
17045
- /** A Claude Code runtime (its command/args reference `claude`) — these embed the
17046
- * @anthropic-ai/claude-agent-sdk, which needs a Claude Code executable. */
17047
- isClaudeRuntime() {
17048
- return [this.runtime.command, ...this.runtime.args].join(" ").toLowerCase().includes("claude");
17049
- }
17050
- async start() {
17051
- if (this.child) throw new Error("AcpHost: already started");
17052
- const env = {
17053
- ...process.env,
17054
- ...Object.fromEntries(this.runtime.env.map((e) => [e.name, e.value])),
17055
- ...this.opts.env ?? {}
17056
- };
17057
- if (!env.CLAUDE_CODE_EXECUTABLE && this.isClaudeRuntime()) {
17058
- const claude = resolveCommandPath("claude", env);
17059
- if (claude) {
17060
- env.CLAUDE_CODE_EXECUTABLE = claude;
17061
- this.opts.log?.info(`acp: CLAUDE_CODE_EXECUTABLE not set — using claude on PATH (${claude})`);
17062
- }
17063
- }
17064
- const child = spawn(this.runtime.command, this.runtime.args, {
17065
- stdio: [
17066
- "pipe",
17067
- "pipe",
17068
- "inherit"
17069
- ],
17070
- env
17071
- });
17072
- this.child = child;
17073
- if (!child.stdin || !child.stdout) throw new Error("AcpHost: subprocess stdin/stdout are not piped");
17074
- const stream = ndJsonStream(Writable.toWeb(child.stdin), Readable.toWeb(child.stdout));
17075
- const self = this;
17076
- this.conn = client({ name: "agentconnect" }).onNotification(methods.client.session.update, (ctx) => {
17077
- if (self.loadingSessions.has(ctx.params.sessionId)) return;
17078
- self.opts.onUpdate(ctx.params.sessionId, ctx.params.update);
17079
- }).onRequest(methods.client.session.requestPermission, (ctx) => {
17080
- const optionId = (ctx.params.options.find((o) => o.kind === "allow_once" || o.kind === "allow_always") ?? ctx.params.options[0])?.optionId;
17081
- return { outcome: optionId ? {
17082
- outcome: "selected",
17083
- optionId
17084
- } : { outcome: "cancelled" } };
17085
- }).connect(stream);
17086
- const init = await this.conn.agent.request(methods.agent.initialize, {
17087
- protocolVersion: PROTOCOL_VERSION,
17088
- clientCapabilities: { fs: {
17089
- readTextFile: false,
17090
- writeTextFile: false
17091
- } }
17092
- });
17093
- this.canLoad = init.agentCapabilities?.loadSession ?? false;
17094
- this.promptCaps = init.agentCapabilities?.promptCapabilities ?? {};
17095
- this.opts.log?.debug(`acp: agent initialized (loadSession=${this.canLoad}, prompt caps: image=${!!this.promptCaps.image} audio=${!!this.promptCaps.audio} embeddedContext=${!!this.promptCaps.embeddedContext})`);
17096
- }
17097
- /** Whether the agent advertised support for a gated prompt content-block kind.
17098
- * `text` and `resource_link` are baseline and always return true. */
17099
- promptSupports(kind) {
17100
- return Boolean(this.promptCaps[kind]);
17101
- }
17102
- async newSession(cwd, mcpServers = []) {
17103
- const res = await this.conn.agent.request(methods.agent.session.new, {
17104
- cwd,
17105
- mcpServers
17106
- });
17107
- this.lastModelOptions = modelOptionsFrom(res.configOptions);
17108
- this.live.add(res.sessionId);
17109
- return res.sessionId;
17110
- }
17111
- /** The model selector advertised by the most recent session/new, or null if the
17112
- * agent exposed none. Used by the runtime probe to report available models. */
17113
- modelOptions() {
17114
- return this.lastModelOptions;
17115
- }
17116
- /** True iff THIS agent process created or loaded `sessionId` in its current lifetime. */
17117
- hasSession(sessionId) {
17118
- return this.live.has(sessionId);
17119
- }
17120
- /** Whether the agent advertised the `loadSession` capability (session/load is usable). */
17121
- loadSupported() {
17122
- return this.canLoad;
17123
- }
17124
- /** Resume a previously-created session by id (ACP `session/load`). The agent
17125
- * restores its own history server-side; its replayed session/update stream is
17126
- * suppressed so it isn't re-posted. Throws if the agent can't load the id
17127
- * (caller falls back to newSession). */
17128
- async loadSession(sessionId, cwd) {
17129
- this.loadingSessions.add(sessionId);
17130
- try {
17131
- await this.conn.agent.request(methods.agent.session.load, {
17132
- sessionId,
17133
- cwd,
17134
- mcpServers: []
17135
- });
17136
- this.live.add(sessionId);
17137
- this.opts.log?.info(`acp: resumed session ${sessionId} via session/load`);
17138
- } catch (err) {
17139
- this.opts.log?.debug(`acp: session/load failed for ${sessionId} (${err.message}) — will recreate`);
17140
- throw err;
17141
- } finally {
17142
- this.loadingSessions.delete(sessionId);
17143
- }
17144
- }
17145
- /** Drive one turn to completion. Returns the stop reason plus the agent's token
17146
- * `usage` when the runtime reports it (ACP's experimental per-turn usage, which
17147
- * carries session-cumulative counts) — `undefined` when it doesn't. */
17148
- async prompt(sessionId, blocks) {
17149
- const res = await this.conn.agent.request(methods.agent.session.prompt, {
17150
- sessionId,
17151
- prompt: blocks
17152
- });
17153
- return {
17154
- stopReason: res.stopReason,
17155
- usage: res.usage ?? void 0
17156
- };
17157
- }
17158
- async cancel(sessionId) {
17159
- await this.conn.agent.notify(methods.agent.session.cancel, { sessionId });
17160
- }
17161
- /** Stop the adapter child: SIGTERM, then escalate to SIGKILL if it hasn't exited
17162
- * within `deadlineMs` (a buggy/hung agent must never block daemon shutdown or an
17163
- * idle reap). Idempotent — the child handle is cleared up front so a concurrent
17164
- * stop (drain + reconcile racing) is a no-op rather than a double-kill. */
17165
- async stop(deadlineMs = 5e3) {
17166
- const child = this.child;
17167
- if (!child) return;
17168
- this.child = void 0;
17169
- child.kill("SIGTERM");
17170
- await new Promise((resolve) => {
17171
- let settled = false;
17172
- const done = () => {
17173
- if (settled) return;
17174
- settled = true;
17175
- clearTimeout(timer);
17176
- resolve();
17177
- };
17178
- const timer = setTimeout(() => {
17179
- this.opts.log?.warn(`acp: child ignored SIGTERM after ${deadlineMs}ms — sending SIGKILL`);
17180
- child.kill("SIGKILL");
17181
- }, deadlineMs);
17182
- child.once("exit", done);
17183
- });
17184
- }
17185
- };
17186
- //#endregion
17187
- //#region src/cli/chat.ts
17188
- function renderUpdate(out) {
17189
- return (_sessionId, update) => {
17190
- if (update.sessionUpdate === "agent_message_chunk" && update.content.type === "text") out.write(update.content.text);
17191
- else if (update.sessionUpdate === "tool_call") {
17192
- const title = "title" in update && update.title || update.toolCallId || "tool";
17193
- out.write(`\n[tool] ${title}\n`);
17194
- }
17195
- };
17196
- }
17197
- async function runChat(opts) {
17198
- const out = opts.out ?? process.stdout;
17199
- const root = resolveRoot(opts.root);
17200
- const cfg = loadConfig({
17201
- root: opts.root,
17202
- configPath: opts.configPath,
17203
- optional: true,
17204
- overrides: { agentsDir: opts.agentsDir }
17205
- });
17206
- const agent = selectAgent(cfg.agentsDir, opts.agentName);
17207
- const runtimes = await resolveRuntimes(cfg, root, {
17208
- neededRuntimes: [agent.runtime],
17209
- mode: "cache-first"
17210
- });
17211
- const runtime = runtimes[agent.runtime];
17212
- if (!runtime) {
17213
- const available = Object.keys(runtimes).sort().join(", ") || "(none)";
17214
- throw new Error(`runtime "${agent.runtime}" not found. Available: ${available}`);
17215
- }
17216
- const host = new AcpHost(runtime, {
17217
- onUpdate: renderUpdate(out),
17218
- env: agentChildEnv(agent)
17219
- });
17220
- await host.start();
17221
- try {
17222
- const cwd = await prepareWorkspace(agent);
17223
- const sessionId = await host.newSession(cwd);
17224
- const send = async (text) => {
17225
- const blocks = [{
17226
- type: "text",
17227
- text
17228
- }];
17229
- await host.prompt(sessionId, blocks);
17230
- out.write("\n");
17231
- };
17232
- if (typeof opts.message === "string" && opts.message.length > 0) {
17233
- await send(opts.message);
17234
- return;
17235
- }
17236
- const rl = createInterface({ input: opts.input ?? process.stdin });
17237
- out.write(`Chatting with ${agent.name} (${agent.id}). Type .exit to quit.\n> `);
17238
- try {
17239
- for await (const line of rl) {
17240
- const text = line.trim();
17241
- if (text === ".exit" || text === ".quit") break;
17242
- if (text.length > 0) await send(text);
17243
- out.write("> ");
17244
- }
17245
- } finally {
17246
- rl.close();
17247
- }
17248
- } finally {
17249
- await host.stop();
17250
- }
17251
- }
17252
- //#endregion
17253
- //#region src/version.ts
17254
- const DAEMON_VERSION = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8")).version;
17255
- //#endregion
17256
- //#region ../protocol/dist/envelope.js
17257
- /**
17258
- * The protocol envelope — every frame, both directions, is wrapped in this.
17259
- * Mirrors daemon-cp-ws-protocol.md §1.1.
17260
- *
17261
- * `payload` is left as `unknown` here and validated by the per-type schema in
17262
- * `frames/*` via `FRAME_SCHEMAS[type]` (see `frame.ts`). This two-step parse is
17263
- * what lets the codec answer an unknown `type` with `UNKNOWN_FRAME` (a REP)
17264
- * instead of a hard close — forward-compat, protocol §1.
17265
- */
17266
- const Envelope = object({
17267
- v: literal(1),
17268
- id: string().uuid(),
17269
- ts: string().datetime(),
17270
- type: string(),
17271
- corr: string().uuid().optional(),
17272
- payload: unknown()
17273
- });
17274
- object({
17275
- epoch: number().int(),
17276
- seq: number().int().optional(),
17277
- agentId: string().uuid().optional(),
17278
- launchId: string().uuid().optional()
17279
- });
17280
- /** The all-zero UUID used when a frame is malformed past the point of reading `id`. */
17281
- const NIL_UUID = "00000000-0000-0000-0000-000000000000";
17282
- //#endregion
17283
- //#region ../protocol/dist/frames/auth.js
17284
- /**
17285
- * Auth & identity — protocol §3.1 / §3.2.
17286
- *
17287
- * `auth` is the first frame after the socket opens. `auth/ok` carries the
17288
- * minted `sessionEpoch` (the global fencing token), heartbeat cadence, and the
17289
- * resume verdict.
17290
- */
17291
- const AuthReq = object({
17292
- apiKey: string(),
17293
- daemonId: string().uuid().optional(),
17294
- machineId: string().uuid().optional(),
17295
- attestation: string().optional(),
17296
- agentVersion: string(),
17297
- resume: object({
17298
- lastEpoch: number().int(),
17299
- lastRecvSeq: record(string(), number())
17300
- }).optional()
17301
- });
17302
- const AuthOk = object({
17303
- daemonId: string().uuid(),
17304
- sessionEpoch: number().int(),
17305
- heartbeatSec: number().int(),
17306
- serverTime: string().datetime(),
17307
- resume: object({
17308
- accepted: boolean(),
17309
- redeliverFromSeq: record(string(), number()).optional()
17310
- }).optional()
17311
- });
17312
- //#endregion
17313
- //#region ../protocol/dist/frames/route.js
17314
- /**
17315
- * Routing & orchestration (C→D control) — protocol §5.
17316
- *
17317
- * `SessionKey` is the canonical session primitive shared across route/*,
17318
- * agent/*, and event/session. Its canonical string form is
17319
- * `${platform}:${channel}:${thread ?? "-"}`.
17320
- */
17321
- const Platform = _enum(["slack", "telegram"]);
17322
- const SessionKey = object({
17323
- platform: Platform,
17324
- channel: string(),
17325
- thread: string().optional()
17326
- });
17327
- /** Trigger-matching rule for a binding (protocol §5.1). */
17328
- const BindRule = object({ match: discriminatedUnion("kind", [
17329
- object({ kind: literal("mention") }),
17330
- object({ kind: literal("dm") }),
17331
- object({
17332
- kind: literal("keyword"),
17333
- value: string()
17334
- }),
17335
- object({ kind: literal("auto") })
17336
- ]) });
17337
- const RouteAssign = object({
17338
- sessionKey: SessionKey,
17339
- agentId: string().uuid(),
17340
- workspaceId: string().uuid(),
17341
- bindRules: array(BindRule).default([])
17342
- });
17343
- const RouteAssignAck = object({
17344
- ok: boolean(),
17345
- sessionKey: SessionKey,
17346
- reason: string().optional()
17347
- });
17348
- const RouteUpdate = object({
17349
- routingEpoch: number().int(),
17350
- rules: array(object({
17351
- match: unknown(),
17352
- agentId: string().uuid()
17353
- }))
17354
- });
17355
- /** Graceful scale-down / rebalance — protocol §5.3. */
17356
- const Drain = object({
17357
- scope: union([
17358
- object({
17359
- kind: literal("agent"),
17360
- agentId: string().uuid()
17361
- }),
17362
- object({ kind: literal("daemon") }),
17363
- object({
17364
- kind: literal("session"),
17365
- sessionKey: SessionKey
17366
- })
17367
- ]),
17368
- deadline: string().datetime()
17369
- });
17370
- const DrainProgress = object({
17371
- remaining: number().int(),
17372
- drained: array(SessionKey)
17373
- });
17374
- const DrainDone = object({ released: array(SessionKey) });
17375
- //#endregion
17376
- //#region ../protocol/dist/frames/cron.js
17377
- /**
17378
- * Cron sinks to the daemon (D5) — protocol §5.4.
17379
- *
17380
- * The CP owns the definition; the daemon owns firing + last-run persistence so
17381
- * crons fire even when the CP is down.
17382
- */
17383
- const CronUpsert = object({
17384
- cronId: string().uuid(),
17385
- schedule: string(),
17386
- target: object({ channel: string() }),
17387
- trigger: string(),
17388
- enabled: boolean().default(true)
17389
- });
17390
- const CronRemove = object({ cronId: string().uuid() });
17391
- //#endregion
17392
- //#region ../protocol/dist/frames/secrets.js
17393
- /**
17394
- * Secrets (C5 ↔ D10) — protocol §6.
17395
- *
17396
- * Lease-based, no plaintext on the wire or in PG. Every frame carries a
17397
- * REFERENCE to a Vault/KMS path, never the secret material itself.
17398
- */
17399
- const SecretsRequest = object({ scope: object({
17400
- platform: Platform,
17401
- workspaceId: string().uuid()
17402
- }) });
17403
- const SecretsGrant = object({
17404
- leaseId: string().uuid(),
17405
- scope: object({
17406
- platform: string(),
17407
- workspaceId: string().uuid()
17408
- }),
17409
- ref: string(),
17410
- ttl: number().int(),
17411
- renewBeforeSec: number().int()
17412
- });
17413
- const SecretsRenew = object({ leaseId: string().uuid() });
17414
- const SecretsRevoke = object({
17415
- leaseId: string().uuid(),
17416
- reason: string()
17417
- });
17418
- /** 🅼 Direct-to-store upload/download grant — protocol §3.2 / frame #25. */
17419
- const ScopeAttestation = object({
17420
- machineId: string().uuid(),
17421
- scope: _enum([
17422
- "attachment.put",
17423
- "attachment.get",
17424
- "facts.put"
17425
- ]),
17426
- resourceRef: string(),
17427
- jws: string(),
17428
- exp: string().datetime()
17429
- });
17430
- //#endregion
17431
- //#region ../protocol/dist/frames/agent.js
17432
- /**
17433
- * Agent lifecycle + delivery (protocol §4.3, §4.4, §7.4, §8).
17434
- *
17435
- * Body-locality invariant (protocol §12): no frame here carries
17436
- * `NormalizedMessage.text`. `agent/prompt` ships a `NormalizedMessageRef` — a
17437
- * reference the daemon resolves against its local body store.
17438
- */
17439
- /**
17440
- * A REFERENCE/digest of a normalized message, NOT the body (protocol §4.3).
17441
- * Enough for D4/D6 to fetch the local body and prompt.
17442
- */
17443
- const NormalizedMessageRef = object({
17444
- sessionKey: SessionKey,
17445
- platformMsgId: string(),
17446
- seenUpToSeq: number().int()
17447
- });
17448
- /**
17449
- * Where the agent runs. Two modes; the **path is always daemon-generated** —
17450
- * never specified by the caller (UX picks the mode, the machine owns the dir).
17451
- *
17452
- * - `scratch`: a fresh empty working dir on the machine, no repo.
17453
- * - `github`: the daemon clones `gitRepo` @ `branch` and runs the agent in
17454
- * `agentDir` (a subdir of the repo, repo-root if omitted). **Multiple agents
17455
- * may share one repo** — they differ by `agentDir`, so the repo is not an
17456
- * owned entity, just shared config on each agent.
17457
- */
17458
- const AgentWorkspace = discriminatedUnion("mode", [object({ mode: literal("scratch") }), object({
17459
- mode: literal("github"),
17460
- gitRepo: string(),
17461
- branch: string().default("main"),
17462
- agentDir: string().optional()
17463
- })]);
17464
- /**
17465
- * The editable agent definition the CP owns and the daemon needs to run it:
17466
- * prompt + runtime selection. Raft ships this in the launch config and the
17467
- * daemon synthesizes the system prompt locally; `description` IS the prompt.
17468
- */
17469
- const AgentSpec = object({
17470
- name: string(),
17471
- description: string().optional(),
17472
- runtime: string().optional(),
17473
- model: string().optional(),
17474
- reasoningEffort: string().optional(),
17475
- executionMode: string().optional(),
17476
- workspace: AgentWorkspace.optional(),
17477
- env: record(string(), string()).optional()
17478
- });
17479
- const AgentLaunch = object({
17480
- agentId: string().uuid(),
17481
- runtime: string(),
17482
- workspaceId: string().uuid(),
17483
- capabilities: array(string()),
17484
- spec: AgentSpec,
17485
- mode: _enum(["long_lived", "per_turn"]).default("long_lived")
17486
- });
17487
- /**
17488
- * Live agent CRUD (C→D): the console edited an agent's spec; push it so a
17489
- * running daemon reloads without waiting for the next launch. `agent/remove`
17490
- * tears the agent down. Deleting an agent never relaunches it.
17491
- */
17492
- const AgentUpsert = object({
17493
- agentId: string().uuid(),
17494
- spec: AgentSpec
17495
- });
17496
- const AgentRemove = object({ agentId: string().uuid() });
17497
- const AgentLaunched = object({
17498
- agentId: string().uuid(),
17499
- launchId: string().uuid(),
17500
- acpSessionId: string().optional(),
17501
- startedAt: string().datetime(),
17502
- runtime: string()
17503
- });
17504
- const AgentStop = object({
17505
- agentId: string().uuid(),
17506
- launchId: string().uuid(),
17507
- reason: string()
17508
- });
17509
- const AgentPrompt = object({
17510
- sessionKey: SessionKey,
17511
- agentId: string().uuid(),
17512
- content: NormalizedMessageRef,
17513
- seenUpToSeq: number().int()
17514
- });
17515
- const AgentPromptAck = object({
17516
- accepted: boolean(),
17517
- reason: _enum([
17518
- "queued",
17519
- "held",
17520
- "scope_denied",
17521
- "no_session",
17522
- "stale"
17523
- ]).optional(),
17524
- seq: number().int()
17525
- });
17526
- const AgentActivity = object({
17527
- agentId: string().uuid(),
17528
- launchId: string().uuid(),
17529
- state: _enum([
17530
- "thinking",
17531
- "tool_call",
17532
- "awaiting_permission",
17533
- "idle"
17534
- ]),
17535
- ts: string().datetime()
17536
- });
17537
- const AgentScopeDenied = object({
17538
- agentId: string().uuid(),
17539
- launchId: string().uuid(),
17540
- capability: string()
17541
- });
17542
- //#endregion
17543
- //#region ../protocol/dist/frames/integration.js
17544
- /**
17545
- * Platform integration distribution (C→D) — the Slack "install" flow.
17546
- *
17547
- * The Control Plane is the source of truth for platform integrations and pushes
17548
- * them to the daemon that owns the integration's agent (`integration/upsert`, and
17549
- * the reconcile snapshot `RegisterOk.integrations[]`). The daemon opens the Socket
17550
- * Mode connection from the delivered config (see slack/connection.ts).
17551
- *
17552
- * SECURITY: `integration/upsert` and `RegisterOk.integrations[]` carry PLAINTEXT
17553
- * platform tokens (botToken/appToken). These payloads MUST NEVER be logged — no
17554
- * body dump on decode error, no register/ok snapshot debug dump. The daemon
17555
- * persists them into the owning agent's local `agent.json` (same trust boundary
17556
- * as hand-authored agents, which already keep tokens there) so integrations
17557
- * survive a restart with the CP down.
17558
- *
17559
- * `signingSecret` is intentionally absent: Socket Mode authenticates with the
17560
- * app-level token, so the daemon never needs a signing secret.
17561
- */
17562
- /** Trigger match — mirrors the daemon BindRuleConfig.match (agents/agent-schema.ts). */
17563
- const BindMatch = discriminatedUnion("kind", [
17564
- object({ kind: literal("mention") }),
17565
- object({ kind: literal("dm") }),
17566
- object({
17567
- kind: literal("keyword"),
17568
- value: string()
17569
- }),
17570
- object({ kind: literal("auto") })
17571
- ]);
17572
- /** One channel/thread trigger binding — mirrors the daemon BindRuleConfig. */
17573
- const IntegrationBindRule = object({
17574
- channel: string().optional(),
17575
- thread: string().optional(),
17576
- match: BindMatch
17577
- });
17578
- /** The Slack config the daemon needs to open + route a socket (no signingSecret). */
17579
- const IntegrationSlackConfig = object({
17580
- botToken: string(),
17581
- appToken: string(),
17582
- allowedUserIds: array(string()).default([]),
17583
- notificationChannelId: string().optional(),
17584
- bindRules: array(IntegrationBindRule).default([])
17585
- });
17586
- /**
17587
- * One platform integration, owned by exactly one agent. Also the element type of
17588
- * `RegisterOk.integrations[]` (the per-daemon reconcile set).
17589
- */
17590
- const IntegrationSpec = object({
17591
- integrationId: string().uuid(),
17592
- agentId: string().uuid(),
17593
- platform: literal("slack"),
17594
- slack: IntegrationSlackConfig
17595
- });
17596
- /** C→D EVT — install/update an integration on the owning agent's daemon. */
17597
- const IntegrationUpsert = IntegrationSpec;
17598
- /** C→D EVT — remove an integration from the daemon. */
17599
- const IntegrationRemove = object({ integrationId: string().uuid() });
17600
- //#endregion
17601
- //#region ../protocol/dist/frames/register.js
17602
- /**
17603
- * Capability upload + the reconcile snapshot — protocol §3.3.
17604
- *
17605
- * `register/ok` is the authoritative source of truth: the daemon converges its
17606
- * local cache to it. CP wins all conflicts, so re-issuing the same snapshot is
17607
- * idempotent.
17608
- */
17609
- const RegisterReq = object({
17610
- host: string(),
17611
- capabilities: object({
17612
- platforms: array(Platform),
17613
- runtimes: array(string()),
17614
- acp: boolean(),
17615
- features: array(string()).default([])
17616
- }),
17617
- maxAgents: number().int(),
17618
- localState: object({
17619
- assignments: array(string()),
17620
- crons: array(string()),
17621
- leases: array(string())
17622
- })
17623
- });
17624
- const RegisterOk = object({
17625
- routingEpoch: number().int(),
17626
- assignments: array(RouteAssign),
17627
- agents: array(AgentSpec.extend({ agentId: string().uuid() })).default([]),
17628
- crons: array(CronUpsert),
17629
- integrations: array(IntegrationSpec).default([]),
17630
- leases: array(SecretsGrant),
17631
- drop: object({
17632
- assignments: array(string()),
17633
- crons: array(string())
17634
- })
17635
- });
17636
- //#endregion
17637
- //#region ../protocol/dist/frames/session.js
17638
- /**
17639
- * Session read-back (C→D REQ → REP) — the console's on-demand pulls.
17640
- *
17641
- * The CP stores NO session data — neither the list nor the bodies. Sessions are
17642
- * created on the Slack→daemon path and live solely in the daemon's local store
17643
- * (body-locality, §1/§12). So both the session **list** and a session's chat
17644
- * **history** are pulled live from the owning daemon(s) and proxied to the
17645
- * console — never persisted on the CP, never on the orchestration hot path.
17646
- *
17647
- * - `session/list`: the daemon's live sessions (CP fans this out to all the org's
17648
- * online daemons and merges, for "all sessions across the workspace").
17649
- * - `session/history`: one cursor-paginated page of a session's transcript.
17650
- */
17651
- /**
17652
- * Per-session token accounting, metered by the daemon from the agent's ACP usage
17653
- * stream. Token counts are CUMULATIVE across the session's turns (summed from each
17654
- * turn's `PromptResponse.usage`); context/cost are the LATEST snapshot from the
17655
- * agent's `usage_update` stream. Every field is optional — a runtime that reports
17656
- * no usage yields an absent field, not a zero.
17657
- */
17658
- const SessionUsage = object({
17659
- totalTokens: number().int().optional(),
17660
- inputTokens: number().int().optional(),
17661
- outputTokens: number().int().optional(),
17662
- thoughtTokens: number().int().optional(),
17663
- cachedReadTokens: number().int().optional(),
17664
- cachedWriteTokens: number().int().optional(),
17665
- contextUsed: number().int().optional(),
17666
- contextSize: number().int().optional(),
17667
- costAmount: number().optional(),
17668
- costCurrency: string().optional()
17669
- });
17670
- /** One row in the session list (metadata + console metrics; NOT the transcript). */
17671
- const SessionListItem = object({
17672
- sessionId: string(),
17673
- sessionKey: SessionKey,
17674
- agentId: string().uuid(),
17675
- title: string().optional(),
17676
- status: string().optional(),
17677
- lastActivityAt: string().optional(),
17678
- usage: SessionUsage.optional(),
17679
- triggeredBy: string().optional()
17680
- });
17681
- /** C→D REQ: list the daemon's sessions (optionally just one agent's). */
17682
- const SessionListReq = object({ agentId: string().uuid().optional() });
17683
- /** D→C REP (corr = req id): the daemon's current sessions. */
17684
- const SessionListPage = object({ sessions: array(SessionListItem) });
17685
- /** One message in a session transcript page (a body — returned only for display). */
17686
- const SessionMessage = object({
17687
- seq: number().int(),
17688
- sender: string(),
17689
- ts: string(),
17690
- kind: string(),
17691
- text: string()
17692
- });
17693
- /** C→D REQ: fetch one page of a session's history from the owning daemon. */
17694
- const SessionHistoryReq = object({
17695
- sessionId: string(),
17696
- cursor: string().optional(),
17697
- limit: number().int().positive().max(200).default(50)
17698
- });
17699
- /** D→C REP (corr = the req id): a page of messages + the cursor for the next page. */
17700
- const SessionHistoryPage = object({
17701
- sessionId: string(),
17702
- messages: array(SessionMessage),
17703
- nextCursor: string().optional()
17704
- });
17705
- //#endregion
17706
- //#region ../protocol/dist/frame.js
17707
- /**
17708
- * The single source of truth for the wire: `type` string → payload zod schema.
17709
- *
17710
- * Mirrors the frame index in daemon-cp-ws-protocol.md §10 (the 29 numbered
17711
- * frames) plus the correlated REP types named in the "Reply" column
17712
- * (`route/assign/ack`, `drain/done`, and the generic `ack` replies) that also
17713
- * travel on the wire and must be decodable.
17714
- *
17715
- * `ws/codec.ts` validates every inbound `payload` against `FRAME_SCHEMAS[type]`;
17716
- * an unknown `type` → `ErrorFrame{code:"UNKNOWN_FRAME"}` (a REP, not a close).
17717
- */
17718
- const FRAME_SCHEMAS = {
17719
- auth: AuthReq,
17720
- "auth/ok": AuthOk,
17721
- register: RegisterReq,
17722
- "register/ok": RegisterOk,
17723
- heartbeat: object({
17724
- load: object({
17725
- cpu: number(),
17726
- mem: number(),
17727
- agents: number().int()
17728
- }),
17729
- health: _enum(["ok", "degraded"]),
17730
- activeSessions: number().int(),
17731
- degradedScopes: array(string()).default([])
17732
- }),
17733
- "agent/launch": AgentLaunch,
17734
- "agent/launched": AgentLaunched,
17735
- "agent/stop": AgentStop,
17736
- "agent/upsert": AgentUpsert,
17737
- "agent/remove": AgentRemove,
17738
- "agent/prompt": AgentPrompt,
17739
- "agent/prompt/ack": AgentPromptAck,
17740
- "agent/activity": AgentActivity,
17741
- "agent/scope-denied": AgentScopeDenied,
17742
- "route/assign": RouteAssign,
17743
- "route/assign/ack": RouteAssignAck,
17744
- "route/update": RouteUpdate,
17745
- "daemon/drain": Drain,
17746
- "drain/progress": DrainProgress,
17747
- "drain/done": DrainDone,
17748
- "cron/upsert": CronUpsert,
17749
- "cron/remove": CronRemove,
17750
- "integration/upsert": IntegrationUpsert,
17751
- "integration/remove": IntegrationRemove,
17752
- "secrets/request": SecretsRequest,
17753
- "secrets/grant": SecretsGrant,
17754
- "secrets/renew": SecretsRenew,
17755
- "secrets/revoke": SecretsRevoke,
17756
- "scope-attestation": ScopeAttestation,
17757
- "event/session": object({
17758
- sessionId: string().uuid(),
17759
- agentId: string().uuid(),
17760
- launchId: string().uuid(),
17761
- phase: _enum([
17762
- "start",
17763
- "plan",
17764
- "problem",
17765
- "end"
17766
- ]),
17767
- link: string().optional(),
17768
- summary: string().optional(),
17769
- ts: string().datetime()
17770
- }),
17771
- "usage/report": object({
17772
- sessionId: string(),
17773
- agentId: string().uuid(),
17774
- platform: string().optional(),
17775
- channel: string().optional(),
17776
- lastActivityAt: string(),
17777
- usage: SessionUsage
17778
- }),
17779
- "facts/runtime-profile": object({
17780
- runtime: string(),
17781
- version: string(),
17782
- models: array(string()),
17783
- contextWindow: number().int().optional(),
17784
- acpSupport: _enum([
17785
- "full",
17786
- "partial",
17787
- "none"
17788
- ]),
17789
- toolCalling: boolean()
17790
- }),
17791
- "session/list": SessionListReq,
17792
- "session/list/page": SessionListPage,
17793
- "session/history": SessionHistoryReq,
17794
- "session/history/page": SessionHistoryPage,
17795
- "config/push": object({ keys: record(string(), unknown()) }),
17796
- "daemon/restart": object({
17797
- reason: string(),
17798
- drainFirst: boolean().default(true)
17799
- }),
17800
- "daemon/upgrade": object({
17801
- targetVersion: string(),
17802
- drainFirst: boolean().default(true)
17803
- }),
17804
- "daemon/control/ack": object({
17805
- accepted: boolean(),
17806
- willDrainUntil: string().datetime().optional()
17807
- }),
17808
- ack: object({
17809
- ok: boolean(),
17810
- reason: string().optional()
17811
- }),
17812
- error: object({
17813
- code: _enum([
17814
- "UNKNOWN_FRAME",
17815
- "FRAME_TOO_LARGE",
17816
- "PROTOCOL_STATE",
17817
- "BAD_PAYLOAD",
17818
- "AUTH_FAILED",
17819
- "ATTESTATION_INVALID",
17820
- "STALE_EPOCH",
17821
- "STALE_LAUNCH",
17822
- "SEQ_GAP",
17823
- "NO_SESSION",
17824
- "HELD",
17825
- "SCOPE_DENIED",
17826
- "LEASE_EXPIRED",
17827
- "LEASE_DENIED",
17828
- "RATE_LIMITED",
17829
- "INTERNAL"
17830
- ]),
17831
- message: string(),
17832
- retryable: boolean(),
17833
- details: record(string(), unknown()).optional()
17834
- })
17620
+ const [thisStream, peerStream] = memoryStreamPair();
17621
+ const peerRawConnection = target[appBuilder]().connect(peerStream);
17622
+ const peerConnection = agentConnection(peerRawConnection);
17623
+ const state = this.openStreamConnection(thisStream);
17624
+ state.rawConnection.closed.then(() => peerConnection.close());
17625
+ peerRawConnection.closed.then(() => state.connection.close());
17626
+ try {
17627
+ target[runAgentConnectHandlers](peerConnection);
17628
+ this[runClientConnectHandlers](state.connection);
17629
+ } catch (error) {
17630
+ peerConnection.close(error);
17631
+ state.connection.close(error);
17632
+ throw error;
17633
+ }
17634
+ return state;
17635
+ }
17636
+ openStreamConnection(stream) {
17637
+ const rawConnection = this.builder.connect(stream);
17638
+ return {
17639
+ rawConnection,
17640
+ connection: clientConnection(rawConnection, this.connectHandlers)
17641
+ };
17642
+ }
17835
17643
  };
17836
- Object.keys(FRAME_SCHEMAS);
17644
+ AGENT_METHODS.initialize, AGENT_METHODS.authenticate, AGENT_METHODS.providers_list, AGENT_METHODS.providers_set, AGENT_METHODS.providers_disable, AGENT_METHODS.session_new, AGENT_METHODS.session_load, AGENT_METHODS.session_set_mode, AGENT_METHODS.session_set_config_option, AGENT_METHODS.session_prompt, AGENT_METHODS.session_list, AGENT_METHODS.session_delete, AGENT_METHODS.session_fork, AGENT_METHODS.session_resume, AGENT_METHODS.session_close, AGENT_METHODS.logout, AGENT_METHODS.nes_start, AGENT_METHODS.nes_suggest, AGENT_METHODS.nes_close;
17645
+ AGENT_METHODS.session_cancel, AGENT_METHODS.nes_accept, AGENT_METHODS.nes_reject, AGENT_METHODS.document_did_open, AGENT_METHODS.document_did_change, AGENT_METHODS.document_did_close, AGENT_METHODS.document_did_save, AGENT_METHODS.document_did_focus;
17646
+ CLIENT_METHODS.session_request_permission, CLIENT_METHODS.fs_write_text_file, CLIENT_METHODS.fs_read_text_file, CLIENT_METHODS.terminal_create, CLIENT_METHODS.terminal_output, CLIENT_METHODS.terminal_release, CLIENT_METHODS.terminal_wait_for_exit, CLIENT_METHODS.terminal_kill, CLIENT_METHODS.elicitation_create;
17647
+ CLIENT_METHODS.session_update, CLIENT_METHODS.elicitation_complete;
17648
+ //#endregion
17649
+ //#region src/runtimes/probe.ts
17837
17650
  /**
17838
- * Builds the envelope schema for one frame `type` with a `type` literal and the
17839
- * typed payload, so the discriminated union infers `payload` precisely.
17651
+ * Host-availability probing for runtimes.
17652
+ *
17653
+ * `resolveRuntimes()` produces a runtime for every ACP-registry entry that has a
17654
+ * distribution for this platform — but that says nothing about whether the tool
17655
+ * is actually runnable *here*. This module answers that question so the daemon
17656
+ * only advertises genuinely-installed runtimes in `RegisterReq.capabilities`.
17657
+ *
17658
+ * Two layers:
17659
+ * 1. A generic launcher check — is the runtime's `command` resolvable? For a
17660
+ * runtime distributed as a real binary (`command` is the tool's own
17661
+ * executable) this is a meaningful signal: the binary on `$PATH` means it's
17662
+ * installed.
17663
+ * 2. For runtimes distributed via a package launcher (`npx`/`uvx`), (1) is
17664
+ * meaningless — the launcher fetches on demand, so "npx exists" is true on
17665
+ * virtually every dev host and tells us nothing about the wrapped agent. For
17666
+ * those we REQUIRE a per-runtime custom probe (`CUSTOM_PROBES`) that looks for
17667
+ * the wrapped CLI's own config/state dir, which appears once the user has
17668
+ * installed and run/initialized it. A launcher-distributed runtime with no
17669
+ * probe is treated as NOT installed — otherwise every npx/uvx agent in the
17670
+ * ACP registry would be falsely advertised just because `npx` is on `$PATH`.
17671
+ *
17672
+ * The custom probes test for the config *directory* (the "installed & initialized
17673
+ * on this host" signal). A stricter "is logged in" check would look for the auth
17674
+ * file instead (e.g. `~/.codex/auth.json`), but that yields false negatives for
17675
+ * users who authenticate via an API-key env var, so directory presence is the
17676
+ * pragmatic signal. Paths verified against each tool's docs (June 2026).
17840
17677
  */
17841
- function frame(type, payload) {
17842
- return object({
17843
- v: literal(1),
17844
- id: string().uuid(),
17845
- ts: string().datetime(),
17846
- type: literal(type),
17847
- corr: string().uuid().optional(),
17848
- payload
17849
- });
17678
+ const isWin = process.platform === "win32";
17679
+ function isExecutableFile(p) {
17680
+ try {
17681
+ accessSync(p, isWin ? constants.F_OK : constants.X_OK);
17682
+ return true;
17683
+ } catch {
17684
+ return false;
17685
+ }
17850
17686
  }
17851
- discriminatedUnion("type", [
17852
- frame("auth", FRAME_SCHEMAS["auth"]),
17853
- frame("auth/ok", FRAME_SCHEMAS["auth/ok"]),
17854
- frame("register", FRAME_SCHEMAS["register"]),
17855
- frame("register/ok", FRAME_SCHEMAS["register/ok"]),
17856
- frame("heartbeat", FRAME_SCHEMAS["heartbeat"]),
17857
- frame("agent/launch", FRAME_SCHEMAS["agent/launch"]),
17858
- frame("agent/launched", FRAME_SCHEMAS["agent/launched"]),
17859
- frame("agent/stop", FRAME_SCHEMAS["agent/stop"]),
17860
- frame("agent/upsert", FRAME_SCHEMAS["agent/upsert"]),
17861
- frame("agent/remove", FRAME_SCHEMAS["agent/remove"]),
17862
- frame("agent/prompt", FRAME_SCHEMAS["agent/prompt"]),
17863
- frame("agent/prompt/ack", FRAME_SCHEMAS["agent/prompt/ack"]),
17864
- frame("agent/activity", FRAME_SCHEMAS["agent/activity"]),
17865
- frame("agent/scope-denied", FRAME_SCHEMAS["agent/scope-denied"]),
17866
- frame("route/assign", FRAME_SCHEMAS["route/assign"]),
17867
- frame("route/assign/ack", FRAME_SCHEMAS["route/assign/ack"]),
17868
- frame("route/update", FRAME_SCHEMAS["route/update"]),
17869
- frame("daemon/drain", FRAME_SCHEMAS["daemon/drain"]),
17870
- frame("drain/progress", FRAME_SCHEMAS["drain/progress"]),
17871
- frame("drain/done", FRAME_SCHEMAS["drain/done"]),
17872
- frame("cron/upsert", FRAME_SCHEMAS["cron/upsert"]),
17873
- frame("cron/remove", FRAME_SCHEMAS["cron/remove"]),
17874
- frame("integration/upsert", FRAME_SCHEMAS["integration/upsert"]),
17875
- frame("integration/remove", FRAME_SCHEMAS["integration/remove"]),
17876
- frame("secrets/request", FRAME_SCHEMAS["secrets/request"]),
17877
- frame("secrets/grant", FRAME_SCHEMAS["secrets/grant"]),
17878
- frame("secrets/renew", FRAME_SCHEMAS["secrets/renew"]),
17879
- frame("secrets/revoke", FRAME_SCHEMAS["secrets/revoke"]),
17880
- frame("scope-attestation", FRAME_SCHEMAS["scope-attestation"]),
17881
- frame("event/session", FRAME_SCHEMAS["event/session"]),
17882
- frame("usage/report", FRAME_SCHEMAS["usage/report"]),
17883
- frame("facts/runtime-profile", FRAME_SCHEMAS["facts/runtime-profile"]),
17884
- frame("session/list", FRAME_SCHEMAS["session/list"]),
17885
- frame("session/list/page", FRAME_SCHEMAS["session/list/page"]),
17886
- frame("session/history", FRAME_SCHEMAS["session/history"]),
17887
- frame("session/history/page", FRAME_SCHEMAS["session/history/page"]),
17888
- frame("config/push", FRAME_SCHEMAS["config/push"]),
17889
- frame("daemon/restart", FRAME_SCHEMAS["daemon/restart"]),
17890
- frame("daemon/upgrade", FRAME_SCHEMAS["daemon/upgrade"]),
17891
- frame("daemon/control/ack", FRAME_SCHEMAS["daemon/control/ack"]),
17892
- frame("ack", FRAME_SCHEMAS["ack"]),
17893
- frame("error", FRAME_SCHEMAS["error"])
17894
- ]);
17895
- //#endregion
17896
- //#region ../protocol/dist/codec.js
17897
- /** Soft cap per frame — 256 KiB (protocol §1). Over this → FRAME_TOO_LARGE. */
17898
- const MAX_FRAME_BYTES = 256 * 1024;
17899
- const textEncoder = new TextEncoder();
17900
- function byteLength(text) {
17901
- return textEncoder.encode(text).length;
17687
+ /**
17688
+ * Resolve a launcher command the way a shell would. A command containing a path
17689
+ * separator is treated as a literal path; a bare name is searched across `$PATH`
17690
+ * (trying `$PATHEXT` extensions on Windows).
17691
+ */
17692
+ function isCommandAvailable(command, env = process.env) {
17693
+ return resolveCommandPath(command, env) !== void 0;
17902
17694
  }
17903
- function extractControlExt(json) {
17904
- if (typeof json !== "object" || json === null) return void 0;
17905
- const o = json;
17906
- const ext = {};
17907
- if (typeof o.epoch === "number") ext.epoch = o.epoch;
17908
- if (typeof o.seq === "number") ext.seq = o.seq;
17909
- if (typeof o.agentId === "string") ext.agentId = o.agentId;
17910
- if (typeof o.launchId === "string") ext.launchId = o.launchId;
17911
- return Object.keys(ext).length > 0 ? ext : void 0;
17695
+ /**
17696
+ * Resolve a launcher/command to its absolute path the way a shell would — a path
17697
+ * containing a separator is checked literally; a bare name is searched across
17698
+ * `$PATH` (trying `$PATHEXT` extensions on Windows). Returns undefined if not found.
17699
+ */
17700
+ function resolveCommandPath(command, env = process.env) {
17701
+ const exts = isWin ? ["", ...(env.PATHEXT ?? ".EXE;.CMD;.BAT;.COM").split(";")] : [""];
17702
+ if (command.includes("/") || isWin && command.includes("\\")) return exts.map((ext) => command + ext).find(isExecutableFile);
17703
+ const dirs = (env.PATH ?? "").split(delimiter).filter(Boolean);
17704
+ for (const dir of dirs) for (const ext of exts) {
17705
+ const p = join(dir, command) + ext;
17706
+ if (isExecutableFile(p)) return p;
17707
+ }
17912
17708
  }
17913
- function decodeEnvelope(text) {
17914
- if (byteLength(text) > 262144) return {
17915
- ok: false,
17916
- id: NIL_UUID,
17917
- msg: "FRAME_TOO_LARGE"
17709
+ function home(env) {
17710
+ return (isWin ? env.USERPROFILE : env.HOME) || homedir();
17711
+ }
17712
+ function xdgConfigHome(env) {
17713
+ return env.XDG_CONFIG_HOME || join(home(env), ".config");
17714
+ }
17715
+ function xdgDataHome(env) {
17716
+ return env.XDG_DATA_HOME || join(home(env), ".local", "share");
17717
+ }
17718
+ function pathExists(p) {
17719
+ return !!p && existsSync(p);
17720
+ }
17721
+ /** True if any candidate path exists (undefined candidates are skipped). */
17722
+ function anyExists(...candidates) {
17723
+ return candidates.some(pathExists);
17724
+ }
17725
+ /**
17726
+ * Custom probes keyed by ACP-registry runtime id. Add an entry here whenever the
17727
+ * launcher-on-PATH check is too weak to mean "actually installed & set up".
17728
+ * Each tests for the tool's home/XDG config dir (or, where the dir is ambiguous,
17729
+ * a tool-specific file), honoring the documented env overrides.
17730
+ */
17731
+ const CUSTOM_PROBES = {
17732
+ "claude-acp": (env) => anyExists(join(home(env), ".claude"), join(home(env), ".claude.json")),
17733
+ "codex-acp": (env) => anyExists(env.CODEX_HOME, join(home(env), ".codex")),
17734
+ gemini: (env) => pathExists(join(home(env), ".gemini")),
17735
+ "qwen-code": (env) => pathExists(join(home(env), ".qwen")),
17736
+ "github-copilot-cli": (env) => anyExists(env.COPILOT_HOME, join(home(env), ".copilot")),
17737
+ cursor: (env) => anyExists(env.CURSOR_CONFIG_DIR ? join(env.CURSOR_CONFIG_DIR, "cli-config.json") : void 0, join(home(env), ".cursor", "cli-config.json")),
17738
+ opencode: (env) => anyExists(join(xdgConfigHome(env), "opencode"), join(xdgDataHome(env), "opencode", "auth.json")),
17739
+ goose: (env) => pathExists(join(xdgConfigHome(env), "goose")),
17740
+ "amp-acp": (env) => anyExists(env.AMP_SETTINGS_FILE, join(xdgConfigHome(env), "amp")),
17741
+ auggie: (env) => pathExists(join(home(env), ".augment")),
17742
+ cline: (env) => anyExists(env.CLINE_DATA_DIR, join(home(env), ".cline")),
17743
+ "grok-build": (env) => pathExists(join(home(env), ".grok")),
17744
+ kimi: (env) => anyExists(env.KIMI_CODE_HOME, join(home(env), ".kimi"), join(home(env), ".kimi-code")),
17745
+ "factory-droid": (env) => pathExists(join(home(env), ".factory")),
17746
+ devin: (env) => anyExists(join(xdgConfigHome(env), "devin"), join(xdgDataHome(env), "devin"))
17747
+ };
17748
+ /**
17749
+ * Package launchers that fetch-and-run on demand. When a runtime is launched via
17750
+ * one of these, the launcher being on `$PATH` says nothing about whether the
17751
+ * wrapped agent is installed, so such runtimes require a custom probe to count.
17752
+ */
17753
+ const PACKAGE_LAUNCHERS = /* @__PURE__ */ new Set(["npx", "uvx"]);
17754
+ /** Is this runtime actually usable on this host? */
17755
+ function isRuntimeAvailable(id, rt, env = process.env) {
17756
+ if (!isCommandAvailable(rt.command, env)) return false;
17757
+ const custom = CUSTOM_PROBES[id];
17758
+ if (custom) return custom(env);
17759
+ return !PACKAGE_LAUNCHERS.has(rt.command);
17760
+ }
17761
+ /**
17762
+ * Filter a runtime map down to those installed on this host — what the daemon
17763
+ * should report to the Control Plane and is able to launch.
17764
+ */
17765
+ function installedRuntimes(runtimes, env = process.env) {
17766
+ const out = {};
17767
+ for (const [id, rt] of Object.entries(runtimes)) if (isRuntimeAvailable(id, rt, env)) out[id] = rt;
17768
+ return out;
17769
+ }
17770
+ //#endregion
17771
+ //#region src/acp/acp-host.ts
17772
+ const PROTOCOL_VERSION = 1;
17773
+ /**
17774
+ * Extract the model selector from a session's `configOptions`. ACP models are an
17775
+ * (experimental) session config option tagged `category: "model"`, `type:
17776
+ * "select"`; the selectable values (optionally grouped) are the model list.
17777
+ * Returns null when the agent advertises no model selector.
17778
+ */
17779
+ function modelOptionsFrom(configOptions) {
17780
+ const model = configOptions?.find((o) => o.category === "model" && o.type === "select");
17781
+ if (!model || model.type !== "select") return null;
17782
+ const models = model.options.flatMap((o) => "group" in o ? o.options : [o]).map((o) => o.value);
17783
+ return {
17784
+ current: model.currentValue,
17785
+ models
17918
17786
  };
17919
- let json;
17920
- try {
17921
- json = JSON.parse(text);
17922
- } catch {
17787
+ }
17788
+ var AcpHost = class {
17789
+ runtime;
17790
+ opts;
17791
+ child;
17792
+ conn;
17793
+ live = /* @__PURE__ */ new Set();
17794
+ loadingSessions = /* @__PURE__ */ new Set();
17795
+ canLoad = false;
17796
+ promptCaps = {};
17797
+ lastModelOptions = null;
17798
+ constructor(runtime, opts) {
17799
+ this.runtime = runtime;
17800
+ this.opts = opts;
17801
+ }
17802
+ /** A Claude Code runtime (its command/args reference `claude`) — these embed the
17803
+ * @anthropic-ai/claude-agent-sdk, which needs a Claude Code executable. */
17804
+ isClaudeRuntime() {
17805
+ return [this.runtime.command, ...this.runtime.args].join(" ").toLowerCase().includes("claude");
17806
+ }
17807
+ async start() {
17808
+ if (this.child) throw new Error("AcpHost: already started");
17809
+ const env = {
17810
+ ...process.env,
17811
+ ...Object.fromEntries(this.runtime.env.map((e) => [e.name, e.value])),
17812
+ ...this.opts.env ?? {}
17813
+ };
17814
+ if (!env.CLAUDE_CODE_EXECUTABLE && this.isClaudeRuntime()) {
17815
+ const claude = resolveCommandPath("claude", env);
17816
+ if (claude) {
17817
+ env.CLAUDE_CODE_EXECUTABLE = claude;
17818
+ this.opts.log?.info(`acp: CLAUDE_CODE_EXECUTABLE not set — using claude on PATH (${claude})`);
17819
+ }
17820
+ }
17821
+ const child = spawn(this.runtime.command, this.runtime.args, {
17822
+ stdio: [
17823
+ "pipe",
17824
+ "pipe",
17825
+ "inherit"
17826
+ ],
17827
+ env
17828
+ });
17829
+ this.child = child;
17830
+ if (!child.stdin || !child.stdout) throw new Error("AcpHost: subprocess stdin/stdout are not piped");
17831
+ const stream = ndJsonStream(Writable.toWeb(child.stdin), Readable.toWeb(child.stdout));
17832
+ const self = this;
17833
+ this.conn = client({ name: "agentconnect" }).onNotification(methods.client.session.update, (ctx) => {
17834
+ if (self.loadingSessions.has(ctx.params.sessionId)) return;
17835
+ self.opts.onUpdate(ctx.params.sessionId, ctx.params.update);
17836
+ }).onRequest(methods.client.session.requestPermission, (ctx) => {
17837
+ const optionId = (ctx.params.options.find((o) => o.kind === "allow_once" || o.kind === "allow_always") ?? ctx.params.options[0])?.optionId;
17838
+ return { outcome: optionId ? {
17839
+ outcome: "selected",
17840
+ optionId
17841
+ } : { outcome: "cancelled" } };
17842
+ }).connect(stream);
17843
+ const init = await this.conn.agent.request(methods.agent.initialize, {
17844
+ protocolVersion: PROTOCOL_VERSION,
17845
+ clientCapabilities: { fs: {
17846
+ readTextFile: false,
17847
+ writeTextFile: false
17848
+ } }
17849
+ });
17850
+ this.canLoad = init.agentCapabilities?.loadSession ?? false;
17851
+ this.promptCaps = init.agentCapabilities?.promptCapabilities ?? {};
17852
+ this.opts.log?.debug(`acp: agent initialized (loadSession=${this.canLoad}, prompt caps: image=${!!this.promptCaps.image} audio=${!!this.promptCaps.audio} embeddedContext=${!!this.promptCaps.embeddedContext})`);
17853
+ }
17854
+ /** Whether the agent advertised support for a gated prompt content-block kind.
17855
+ * `text` and `resource_link` are baseline and always return true. */
17856
+ promptSupports(kind) {
17857
+ return Boolean(this.promptCaps[kind]);
17858
+ }
17859
+ async newSession(cwd, mcpServers = []) {
17860
+ const res = await this.conn.agent.request(methods.agent.session.new, {
17861
+ cwd,
17862
+ mcpServers
17863
+ });
17864
+ this.lastModelOptions = modelOptionsFrom(res.configOptions);
17865
+ this.live.add(res.sessionId);
17866
+ return res.sessionId;
17867
+ }
17868
+ /** The model selector advertised by the most recent session/new, or null if the
17869
+ * agent exposed none. Used by the runtime probe to report available models. */
17870
+ modelOptions() {
17871
+ return this.lastModelOptions;
17872
+ }
17873
+ /** True iff THIS agent process created or loaded `sessionId` in its current lifetime. */
17874
+ hasSession(sessionId) {
17875
+ return this.live.has(sessionId);
17876
+ }
17877
+ /** Whether the agent advertised the `loadSession` capability (session/load is usable). */
17878
+ loadSupported() {
17879
+ return this.canLoad;
17880
+ }
17881
+ /** Resume a previously-created session by id (ACP `session/load`). The agent
17882
+ * restores its own history server-side; its replayed session/update stream is
17883
+ * suppressed so it isn't re-posted. Throws if the agent can't load the id
17884
+ * (caller falls back to newSession). */
17885
+ async loadSession(sessionId, cwd) {
17886
+ this.loadingSessions.add(sessionId);
17887
+ try {
17888
+ await this.conn.agent.request(methods.agent.session.load, {
17889
+ sessionId,
17890
+ cwd,
17891
+ mcpServers: []
17892
+ });
17893
+ this.live.add(sessionId);
17894
+ this.opts.log?.info(`acp: resumed session ${sessionId} via session/load`);
17895
+ } catch (err) {
17896
+ this.opts.log?.debug(`acp: session/load failed for ${sessionId} (${err.message}) — will recreate`);
17897
+ throw err;
17898
+ } finally {
17899
+ this.loadingSessions.delete(sessionId);
17900
+ }
17901
+ }
17902
+ /** Drive one turn to completion. Returns the stop reason plus the agent's token
17903
+ * `usage` when the runtime reports it (ACP's experimental per-turn usage, which
17904
+ * carries session-cumulative counts) — `undefined` when it doesn't. */
17905
+ async prompt(sessionId, blocks) {
17906
+ const res = await this.conn.agent.request(methods.agent.session.prompt, {
17907
+ sessionId,
17908
+ prompt: blocks
17909
+ });
17923
17910
  return {
17924
- ok: false,
17925
- id: NIL_UUID,
17926
- msg: "invalid json"
17911
+ stopReason: res.stopReason,
17912
+ usage: res.usage ?? void 0
17927
17913
  };
17928
17914
  }
17929
- const env = Envelope.safeParse(json);
17930
- if (!env.success) return {
17931
- ok: false,
17932
- id: typeof json === "object" && json !== null && typeof json.id === "string" ? json.id : NIL_UUID,
17933
- msg: env.error.message
17934
- };
17935
- const schema = FRAME_SCHEMAS[env.data.type];
17936
- if (!schema) return {
17937
- ok: false,
17938
- id: env.data.id,
17939
- msg: "UNKNOWN_FRAME"
17940
- };
17941
- const payload = schema.safeParse(env.data.payload);
17942
- if (!payload.success) return {
17943
- ok: false,
17944
- id: env.data.id,
17945
- msg: payload.error.message
17946
- };
17947
- const ext = extractControlExt(json);
17948
- return {
17949
- ok: true,
17950
- frame: {
17951
- ...env.data,
17952
- payload: payload.data
17953
- },
17954
- ...ext ? { ext } : {}
17955
- };
17956
- }
17957
- function buildEnvelope(type, payload, opts = {}) {
17958
- return {
17959
- v: 1,
17960
- id: opts.id ?? randomUUID(),
17961
- ts: opts.ts ?? (/* @__PURE__ */ new Date()).toISOString(),
17962
- type,
17963
- payload,
17964
- ...opts.corr ? { corr: opts.corr } : {},
17965
- ...opts.ext ?? {}
17915
+ async cancel(sessionId) {
17916
+ await this.conn.agent.notify(methods.agent.session.cancel, { sessionId });
17917
+ }
17918
+ /** Stop the adapter child: SIGTERM, then escalate to SIGKILL if it hasn't exited
17919
+ * within `deadlineMs` (a buggy/hung agent must never block daemon shutdown or an
17920
+ * idle reap). Idempotent — the child handle is cleared up front so a concurrent
17921
+ * stop (drain + reconcile racing) is a no-op rather than a double-kill. */
17922
+ async stop(deadlineMs = 5e3) {
17923
+ const child = this.child;
17924
+ if (!child) return;
17925
+ this.child = void 0;
17926
+ child.kill("SIGTERM");
17927
+ await new Promise((resolve) => {
17928
+ let settled = false;
17929
+ const done = () => {
17930
+ if (settled) return;
17931
+ settled = true;
17932
+ clearTimeout(timer);
17933
+ resolve();
17934
+ };
17935
+ const timer = setTimeout(() => {
17936
+ this.opts.log?.warn(`acp: child ignored SIGTERM after ${deadlineMs}ms — sending SIGKILL`);
17937
+ child.kill("SIGKILL");
17938
+ }, deadlineMs);
17939
+ child.once("exit", done);
17940
+ });
17941
+ }
17942
+ };
17943
+ //#endregion
17944
+ //#region src/cli/chat.ts
17945
+ function renderUpdate(out) {
17946
+ return (_sessionId, update) => {
17947
+ if (update.sessionUpdate === "agent_message_chunk" && update.content.type === "text") out.write(update.content.text);
17948
+ else if (update.sessionUpdate === "tool_call") {
17949
+ const title = "title" in update && update.title || update.toolCallId || "tool";
17950
+ out.write(`\n[tool] ${title}\n`);
17951
+ }
17966
17952
  };
17967
17953
  }
17968
- function encode(frame) {
17969
- return JSON.stringify(frame);
17954
+ async function runChat(opts) {
17955
+ const out = opts.out ?? process.stdout;
17956
+ const root = resolveRoot(opts.root);
17957
+ const cfg = loadConfig({
17958
+ root: opts.root,
17959
+ configPath: opts.configPath,
17960
+ optional: true,
17961
+ overrides: { agentsDir: opts.agentsDir }
17962
+ });
17963
+ const agent = selectAgent(cfg.agentsDir, opts.agentName);
17964
+ const runtimes = await resolveRuntimes(cfg, root, {
17965
+ neededRuntimes: [agent.runtime],
17966
+ mode: "cache-first"
17967
+ });
17968
+ const runtime = runtimes[agent.runtime];
17969
+ if (!runtime) {
17970
+ const available = Object.keys(runtimes).sort().join(", ") || "(none)";
17971
+ throw new Error(`runtime "${agent.runtime}" not found. Available: ${available}`);
17972
+ }
17973
+ const host = new AcpHost(runtime, {
17974
+ onUpdate: renderUpdate(out),
17975
+ env: agentChildEnv(agent)
17976
+ });
17977
+ await host.start();
17978
+ try {
17979
+ const cwd = await prepareWorkspace(agent);
17980
+ const sessionId = await host.newSession(cwd);
17981
+ const send = async (text) => {
17982
+ const blocks = [{
17983
+ type: "text",
17984
+ text
17985
+ }];
17986
+ await host.prompt(sessionId, blocks);
17987
+ out.write("\n");
17988
+ };
17989
+ if (typeof opts.message === "string" && opts.message.length > 0) {
17990
+ await send(opts.message);
17991
+ return;
17992
+ }
17993
+ const rl = createInterface({ input: opts.input ?? process.stdin });
17994
+ out.write(`Chatting with ${agent.name} (${agent.id}). Type .exit to quit.\n> `);
17995
+ try {
17996
+ for await (const line of rl) {
17997
+ const text = line.trim();
17998
+ if (text === ".exit" || text === ".quit") break;
17999
+ if (text.length > 0) await send(text);
18000
+ out.write("> ");
18001
+ }
18002
+ } finally {
18003
+ rl.close();
18004
+ }
18005
+ } finally {
18006
+ await host.stop();
18007
+ }
17970
18008
  }
17971
18009
  //#endregion
17972
- //#region ../protocol/dist/index.js
17973
- /**
17974
- * Narrowing guard factory: `isFrame("auth")(frame)` narrows a decoded
17975
- * `AnyFrame` to the member whose `type` matches.
17976
- */
17977
- function isFrame(type) {
17978
- return (frame) => frame.type === type;
17979
- }
18010
+ //#region src/version.ts
18011
+ const DAEMON_VERSION = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8")).version;
17980
18012
  //#endregion
17981
18013
  //#region ../../node_modules/.pnpm/ws@8.21.0/node_modules/ws/lib/constants.js
17982
18014
  var require_constants$1 = /* @__PURE__ */ __commonJSMin(((exports, module) => {
@@ -80832,9 +80864,13 @@ function splitIntoSections(text, maxLen = SLACK_SECTION_LIMIT) {
80832
80864
  const THINKING = "is thinking…";
80833
80865
  const MAX_ACTIVITY = 10;
80834
80866
  const MAX_LABEL = 100;
80835
- function clampLabel(s) {
80867
+ const MAX_STATUS = 50;
80868
+ function clampTo(s, max) {
80836
80869
  const t = s.trim();
80837
- return t.length > MAX_LABEL ? `${t.slice(0, MAX_LABEL - 1)}…` : t;
80870
+ return t.length > max ? `${t.slice(0, max - 1)}…` : t;
80871
+ }
80872
+ function clampLabel(s) {
80873
+ return clampTo(s, MAX_LABEL);
80838
80874
  }
80839
80875
  function planIcon(status) {
80840
80876
  if (status === "completed") return ":white_check_mark:";
@@ -80878,7 +80914,7 @@ var OutputConverger = class {
80878
80914
  * which throttles streamed thought chunks down to one status update per thinking run.
80879
80915
  */
80880
80916
  pushActivity(raw) {
80881
- const label = clampLabel(raw);
80917
+ const label = clampTo(raw, MAX_STATUS);
80882
80918
  if (this.activity[this.activity.length - 1] === label) return [];
80883
80919
  this.activity.push(label);
80884
80920
  if (this.activity.length > MAX_ACTIVITY) this.activity.shift();
@@ -82369,7 +82405,7 @@ function applySpecFields(raw, spec, opts) {
82369
82405
  const existing = typeof raw.workspace === "object" && raw.workspace !== null ? raw.workspace : {};
82370
82406
  existing.mode = mapWorkspaceMode(ws.mode);
82371
82407
  if (ws.mode === "github") {
82372
- existing.gitRepo = ws.gitRepo;
82408
+ existing.gitRepo = normalizeGitUrl(ws.gitRepo);
82373
82409
  existing.gitBranch = ws.branch;
82374
82410
  }
82375
82411
  if (opts.creating && existing.path === void 0) existing.path = join(opts.agentDir, "workspace");