@agentconnect.md/daemon 1.0.0-rc.56 → 1.0.0-rc.58

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(),
@@ -17253,731 +18010,6 @@ async function runChat(opts) {
17253
18010
  //#region src/version.ts
17254
18011
  const DAEMON_VERSION = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8")).version;
17255
18012
  //#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
- })
17835
- };
17836
- Object.keys(FRAME_SCHEMAS);
17837
- /**
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.
17840
- */
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
- });
17850
- }
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;
17902
- }
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;
17912
- }
17913
- function decodeEnvelope(text) {
17914
- if (byteLength(text) > 262144) return {
17915
- ok: false,
17916
- id: NIL_UUID,
17917
- msg: "FRAME_TOO_LARGE"
17918
- };
17919
- let json;
17920
- try {
17921
- json = JSON.parse(text);
17922
- } catch {
17923
- return {
17924
- ok: false,
17925
- id: NIL_UUID,
17926
- msg: "invalid json"
17927
- };
17928
- }
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 ?? {}
17966
- };
17967
- }
17968
- function encode(frame) {
17969
- return JSON.stringify(frame);
17970
- }
17971
- //#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
- }
17980
- //#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) => {
17983
18015
  const BINARY_TYPES = [
@@ -82373,7 +82405,7 @@ function applySpecFields(raw, spec, opts) {
82373
82405
  const existing = typeof raw.workspace === "object" && raw.workspace !== null ? raw.workspace : {};
82374
82406
  existing.mode = mapWorkspaceMode(ws.mode);
82375
82407
  if (ws.mode === "github") {
82376
- existing.gitRepo = ws.gitRepo;
82408
+ existing.gitRepo = normalizeGitUrl(ws.gitRepo);
82377
82409
  existing.gitBranch = ws.branch;
82378
82410
  }
82379
82411
  if (opts.creating && existing.path === void 0) existing.path = join(opts.agentDir, "workspace");