@agentconnect.md/daemon 1.41.0-rc.31 → 1.41.0-rc.32

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.
@@ -1,6 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
  import { createRequire } from "node:module";
3
- import { readFileSync } from "node:fs";
3
+ import { accessSync, constants, readFileSync, statSync } from "node:fs";
4
+ import { spawn } from "node:child_process";
5
+ import { delimiter, isAbsolute, join, resolve } from "node:path";
4
6
  //#region \0rolldown/runtime.js
5
7
  var __create = Object.create;
6
8
  var __defProp = Object.defineProperty;
@@ -5563,6 +5565,115 @@ function handleIntersectionResults(result, left, right) {
5563
5565
  result.value = merged.data;
5564
5566
  return result;
5565
5567
  }
5568
+ const $ZodRecord = /*@__PURE__*/ $constructor("$ZodRecord", (inst, def) => {
5569
+ $ZodType.init(inst, def);
5570
+ inst._zod.parse = (payload, ctx) => {
5571
+ const input = payload.value;
5572
+ if (!isPlainObject(input)) {
5573
+ payload.issues.push({
5574
+ expected: "record",
5575
+ code: "invalid_type",
5576
+ input,
5577
+ inst
5578
+ });
5579
+ return payload;
5580
+ }
5581
+ const proms = [];
5582
+ const values = def.keyType._zod.values;
5583
+ if (values) {
5584
+ payload.value = {};
5585
+ const recordKeys = /* @__PURE__ */ new Set();
5586
+ for (const key of values) if (typeof key === "string" || typeof key === "number" || typeof key === "symbol") {
5587
+ recordKeys.add(typeof key === "number" ? key.toString() : key);
5588
+ const keyResult = def.keyType._zod.run({
5589
+ value: key,
5590
+ issues: []
5591
+ }, ctx);
5592
+ if (keyResult instanceof Promise) throw new Error("Async schemas not supported in object keys currently");
5593
+ if (keyResult.issues.length) {
5594
+ payload.issues.push({
5595
+ code: "invalid_key",
5596
+ origin: "record",
5597
+ issues: keyResult.issues.map((iss) => finalizeIssue(iss, ctx, config())),
5598
+ input: key,
5599
+ path: [key],
5600
+ inst
5601
+ });
5602
+ continue;
5603
+ }
5604
+ const outKey = keyResult.value;
5605
+ const result = def.valueType._zod.run({
5606
+ value: input[key],
5607
+ issues: []
5608
+ }, ctx);
5609
+ if (result instanceof Promise) proms.push(result.then((result) => {
5610
+ if (result.issues.length) payload.issues.push(...prefixIssues(key, result.issues));
5611
+ payload.value[outKey] = result.value;
5612
+ }));
5613
+ else {
5614
+ if (result.issues.length) payload.issues.push(...prefixIssues(key, result.issues));
5615
+ payload.value[outKey] = result.value;
5616
+ }
5617
+ }
5618
+ let unrecognized;
5619
+ for (const key in input) if (!recordKeys.has(key)) {
5620
+ unrecognized = unrecognized ?? [];
5621
+ unrecognized.push(key);
5622
+ }
5623
+ if (unrecognized && unrecognized.length > 0) payload.issues.push({
5624
+ code: "unrecognized_keys",
5625
+ input,
5626
+ inst,
5627
+ keys: unrecognized
5628
+ });
5629
+ } else {
5630
+ payload.value = {};
5631
+ for (const key of Reflect.ownKeys(input)) {
5632
+ if (key === "__proto__") continue;
5633
+ if (!Object.prototype.propertyIsEnumerable.call(input, key)) continue;
5634
+ let keyResult = def.keyType._zod.run({
5635
+ value: key,
5636
+ issues: []
5637
+ }, ctx);
5638
+ if (keyResult instanceof Promise) throw new Error("Async schemas not supported in object keys currently");
5639
+ if (typeof key === "string" && number$1.test(key) && keyResult.issues.length) {
5640
+ const retryResult = def.keyType._zod.run({
5641
+ value: Number(key),
5642
+ issues: []
5643
+ }, ctx);
5644
+ if (retryResult instanceof Promise) throw new Error("Async schemas not supported in object keys currently");
5645
+ if (retryResult.issues.length === 0) keyResult = retryResult;
5646
+ }
5647
+ if (keyResult.issues.length) {
5648
+ if (def.mode === "loose") payload.value[key] = input[key];
5649
+ else payload.issues.push({
5650
+ code: "invalid_key",
5651
+ origin: "record",
5652
+ issues: keyResult.issues.map((iss) => finalizeIssue(iss, ctx, config())),
5653
+ input: key,
5654
+ path: [key],
5655
+ inst
5656
+ });
5657
+ continue;
5658
+ }
5659
+ const result = def.valueType._zod.run({
5660
+ value: input[key],
5661
+ issues: []
5662
+ }, ctx);
5663
+ if (result instanceof Promise) proms.push(result.then((result) => {
5664
+ if (result.issues.length) payload.issues.push(...prefixIssues(key, result.issues));
5665
+ payload.value[keyResult.value] = result.value;
5666
+ }));
5667
+ else {
5668
+ if (result.issues.length) payload.issues.push(...prefixIssues(key, result.issues));
5669
+ payload.value[keyResult.value] = result.value;
5670
+ }
5671
+ }
5672
+ }
5673
+ if (proms.length) return Promise.all(proms).then(() => payload);
5674
+ return payload;
5675
+ };
5676
+ });
5566
5677
  const $ZodEnum = /*@__PURE__*/ $constructor("$ZodEnum", (inst, def) => {
5567
5678
  $ZodType.init(inst, def);
5568
5679
  const values = getEnumValues(def.entries);
@@ -6838,6 +6949,39 @@ const intersectionProcessor = (schema, ctx, json, params) => {
6838
6949
  const isSimpleIntersection = (val) => "allOf" in val && Object.keys(val).length === 1;
6839
6950
  json.allOf = [...isSimpleIntersection(a) ? a.allOf : [a], ...isSimpleIntersection(b) ? b.allOf : [b]];
6840
6951
  };
6952
+ const recordProcessor = (schema, ctx, _json, params) => {
6953
+ const json = _json;
6954
+ const def = schema._zod.def;
6955
+ json.type = "object";
6956
+ const keyType = def.keyType;
6957
+ const patterns = keyType._zod.bag?.patterns;
6958
+ if (def.mode === "loose" && patterns && patterns.size > 0) {
6959
+ const valueSchema = process$1(def.valueType, ctx, {
6960
+ ...params,
6961
+ path: [
6962
+ ...params.path,
6963
+ "patternProperties",
6964
+ "*"
6965
+ ]
6966
+ });
6967
+ json.patternProperties = {};
6968
+ for (const pattern of patterns) json.patternProperties[pattern.source] = valueSchema;
6969
+ } else {
6970
+ if (ctx.target === "draft-07" || ctx.target === "draft-2020-12") json.propertyNames = process$1(def.keyType, ctx, {
6971
+ ...params,
6972
+ path: [...params.path, "propertyNames"]
6973
+ });
6974
+ json.additionalProperties = process$1(def.valueType, ctx, {
6975
+ ...params,
6976
+ path: [...params.path, "additionalProperties"]
6977
+ });
6978
+ }
6979
+ const keyValues = keyType._zod.values;
6980
+ if (keyValues) {
6981
+ const validKeyValues = [...keyValues].filter((v) => typeof v === "string" || typeof v === "number");
6982
+ if (validKeyValues.length > 0) json.required = validKeyValues;
6983
+ }
6984
+ };
6841
6985
  const nullableProcessor = (schema, ctx, json, params) => {
6842
6986
  const def = schema._zod.def;
6843
6987
  const inner = process$1(def.innerType, ctx, params);
@@ -7533,6 +7677,27 @@ function intersection(left, right) {
7533
7677
  right
7534
7678
  });
7535
7679
  }
7680
+ const ZodRecord = /*@__PURE__*/ $constructor("ZodRecord", (inst, def) => {
7681
+ $ZodRecord.init(inst, def);
7682
+ ZodType.init(inst, def);
7683
+ inst._zod.processJSONSchema = (ctx, json, params) => recordProcessor(inst, ctx, json, params);
7684
+ inst.keyType = def.keyType;
7685
+ inst.valueType = def.valueType;
7686
+ });
7687
+ function record(keyType, valueType, params) {
7688
+ if (!valueType || !valueType._zod) return new ZodRecord({
7689
+ type: "record",
7690
+ keyType: string(),
7691
+ valueType: keyType,
7692
+ ...normalizeParams(valueType)
7693
+ });
7694
+ return new ZodRecord({
7695
+ type: "record",
7696
+ keyType,
7697
+ valueType,
7698
+ ...normalizeParams(params)
7699
+ });
7700
+ }
7536
7701
  const ZodEnum = /*@__PURE__*/ $constructor("ZodEnum", (inst, def) => {
7537
7702
  $ZodEnum.init(inst, def);
7538
7703
  ZodType.init(inst, def);
@@ -7863,6 +8028,146 @@ var ClientTransport = class ClientTransport {
7863
8028
  this.ws.close(code, reason);
7864
8029
  }
7865
8030
  };
8031
+ const AcpStreamPayloadSchema = discriminatedUnion("op", [
8032
+ object({
8033
+ op: literal("open"),
8034
+ /** The runtime command to start inside the sandbox, resolved in ITS filesystem. */
8035
+ command: string().min(1),
8036
+ args: array(string()),
8037
+ /** Complete child environment, decided by the daemon. */
8038
+ env: record(string(), string()),
8039
+ cwd: string().min(1).optional(),
8040
+ /** Env pointers the shim fills from its own PATH when unset (e.g. CLAUDE_CODE_EXECUTABLE). */
8041
+ hints: array(object({
8042
+ envVar: string().min(1),
8043
+ command: string().min(1)
8044
+ })).optional()
8045
+ }),
8046
+ object({
8047
+ op: literal("chunk"),
8048
+ /** base64: the frame is JSON text, the payload is opaque ND-JSON bytes. */
8049
+ data: string()
8050
+ }),
8051
+ object({
8052
+ op: literal("close"),
8053
+ /** Graceful stop deadline before the shim escalates to a kill. */
8054
+ deadlineMs: number().int().nonnegative().optional(),
8055
+ error: string().max(500).optional()
8056
+ })
8057
+ ]);
8058
+ discriminatedUnion("event", [object({
8059
+ event: literal("chunk"),
8060
+ data: string()
8061
+ }), object({
8062
+ event: literal("exit"),
8063
+ code: number().int().nullable(),
8064
+ signal: string().nullable()
8065
+ })]);
8066
+ //#endregion
8067
+ //#region src/shim/acp-runner.ts
8068
+ /**
8069
+ * Runs the ACP runtime inside the sandbox and relays its stdio.
8070
+ *
8071
+ * The relay is deliberately dumb: bytes out as chunk events, bytes in written to stdin,
8072
+ * exit reported once. ACP is a complete protocol already, so parsing it here would only add
8073
+ * a second place for it to go wrong — and the daemon's `AcpHost` is the half that speaks it.
8074
+ */
8075
+ var AcpRunner = class {
8076
+ deps;
8077
+ child;
8078
+ exited = false;
8079
+ constructor(deps) {
8080
+ this.deps = deps;
8081
+ }
8082
+ /** Handle one payload on the ACP stream. Returns once the payload is applied. */
8083
+ async apply(rawPayload) {
8084
+ const payload = AcpStreamPayloadSchema.parse(rawPayload);
8085
+ if (payload.op === "open") return this.open(payload);
8086
+ if (payload.op === "chunk") {
8087
+ const child = this.child;
8088
+ if (!child?.stdin) throw new Error("acp stream is not open");
8089
+ await new Promise((resolve, reject) => {
8090
+ child.stdin.write(Buffer.from(payload.data, "base64"), (err) => err ? reject(err) : resolve());
8091
+ });
8092
+ return;
8093
+ }
8094
+ await this.close(payload.deadlineMs ?? 5e3);
8095
+ }
8096
+ async open(payload) {
8097
+ if (this.child) throw new Error("acp stream is already open");
8098
+ const env = { ...payload.env };
8099
+ for (const hint of payload.hints ?? []) {
8100
+ if (env[hint.envVar]) continue;
8101
+ const resolved = this.deps.resolveCommand?.(hint.command, env);
8102
+ if (resolved) env[hint.envVar] = resolved;
8103
+ }
8104
+ const child = spawn(this.deps.resolveCommand?.(payload.command, env) ?? payload.command, payload.args, {
8105
+ stdio: [
8106
+ "pipe",
8107
+ "pipe",
8108
+ "inherit"
8109
+ ],
8110
+ env,
8111
+ ...payload.cwd ? { cwd: payload.cwd } : {},
8112
+ detached: process.platform !== "win32"
8113
+ });
8114
+ this.child = child;
8115
+ child.stdout?.on("data", (chunk) => this.deps.emit({
8116
+ kind: "chunk",
8117
+ data: Buffer.from(chunk).toString("base64")
8118
+ }));
8119
+ child.once("error", (err) => this.finish(null, null, err.message));
8120
+ child.once("exit", (code, signal) => this.finish(code, signal));
8121
+ if (!child.stdin || !child.stdout) throw new Error("acp runtime stdio is not piped");
8122
+ }
8123
+ finish(code, signal, error) {
8124
+ if (this.exited) return;
8125
+ this.exited = true;
8126
+ const child = this.child;
8127
+ if (child?.pid && process.platform !== "win32") try {
8128
+ process.kill(-child.pid, "SIGTERM");
8129
+ } catch {}
8130
+ this.deps.emit({
8131
+ kind: "exit",
8132
+ code: typeof code === "number" ? code : null,
8133
+ signal: typeof signal === "string" ? signal : null,
8134
+ ...error ? { error } : {}
8135
+ });
8136
+ }
8137
+ /** Graceful stop, escalating past the deadline — the same shape as the local driver. */
8138
+ async close(deadlineMs) {
8139
+ const child = this.child;
8140
+ this.child = void 0;
8141
+ if (!child) return;
8142
+ if (child.exitCode !== null || child.signalCode !== null) return;
8143
+ const kill = (signal) => {
8144
+ if (child.pid && process.platform !== "win32") try {
8145
+ process.kill(-child.pid, signal);
8146
+ return;
8147
+ } catch {}
8148
+ child.kill(signal);
8149
+ };
8150
+ try {
8151
+ child.stdin?.end();
8152
+ } catch {}
8153
+ kill("SIGTERM");
8154
+ await new Promise((resolve) => {
8155
+ let settled = false;
8156
+ const done = () => {
8157
+ if (settled) return;
8158
+ settled = true;
8159
+ clearTimeout(timer);
8160
+ resolve();
8161
+ };
8162
+ const timer = setTimeout(() => {
8163
+ this.deps.log?.warn(`acp runtime ignored SIGTERM after ${deadlineMs}ms — sending SIGKILL`);
8164
+ kill("SIGKILL");
8165
+ setTimeout(done, 2e3);
8166
+ }, deadlineMs);
8167
+ child.once("exit", done);
8168
+ });
8169
+ }
8170
+ };
7866
8171
  //#endregion
7867
8172
  //#region src/shim/protocol.ts
7868
8173
  /** WS subprotocol and path the shim dials the daemon on. */
@@ -7878,7 +8183,8 @@ const ShimCapabilitySchema = _enum([
7878
8183
  "materialize",
7879
8184
  "exec",
7880
8185
  "read",
7881
- "tunnel"
8186
+ "tunnel",
8187
+ "acp"
7882
8188
  ]);
7883
8189
  const ShimFrameSchema = discriminatedUnion("type", [
7884
8190
  object({
@@ -7925,6 +8231,20 @@ const ShimFrameSchema = discriminatedUnion("type", [
7925
8231
  ok: boolean(),
7926
8232
  payload: unknown().optional(),
7927
8233
  error: string().max(500).optional()
8234
+ }),
8235
+ object({
8236
+ type: literal("shim/event"),
8237
+ /** The request id that opened the stream. */
8238
+ streamId: string().uuid(),
8239
+ event: discriminatedUnion("kind", [object({
8240
+ kind: literal("chunk"),
8241
+ data: string()
8242
+ }), object({
8243
+ kind: literal("exit"),
8244
+ code: number().int().nullable(),
8245
+ signal: string().nullable(),
8246
+ error: string().max(500).optional()
8247
+ })])
7928
8248
  })
7929
8249
  ]);
7930
8250
  /** Parse an inbound frame, returning undefined rather than throwing: a malformed frame
@@ -7939,6 +8259,8 @@ function parseShimFrame(text) {
7939
8259
  }
7940
8260
  //#endregion
7941
8261
  //#region src/shim/client.ts
8262
+ /** Bounded so a long outage cannot grow the buffer without limit. */
8263
+ const MAX_BUFFERED_EVENTS = 2e3;
7942
8264
  /**
7943
8265
  * The in-sandbox arm. It holds no policy: it proves which pod it is, then executes the
7944
8266
  * operations the daemon authorizes. Everything it carries is short-lived and re-obtained
@@ -7950,6 +8272,12 @@ function parseShimFrame(text) {
7950
8272
  */
7951
8273
  var ShimClient = class {
7952
8274
  deps;
8275
+ /** ACP streams by the request id that opened them; each emits many events. */
8276
+ acpStreams = /* @__PURE__ */ new Map();
8277
+ /** Events produced while no transport is attached. A renewal closes the old socket before
8278
+ * the replacement binds, and ACP bytes cannot simply be dropped — losing a fragment
8279
+ * corrupts the protocol — so they wait here and flush on the next bind. */
8280
+ pendingEvents = [];
7953
8281
  transport;
7954
8282
  bound;
7955
8283
  stopped = false;
@@ -8084,6 +8412,7 @@ var ShimClient = class {
8084
8412
  if (frame.type === "shim/bound") {
8085
8413
  this.bound = frame;
8086
8414
  this.backoff.reset();
8415
+ this.flushPendingEvents(transport);
8087
8416
  if (!settled) {
8088
8417
  settled = true;
8089
8418
  this.deps.log?.info(`shim: bound as ${frame.agentId} generation ${frame.generation}`);
@@ -8107,6 +8436,89 @@ var ShimClient = class {
8107
8436
  }));
8108
8437
  });
8109
8438
  }
8439
+ /**
8440
+ * The ACP stream: one request opens it, then many events flow until the runtime exits.
8441
+ *
8442
+ * The opening request is still acknowledged with a single response so the caller knows the
8443
+ * runtime started (or why it did not); the recurring traffic goes out as `shim/event`
8444
+ * frames keyed by that request id, because one-shot correlation cannot express continuous
8445
+ * stdout and a terminal exit.
8446
+ */
8447
+ async serveAcp(transport, request) {
8448
+ const payload = request.payload;
8449
+ const streamId = payload?.op === "open" ? request.id : payload?.streamId ?? "";
8450
+ if (payload?.op === "open") {
8451
+ const runner = new AcpRunner({
8452
+ emit: (event) => this.emitEvent(streamId, event),
8453
+ ...this.deps.resolveCommand ? { resolveCommand: this.deps.resolveCommand } : {},
8454
+ ...this.deps.log ? { log: this.deps.log } : {}
8455
+ });
8456
+ this.acpStreams.set(streamId, runner);
8457
+ await runner.apply(request.payload);
8458
+ transport.send(JSON.stringify({
8459
+ type: "shim/response",
8460
+ id: request.id,
8461
+ ok: true,
8462
+ payload: { streamId }
8463
+ }));
8464
+ return;
8465
+ }
8466
+ const runner = this.acpStreams.get(streamId);
8467
+ if (!runner) {
8468
+ transport.send(JSON.stringify({
8469
+ type: "shim/response",
8470
+ id: request.id,
8471
+ ok: false,
8472
+ error: "unknown acp stream"
8473
+ }));
8474
+ return;
8475
+ }
8476
+ await runner.apply(request.payload);
8477
+ if (payload.op === "close") this.acpStreams.delete(streamId);
8478
+ transport.send(JSON.stringify({
8479
+ type: "shim/response",
8480
+ id: request.id,
8481
+ ok: true
8482
+ }));
8483
+ }
8484
+ /** Send an event on whichever transport is currently bound, or hold it until one is. */
8485
+ emitEvent(streamId, event) {
8486
+ const text = JSON.stringify({
8487
+ type: "shim/event",
8488
+ streamId,
8489
+ event
8490
+ });
8491
+ if (event.kind === "exit") this.acpStreams.delete(streamId);
8492
+ const transport = this.transport;
8493
+ if (transport && this.bound) {
8494
+ transport.send(text);
8495
+ return;
8496
+ }
8497
+ if (this.pendingEvents.length >= MAX_BUFFERED_EVENTS) {
8498
+ this.deps.log?.warn(`shim: buffered ${MAX_BUFFERED_EVENTS} events with no channel — failing stream ${streamId}`);
8499
+ this.pendingEvents.length = 0;
8500
+ this.acpStreams.get(streamId)?.close(0);
8501
+ this.acpStreams.delete(streamId);
8502
+ this.pendingEvents.push(JSON.stringify({
8503
+ type: "shim/event",
8504
+ streamId,
8505
+ event: {
8506
+ kind: "exit",
8507
+ code: null,
8508
+ signal: null,
8509
+ error: "channel unavailable too long"
8510
+ }
8511
+ }));
8512
+ return;
8513
+ }
8514
+ this.pendingEvents.push(text);
8515
+ }
8516
+ /** Flush events produced while the channel was down, oldest first. */
8517
+ flushPendingEvents(transport) {
8518
+ if (this.pendingEvents.length === 0) return;
8519
+ this.deps.log?.info(`shim: flushing ${this.pendingEvents.length} event(s) buffered across a rebind`);
8520
+ for (const text of this.pendingEvents.splice(0)) transport.send(text);
8521
+ }
8110
8522
  async serve(transport, request) {
8111
8523
  const bound = this.bound;
8112
8524
  if (!bound || request.sessionCredential !== bound.sessionCredential || request.generation !== bound.generation) {
@@ -8128,6 +8540,10 @@ var ShimClient = class {
8128
8540
  return;
8129
8541
  }
8130
8542
  try {
8543
+ if (request.capability === "acp") {
8544
+ await this.serveAcp(transport, request);
8545
+ return;
8546
+ }
8131
8547
  const payload = await (this.deps.handle ?? (async () => void 0))(request.capability, request.payload);
8132
8548
  transport.send(JSON.stringify({
8133
8549
  type: "shim/response",
@@ -8146,6 +8562,33 @@ var ShimClient = class {
8146
8562
  }
8147
8563
  };
8148
8564
  //#endregion
8565
+ //#region src/shim/path-resolve.ts
8566
+ /**
8567
+ * Resolve a command in THIS filesystem, which inside a sandbox is the only one that counts.
8568
+ *
8569
+ * Deliberately a local implementation rather than an import of the daemon's resolver: that
8570
+ * one pulls the runtime registry and curated catalog behind it, and the shim ships as a
8571
+ * single self-contained file with nothing but node builtins.
8572
+ */
8573
+ function resolveCommandInPath(command, env) {
8574
+ const executable = (candidate) => {
8575
+ try {
8576
+ if (!statSync(candidate).isFile()) return void 0;
8577
+ accessSync(candidate, constants.X_OK);
8578
+ return candidate;
8579
+ } catch {
8580
+ return;
8581
+ }
8582
+ };
8583
+ if (isAbsolute(command)) return executable(command);
8584
+ if (command.startsWith("./") || command.startsWith("../")) return executable(resolve(command));
8585
+ for (const dir of (env.PATH ?? "").split(delimiter)) {
8586
+ if (!dir) continue;
8587
+ const found = executable(join(dir, command));
8588
+ if (found) return found;
8589
+ }
8590
+ }
8591
+ //#endregion
8149
8592
  //#region src/shim/index.ts
8150
8593
  /** The in-sandbox shim executable. Lives at a fixed path in the runtime image
8151
8594
  * (`/opt/agentconnect/shim`), root-owned and read-only, with tini as PID 1. */
@@ -8165,6 +8608,7 @@ async function main() {
8165
8608
  subprotocol: opts.subprotocol,
8166
8609
  path: opts.path
8167
8610
  }),
8611
+ resolveCommand: resolveCommandInPath,
8168
8612
  log
8169
8613
  });
8170
8614
  for (const signal of ["SIGTERM", "SIGINT"]) process.on(signal, () => {