@themoltnet/agent-daemon 0.57.1 → 0.58.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +21 -0
  2. package/dist/cli.js +299 -37
  3. package/package.json +7 -7
package/README.md CHANGED
@@ -8,6 +8,27 @@ and finalization in both cases.
8
8
 
9
9
  ## Install
10
10
 
11
+ ### MoltNet Agent for Mac
12
+
13
+ On an Apple Silicon Mac running macOS 13 or newer, download the desktop app
14
+ from [themolt.net/download](https://themolt.net/download). Opening the app:
15
+
16
+ 1. installs or updates the publisher-verified Agent CLI bundle under
17
+ `~/.local/share/moltnet/agent`;
18
+ 2. asks before adding the per-user local HTTPS CA to the login Keychain;
19
+ 3. starts a foreground, supervised Agent Server; and
20
+ 4. opens Console for process-scoped pairing and runtime management.
21
+
22
+ Closing the status window hides it. **Quit and Stop Server** stops the owned
23
+ server process before the app exits. Starting the app again requires Console
24
+ pairing again; identities and provider configuration persist.
25
+
26
+ Agent CLI updates and desktop-app updates use independent signed channels and
27
+ always require consent. Removing the Agent CLI bundle preserves
28
+ `~/.config/moltnet`. Removing the local CA is a separate opt-in action.
29
+
30
+ ### Agent CLI
31
+
11
32
  Install the signed bundle, then use `moltnet-agent` for normal operation:
12
33
 
13
34
  ```bash
package/dist/cli.js CHANGED
@@ -27,7 +27,7 @@ import { BatchSpanProcessor } from "@opentelemetry/sdk-trace-base";
27
27
  import { NodeTracerProvider } from "@opentelemetry/sdk-trace-node";
28
28
  import { ATTR_SERVICE_NAME, ATTR_SERVICE_VERSION } from "@opentelemetry/semantic-conventions";
29
29
  import { homedir, platform, tmpdir } from "node:os";
30
- import { writePiConfig } from "@themoltnet/pi-runtime/pi-config";
30
+ import { PI_MODEL_MODALITIES, writePiConfig } from "@themoltnet/pi-runtime/pi-config";
31
31
  import { AsyncLocalStorage } from "node:async_hooks";
32
32
  import { mkdir, open, readFile, realpath, stat, writeFile } from "node:fs/promises";
33
33
  import { pipeline } from "node:stream/promises";
@@ -3653,10 +3653,15 @@ Options:
3653
3653
  Default: https://api.themolt.net.
3654
3654
  --heartbeat-interval-ms <n> Child reporter heartbeat cadence. Default: 60000.
3655
3655
  --warm-retention-sec <n> Child session/workspace retention. Default: 1800.
3656
+ --supervised Also stop gracefully when stdin reaches EOF.
3656
3657
 
3657
3658
  On macOS, the first interactive run asks to trust a per-user local CA in the
3658
- login keychain and serves HTTPS. Run \`agent-daemon server trust --remove\` to
3659
- remove that exact CA. Linux continues to use the Chromium PNA HTTP path.
3659
+ login keychain and serves HTTPS. Native supervisors use:
3660
+ server trust --status --json
3661
+ server trust --yes --json
3662
+ server trust --remove --yes --json
3663
+ Run \`agent-daemon server trust --remove\` interactively to remove that exact
3664
+ CA. Linux continues to use the Chromium PNA HTTP path.
3660
3665
  `;
3661
3666
  var PROVIDERS_HELP = `\
3662
3667
  moltnet-agent providers — manage local model providers.
@@ -3665,6 +3670,7 @@ Usage:
3665
3670
  moltnet-agent providers list [--json] [--root <path>]
3666
3671
  moltnet-agent providers set <id> [--base-url <url>] [--api <pi-api-kind>]
3667
3672
  [--model <id> ... | --clear-models]
3673
+ [--model-input <id>=text,image ...]
3668
3674
  [--api-key-stdin | --clear-api-key] [--root <path>]
3669
3675
  moltnet-agent providers discover <id> [--save] [--json] [--root <path>]
3670
3676
  moltnet-agent providers remove <id> [--yes] [--root <path>]
@@ -3675,6 +3681,11 @@ Usage:
3675
3681
  The default root is ~/.config/moltnet. MOLTNET_AGENT_SERVER_ROOT remains the
3676
3682
  environment override. API keys are accepted only from redirected stdin; they
3677
3683
  are stored separately and providers.json contains only a secret reference.
3684
+
3685
+ --model declares a text-only model. --model-input declares a model together
3686
+ with the input modalities it accepts, and is what makes a vision model usable:
3687
+ a model with no declared modalities is text-only to Pi, which drops image
3688
+ content parts before the request leaves the runtime.
3678
3689
  `;
3679
3690
  //#endregion
3680
3691
  //#region src/lib/identity-pin.ts
@@ -5424,6 +5435,17 @@ function resolveAgentServerRoot(input) {
5424
5435
  if (override) return override;
5425
5436
  return getConfigDir();
5426
5437
  }
5438
+ /**
5439
+ * Copy a model entry, dropping an empty `input` so it never reaches the wire.
5440
+ * Not a compatibility shim: `providers.json` has exactly one model shape, and
5441
+ * `validateProviders` rejects anything else on read.
5442
+ */
5443
+ function copyProviderModel(entry) {
5444
+ return {
5445
+ id: entry.id,
5446
+ ...entry.input && entry.input.length > 0 ? { input: [...entry.input] } : {}
5447
+ };
5448
+ }
5427
5449
  function providerEnvName(providerId) {
5428
5450
  return `MOLTNET_PROVIDER_${assertProviderId(providerId).replaceAll("-", "_").toUpperCase()}_API_KEY`;
5429
5451
  }
@@ -5622,6 +5644,7 @@ var AgentServerStore = class {
5622
5644
  for (const [id, provider] of Object.entries(state)) {
5623
5645
  assertProviderId(id);
5624
5646
  assertProviderEnvName(id, provider.envName);
5647
+ for (const model of provider.models ?? []) if (typeof model !== "object" || model === null || !model.id) throw new AgentServerStoreError("invalid_state", `provider "${id}" has a model entry that is not { id, input? }; rewrite providers.json entries as objects`);
5625
5648
  }
5626
5649
  }
5627
5650
  runDir(id) {
@@ -7791,14 +7814,41 @@ var AgentServerModelDiscoveryError = class extends Error {
7791
7814
  this.statusCode = statusCode;
7792
7815
  }
7793
7816
  };
7817
+ /**
7818
+ * The Ollama capability that means a model accepts image input. Ollama also
7819
+ * reports `completion`, `tools`, `thinking` and `embedding`; none of those map
7820
+ * onto a Pi input modality, so only this one is read.
7821
+ */
7822
+ var OLLAMA_VISION_CAPABILITY = "vision";
7823
+ /**
7824
+ * Read Ollama's `capabilities` array, when the endpoint supplies one.
7825
+ *
7826
+ * Returns `undefined` when the field is absent — which is meaningfully
7827
+ * different from "present and without vision". A local Ollama returns
7828
+ * capabilities from `/api/tags`; Ollama Cloud does not, and needs a per-model
7829
+ * `/api/show` probe. Only an explicit absence should trigger that probe.
7830
+ */
7831
+ function readOllamaModalities(value) {
7832
+ if (!isRecord(value) || !Array.isArray(value["capabilities"])) return void 0;
7833
+ return value["capabilities"].includes(OLLAMA_VISION_CAPABILITY) ? ["text", "image"] : [];
7834
+ }
7794
7835
  var ModelDiscoveryCollector = class {
7795
- models = /* @__PURE__ */ new Set();
7836
+ /**
7837
+ * Model id → declared input modalities. `undefined` means the id is known but
7838
+ * its capabilities are not, so it is still a probe candidate; `[]` means the
7839
+ * source answered and the model is text-only.
7840
+ */
7841
+ models = /* @__PURE__ */ new Map();
7842
+ record(id, input) {
7843
+ if (input === void 0 && this.models.has(id)) return;
7844
+ this.models.set(id, input);
7845
+ }
7796
7846
  addOpenAiResponse(value) {
7797
7847
  if (!isRecord(value) || !Array.isArray(value["data"])) return;
7798
7848
  for (const candidate of value["data"]) {
7799
7849
  if (!isRecord(candidate)) continue;
7800
7850
  const id = candidate["id"];
7801
- if (typeof id === "string" && id.length > 0) this.models.add(id);
7851
+ if (typeof id === "string" && id.length > 0) this.record(id, void 0);
7802
7852
  }
7803
7853
  }
7804
7854
  addOllamaResponse(value) {
@@ -7806,16 +7856,28 @@ var ModelDiscoveryCollector = class {
7806
7856
  for (const candidate of value["models"]) {
7807
7857
  if (!isRecord(candidate)) continue;
7808
7858
  const name = candidate["name"];
7809
- if (typeof name === "string" && name.length > 0) this.models.add(name);
7859
+ if (typeof name === "string" && name.length > 0) this.record(name, readOllamaModalities(candidate));
7810
7860
  }
7811
7861
  }
7862
+ /** Attach modalities learned after collection, e.g. from an `/api/show` probe. */
7863
+ setModalities(id, input) {
7864
+ if (this.models.has(id)) this.models.set(id, input);
7865
+ }
7812
7866
  get size() {
7813
7867
  return this.models.size;
7814
7868
  }
7815
7869
  result(providerId, failures) {
7816
7870
  if (this.models.size === 0) throw discoveryFailure(providerId, failures);
7871
+ const ids = [...this.models.keys()].sort().slice(0, 500);
7817
7872
  return {
7818
- models: [...this.models].sort().slice(0, 500),
7873
+ models: ids.map((id) => {
7874
+ const input = this.models.get(id);
7875
+ return input && input.length > 0 ? {
7876
+ id,
7877
+ input: [...input]
7878
+ } : { id };
7879
+ }),
7880
+ unresolved: ids.filter((id) => this.models.get(id) === void 0),
7819
7881
  discoveredCount: this.models.size
7820
7882
  };
7821
7883
  }
@@ -7876,6 +7938,12 @@ function safeErrorToken(value) {
7876
7938
  //#endregion
7877
7939
  //#region src/lib/provider-configuration.ts
7878
7940
  var DEFAULT_PROVIDER_API = "openai-completions";
7941
+ /**
7942
+ * Concurrent `/api/show` probes. Small on purpose: this runs against a
7943
+ * third-party endpoint on an operator's behalf, and discovery is interactive,
7944
+ * so the cap favours being a polite client over shaving a second.
7945
+ */
7946
+ var MODALITY_PROBE_CONCURRENCY = 5;
7879
7947
  var ProviderConfigurationError = class extends Error {
7880
7948
  name = "ProviderConfigurationError";
7881
7949
  constructor(code, message, statusCode, options) {
@@ -7913,7 +7981,7 @@ var ProviderConfigurationService = class {
7913
7981
  api: input.api ?? previous?.api ?? DEFAULT_PROVIDER_API,
7914
7982
  baseUrl,
7915
7983
  envName: assertProviderEnvName(providerId, input.envName ?? previous?.envName ?? providerEnvName(providerId)),
7916
- models: [...input.models ?? previous?.models ?? []],
7984
+ models: (input.models ?? previous?.models ?? []).map(copyProviderModel),
7917
7985
  ...!input.clearApiKey && previous?.apiKeyRef ? { apiKeyRef: previous.apiKeyRef } : {}
7918
7986
  };
7919
7987
  const key = `pi-provider/${providerId}`;
@@ -8014,13 +8082,65 @@ var ProviderConfigurationService = class {
8014
8082
  providerId,
8015
8083
  returnedCount: 500
8016
8084
  }, "Provider model discovery result was truncated");
8017
- if (options.save) await this.set(providerId, { models: result.models }, options);
8085
+ if (isOllamaProvider(providerId, parsed) && result.unresolved.length > 0) await this.resolveOllamaModalities({
8086
+ collector,
8087
+ headers,
8088
+ ids: result.unresolved,
8089
+ origin: parsed.origin,
8090
+ providerId,
8091
+ signal: options.signal
8092
+ });
8093
+ const resolved = collector.result(providerId, failures);
8094
+ const declared = new Map(provider.models.map((model) => [model.id, model.input]));
8095
+ const models = resolved.models.map((model) => {
8096
+ const override = declared.get(model.id);
8097
+ return override && override.length > 0 ? {
8098
+ id: model.id,
8099
+ input: [...override]
8100
+ } : model;
8101
+ });
8102
+ const detected = models.filter((model) => model.input?.includes("image") && !declared.get(model.id)?.includes("image"));
8103
+ if (detected.length > 0) this.logger.info({
8104
+ code: "agent_server_provider_discovery_modalities_detected",
8105
+ models: detected.map((model) => model.id),
8106
+ providerId
8107
+ }, "Provider models reported image input support");
8108
+ if (options.save) await this.set(providerId, { models }, options);
8018
8109
  this.logger.info({
8019
8110
  code: "agent_server_provider_discovery_completed",
8020
- modelCount: result.models.length,
8111
+ modelCount: models.length,
8021
8112
  providerId
8022
8113
  }, "Provider model discovery completed");
8023
- return { models: result.models };
8114
+ return { models };
8115
+ }
8116
+ /**
8117
+ * Fill in modalities Ollama Cloud's `/api/tags` omits, one `/api/show` per
8118
+ * still-unknown model.
8119
+ *
8120
+ * Failures here are deliberately not pushed into the discovery `failures`
8121
+ * array: that array decides the error code of a *failed* discovery, so a
8122
+ * probe rejection must not relabel an otherwise-successful one. A model whose
8123
+ * probe fails simply stays text-only.
8124
+ */
8125
+ async resolveOllamaModalities(input) {
8126
+ const url = `${input.origin}/api/show`;
8127
+ for (let index = 0; index < input.ids.length; index += MODALITY_PROBE_CONCURRENCY) {
8128
+ if (input.signal?.aborted) throw new ProviderConfigurationError("operation_aborted", `provider "${input.providerId}" discovery was cancelled`, 408, { cause: input.signal.reason });
8129
+ const batch = input.ids.slice(index, index + MODALITY_PROBE_CONCURRENCY);
8130
+ await Promise.all(batch.map(async (id) => {
8131
+ const modalities = readOllamaModalities(await this.requestDiscoveryEndpoint({
8132
+ body: { model: id },
8133
+ endpoint: "ollama_show",
8134
+ failures: [],
8135
+ headers: input.headers,
8136
+ method: "POST",
8137
+ providerId: input.providerId,
8138
+ signal: input.signal,
8139
+ url
8140
+ }));
8141
+ if (modalities) input.collector.setModalities(id, modalities);
8142
+ }));
8143
+ }
8024
8144
  }
8025
8145
  async resolveApiKey(providerId, provider) {
8026
8146
  if (!provider.apiKeyRef) return void 0;
@@ -8041,7 +8161,14 @@ var ProviderConfigurationService = class {
8041
8161
  let response;
8042
8162
  try {
8043
8163
  response = await this.fetchImpl(input.url, {
8044
- headers: input.headers,
8164
+ ...input.body ? {
8165
+ body: JSON.stringify(input.body),
8166
+ method: input.method
8167
+ } : {},
8168
+ headers: input.body ? {
8169
+ ...input.headers,
8170
+ "content-type": "application/json"
8171
+ } : input.headers,
8045
8172
  redirect: "error",
8046
8173
  signal: input.signal ? AbortSignal.any([input.signal, timeout]) : timeout
8047
8174
  });
@@ -8108,7 +8235,7 @@ function providerView(provider) {
8108
8235
  api: provider.api,
8109
8236
  baseUrl: provider.baseUrl,
8110
8237
  envName: provider.envName,
8111
- models: [...provider.models],
8238
+ models: provider.models.map(copyProviderModel),
8112
8239
  hasApiKey: Boolean(provider.apiKeyRef)
8113
8240
  };
8114
8241
  }
@@ -8130,6 +8257,31 @@ function providerAbortSource(reason) {
8130
8257
  }
8131
8258
  //#endregion
8132
8259
  //#region src/cli/providers.ts
8260
+ var MODEL_MODALITIES$1 = PI_MODEL_MODALITIES;
8261
+ /**
8262
+ * Build the model list from `--model <id>` (text-only) and
8263
+ * `--model-input <id>=text,image` (declares modalities). Model ids contain
8264
+ * colons, so `=` separates the id from its modality list. A `--model-input`
8265
+ * entry also declares the model, and overrides a bare `--model` for that id.
8266
+ */
8267
+ function parseModelArgs(models, modelInputs) {
8268
+ if (!models && !modelInputs) return void 0;
8269
+ const entries = /* @__PURE__ */ new Map();
8270
+ for (const id of models ?? []) entries.set(id, { id });
8271
+ for (const raw of modelInputs ?? []) {
8272
+ const separator = raw.indexOf("=");
8273
+ if (separator <= 0) throw new ProviderCliError("invalid_arguments", `--model-input expects <model-id>=<modality>[,<modality>], received "${raw}"`);
8274
+ const id = raw.slice(0, separator);
8275
+ const input = raw.slice(separator + 1).split(",").map((value) => value.trim()).filter((value) => value.length > 0);
8276
+ if (input.length === 0) throw new ProviderCliError("invalid_arguments", `--model-input for "${id}" declared no modality`);
8277
+ for (const modality of input) if (!MODEL_MODALITIES$1.includes(modality)) throw new ProviderCliError("invalid_arguments", `--model-input for "${id}" has unknown modality "${modality}"; expected ${MODEL_MODALITIES$1.join(" or ")}`);
8278
+ entries.set(id, {
8279
+ id,
8280
+ input
8281
+ });
8282
+ }
8283
+ return [...entries.values()];
8284
+ }
8133
8285
  async function runProviders(argv, dependencies = {}) {
8134
8286
  if (isHelpFlag(argv) || argv.length === 0) {
8135
8287
  (dependencies.stdout ?? console.log)(PROVIDERS_HELP);
@@ -8203,6 +8355,10 @@ function parseProviderArgs(command, args) {
8203
8355
  type: "string",
8204
8356
  multiple: true
8205
8357
  },
8358
+ "model-input": {
8359
+ type: "string",
8360
+ multiple: true
8361
+ },
8206
8362
  "clear-models": { type: "boolean" },
8207
8363
  "api-key-stdin": { type: "boolean" },
8208
8364
  "clear-api-key": { type: "boolean" }
@@ -8212,6 +8368,7 @@ function parseProviderArgs(command, args) {
8212
8368
  });
8213
8369
  requirePositionals(positionals, 1, "providers set <id>");
8214
8370
  if (values.model && values["clear-models"]) throw new ProviderCliError("invalid_arguments", "--model and --clear-models cannot be used together");
8371
+ if (values["model-input"] && values["clear-models"]) throw new ProviderCliError("invalid_arguments", "--model-input and --clear-models cannot be used together");
8215
8372
  if (values["api-key-stdin"] && values["clear-api-key"]) throw new ProviderCliError("invalid_arguments", "--api-key-stdin and --clear-api-key cannot be used together");
8216
8373
  return {
8217
8374
  command,
@@ -8219,7 +8376,7 @@ function parseProviderArgs(command, args) {
8219
8376
  providerId: positionals[0],
8220
8377
  baseUrl: values["base-url"],
8221
8378
  api: values.api,
8222
- models: values["clear-models"] ? [] : values.model,
8379
+ models: values["clear-models"] ? [] : parseModelArgs(values.model, values["model-input"]),
8223
8380
  apiKeyStdin: values["api-key-stdin"] ?? false,
8224
8381
  clearApiKey: values["clear-api-key"] ?? false
8225
8382
  };
@@ -8374,7 +8531,7 @@ async function discoverProvider(context, parsed) {
8374
8531
  signal: context.signal
8375
8532
  });
8376
8533
  if (parsed.json) context.stdout(JSON.stringify(result));
8377
- else for (const model of result.models) context.stdout(model);
8534
+ else for (const model of result.models) context.stdout(model.input && model.input.length > 0 ? `${model.id}\t${model.input.join(",")}` : model.id);
8378
8535
  return 0;
8379
8536
  }
8380
8537
  async function removeProvider(context, parsed) {
@@ -10371,6 +10528,13 @@ function writeRegistry(path, entries) {
10371
10528
  //#region src/lib/agent-server/protocol.ts
10372
10529
  var DateTime = Type.String({ format: "date-time" });
10373
10530
  var StringList = Type.Array(Type.String());
10531
+ /** A model the provider offers, with the input modalities it accepts. */
10532
+ var ProviderModelSchema = Type.Object({
10533
+ id: Type.String(),
10534
+ input: Type.Optional(Type.Array(Type.Union(PI_MODEL_MODALITIES.map((modality) => Type.Literal(modality))), { minItems: 1 }))
10535
+ });
10536
+ /** One shape on the wire, for both requests and responses. */
10537
+ var ProviderModelList = Type.Array(ProviderModelSchema);
10374
10538
  function schemaRef(schema) {
10375
10539
  const id = schema.$id;
10376
10540
  if (typeof id !== "string" || id.length === 0) throw new Error("Agent Server protocol schemas must have an identifier");
@@ -10403,7 +10567,7 @@ var AgentServerProviderSchema = Type.Object({
10403
10567
  api: Type.String(),
10404
10568
  baseUrl: Type.String({ format: "uri" }),
10405
10569
  envName: Type.String(),
10406
- models: StringList,
10570
+ models: ProviderModelList,
10407
10571
  hasApiKey: Type.Boolean()
10408
10572
  }, { $id: "AgentServerProvider" });
10409
10573
  var AgentServerRunRecordSchema = Type.Object({
@@ -10480,10 +10644,10 @@ var PutProviderSchema = Type.Object({
10480
10644
  api: Type.String(),
10481
10645
  baseUrl: Type.String({ format: "uri" }),
10482
10646
  envName: Type.String(),
10483
- models: StringList,
10647
+ models: ProviderModelList,
10484
10648
  apiKey: Type.Optional(Type.String())
10485
10649
  });
10486
- var DiscoverModelsSchema = Type.Object({ models: StringList }, { $id: "DiscoveredModels" });
10650
+ var DiscoverModelsSchema = Type.Object({ models: ProviderModelList }, { $id: "DiscoveredModels" });
10487
10651
  var StartRunSchema = Type.Object({
10488
10652
  agent: Type.String(),
10489
10653
  teamId: Type.String(),
@@ -10798,6 +10962,27 @@ function stringArray(body, field, options = {}) {
10798
10962
  if (!Array.isArray(value) || !options.allowEmpty && value.length === 0 || value.some((item) => typeof item !== "string" || item.length === 0)) throw new AgentServerHttpError(400, "invalid_body", `"${field}" must be ${options.allowEmpty ? "a" : "a non-empty"} string array`);
10799
10963
  return value;
10800
10964
  }
10965
+ var MODEL_MODALITIES = new Set(PI_MODEL_MODALITIES);
10966
+ /** Parse the provider `models` field: `{ id, input? }` entries only. */
10967
+ function modelArray(body, field) {
10968
+ const value = body[field];
10969
+ const invalid = (detail) => {
10970
+ throw new AgentServerHttpError(400, "invalid_body", detail);
10971
+ };
10972
+ if (!Array.isArray(value)) return invalid(`"${field}" must be an array of { id, input? } entries`);
10973
+ return value.map((item) => {
10974
+ if (typeof item !== "object" || item === null || Array.isArray(item)) return invalid(`"${field}" entries must be an { id, input? } object`);
10975
+ const entry = item;
10976
+ const id = entry.id;
10977
+ if (typeof id !== "string" || id.length === 0) return invalid(`"${field}" entries must carry a non-empty "id"`);
10978
+ if (entry.input === void 0) return { id };
10979
+ if (!Array.isArray(entry.input) || entry.input.length === 0 || entry.input.some((modality) => typeof modality !== "string" || !MODEL_MODALITIES.has(modality))) return invalid(`"${field}" entry "${id}" must declare "input" as a non-empty array of "text" or "image"`);
10980
+ return {
10981
+ id,
10982
+ input: entry.input
10983
+ };
10984
+ });
10985
+ }
10801
10986
  function requestOperationSignal(request, shutdownSignal) {
10802
10987
  const disconnected = new AbortController();
10803
10988
  if (request.raw.aborted) disconnected.abort({ source: "request" });
@@ -11018,7 +11203,7 @@ function registerProviderRoutes(app, options, requirePairedOrigin) {
11018
11203
  api: requireString(body, "api"),
11019
11204
  baseUrl: requireString(body, "baseUrl"),
11020
11205
  envName: requireString(body, "envName"),
11021
- models: stringArray(body, "models", { allowEmpty: true }),
11206
+ models: modelArray(body, "models"),
11022
11207
  ...optionalString(body, "apiKey") ? { apiKey: optionalString(body, "apiKey") } : {}
11023
11208
  });
11024
11209
  return reply.code(200).send(entry);
@@ -11423,6 +11608,7 @@ async function runAgentServer(argv) {
11423
11608
  const trustRequested = argv[0] === "trust";
11424
11609
  const commandArgs = trustRequested ? argv.slice(1) : argv;
11425
11610
  const envConfig = loadAgentServerEnvConfig();
11611
+ if (trustRequested) return runTrustCommand(commandArgs, resolveAgentServerRoot({ root: envConfig.root }));
11426
11612
  const { values } = parseArgs({
11427
11613
  args: commandArgs,
11428
11614
  options: {
@@ -11432,7 +11618,7 @@ async function runAgentServer(argv) {
11432
11618
  "api-url": { type: "string" },
11433
11619
  "heartbeat-interval-ms": { type: "string" },
11434
11620
  "warm-retention-sec": { type: "string" },
11435
- remove: { type: "boolean" }
11621
+ supervised: { type: "boolean" }
11436
11622
  }
11437
11623
  });
11438
11624
  const port = Number.parseInt(values.port ?? (envConfig.port || `${DEFAULT_PORT}`), 10);
@@ -11445,7 +11631,6 @@ async function runAgentServer(argv) {
11445
11631
  const defaultApiUrl = values["api-url"] ?? (envConfig.apiUrl || DEFAULT_API_URL);
11446
11632
  const runtimeSettings = parseLocalOperationalSettings(values);
11447
11633
  const store = new AgentServerStore(root).ensure();
11448
- if (trustRequested) return runTrustCommand(commandArgs, root);
11449
11634
  const { logger, shutdown: shutdownLogger } = createRootLogger({
11450
11635
  name: "agent-daemon.server",
11451
11636
  level: envConfig.logLevel || "info"
@@ -11512,7 +11697,7 @@ async function runAgentServer(argv) {
11512
11697
  console.error(`config root: ${root}`);
11513
11698
  console.error(`allowed origins: ${allowedOrigins.join(", ")}`);
11514
11699
  console.error("Pair from the Console \"Local runtime\" page; approve the one-click prompt this server opens.");
11515
- return await waitForAgentServerShutdown(runs, app, shutdownController);
11700
+ return await waitForAgentServerShutdown(runs, app, shutdownController, Boolean(values.supervised));
11516
11701
  } catch (cause) {
11517
11702
  await app.close().catch(() => void 0);
11518
11703
  throw cause;
@@ -11533,19 +11718,80 @@ async function runAgentServer(argv) {
11533
11718
  await shutdownLogger();
11534
11719
  }
11535
11720
  }
11536
- async function runTrustCommand(argv, root) {
11537
- if (!isMacos()) {
11538
- console.error("Local HTTPS trust setup is currently supported on macOS only.");
11539
- return 1;
11540
- }
11541
- if (argv.includes("--remove")) {
11542
- await removeLocalCa(root);
11543
- console.error("Removed the MoltNet local CA from your login keychain.");
11721
+ async function runTrustCommand(argv, defaultRoot) {
11722
+ try {
11723
+ const { values } = parseArgs({
11724
+ args: argv,
11725
+ options: {
11726
+ root: { type: "string" },
11727
+ remove: { type: "boolean" },
11728
+ status: { type: "boolean" },
11729
+ yes: { type: "boolean" },
11730
+ json: { type: "boolean" }
11731
+ }
11732
+ });
11733
+ const root = values.root ?? defaultRoot;
11734
+ const statusRequested = Boolean(values.status);
11735
+ const removeRequested = Boolean(values.remove);
11736
+ const yes = Boolean(values.yes);
11737
+ const json = Boolean(values.json);
11738
+ if (statusRequested && (removeRequested || yes)) {
11739
+ console.error("Usage: moltnet-agent server trust --status [--json]");
11740
+ return 1;
11741
+ }
11742
+ if (!isMacos()) {
11743
+ if (json) {
11744
+ printTrustStatus({
11745
+ supported: false,
11746
+ trusted: false,
11747
+ fingerprint: null
11748
+ });
11749
+ return 0;
11750
+ }
11751
+ console.error("Local HTTPS trust setup is currently supported on macOS only.");
11752
+ return 1;
11753
+ }
11754
+ const material = await ensureLocalTlsMaterial(root);
11755
+ if (statusRequested) {
11756
+ const trusted = await isLocalCaTrusted(root);
11757
+ if (json) printTrustStatus({
11758
+ supported: true,
11759
+ trusted,
11760
+ fingerprint: material.fingerprint
11761
+ });
11762
+ else console.log(trusted ? `MoltNet local CA ${material.fingerprint} is trusted.` : `MoltNet local CA ${material.fingerprint} is not trusted.`);
11763
+ return 0;
11764
+ }
11765
+ if (json && !yes) {
11766
+ console.error("Machine-readable trust changes require --yes after native app consent.");
11767
+ return 1;
11768
+ }
11769
+ if (removeRequested) {
11770
+ await removeLocalCa(root);
11771
+ if (json) printTrustStatus({
11772
+ supported: true,
11773
+ trusted: false,
11774
+ fingerprint: material.fingerprint
11775
+ });
11776
+ else console.log("Removed the MoltNet local CA from your login keychain.");
11777
+ return 0;
11778
+ }
11779
+ if (yes) await trustLocalCa(root);
11780
+ else await ensureTrustedLocalTls(root);
11781
+ if (json) printTrustStatus({
11782
+ supported: true,
11783
+ trusted: await isLocalCaTrusted(root),
11784
+ fingerprint: material.fingerprint
11785
+ });
11786
+ else console.log("MoltNet local HTTPS trust is ready for this macOS user.");
11544
11787
  return 0;
11788
+ } catch (cause) {
11789
+ console.error(`Agent Server trust command failed: ${cause instanceof Error ? cause.message : String(cause)}`);
11790
+ return 1;
11545
11791
  }
11546
- await ensureTrustedLocalTls(root);
11547
- console.error("MoltNet local HTTPS trust is ready for this macOS user.");
11548
- return 0;
11792
+ }
11793
+ function printTrustStatus(status) {
11794
+ console.log(JSON.stringify(status));
11549
11795
  }
11550
11796
  async function ensureTrustedLocalTls(root) {
11551
11797
  const material = await ensureLocalTlsMaterial(root);
@@ -11564,12 +11810,13 @@ async function ensureTrustedLocalTls(root) {
11564
11810
  await trustLocalCa(root);
11565
11811
  return material;
11566
11812
  }
11567
- function waitForAgentServerShutdown(runs, app, shutdownController) {
11813
+ function waitForAgentServerShutdown(runs, app, shutdownController, supervised) {
11568
11814
  return new Promise((resolvePromise) => {
11569
11815
  let shuttingDown = false;
11570
- const shutdown = () => {
11816
+ const shutdown = (source) => {
11571
11817
  if (shuttingDown) return;
11572
11818
  shuttingDown = true;
11819
+ if (source === "stdin") console.error("shutting down: stdin EOF");
11573
11820
  shutdownController.abort({ source: "shutdown" });
11574
11821
  (async () => {
11575
11822
  app.server.closeAllConnections();
@@ -11595,16 +11842,31 @@ function waitForAgentServerShutdown(runs, app, shutdownController) {
11595
11842
  const failures = results.filter((result) => result.status === "rejected");
11596
11843
  for (const failure of failures) console.error(`shutdown cleanup failed: ${failure.reason.message}`);
11597
11844
  handlers.dispose();
11845
+ stdinGuard.dispose();
11598
11846
  const exitCode = typeof process.exitCode === "number" ? process.exitCode : 0;
11599
11847
  resolvePromise(failures.length > 0 ? 1 : exitCode);
11600
11848
  })();
11601
11849
  };
11602
11850
  const handlers = installShutdownSignalHandlers({
11603
11851
  logDrain: () => console.error("shutting down: stopping runs…"),
11604
- drain: shutdown
11852
+ drain: () => shutdown()
11853
+ });
11854
+ const stdinGuard = installSupervisedStdinGuard({
11855
+ enabled: supervised,
11856
+ shutdown: () => shutdown("stdin")
11605
11857
  });
11606
11858
  });
11607
11859
  }
11860
+ /** Makes the supervising process's stdin pipe part of the server lifecycle. */
11861
+ function installSupervisedStdinGuard(options) {
11862
+ if (!options.enabled) return { dispose: () => void 0 };
11863
+ const input = options.input ?? process.stdin;
11864
+ const onEnd = () => options.shutdown();
11865
+ input.once("end", onEnd);
11866
+ input.resume();
11867
+ if (input.readableEnded) queueMicrotask(onEnd);
11868
+ return { dispose: () => input.off("end", onEnd) };
11869
+ }
11608
11870
  //#endregion
11609
11871
  //#region src/lib/runtime-session-sync.ts
11610
11872
  async function syncRuntimeSessions(deps, input) {
@@ -11942,7 +12204,7 @@ async function writeCache(cache) {
11942
12204
  }
11943
12205
  //#endregion
11944
12206
  //#region src/version.ts
11945
- var DAEMON_VERSION = "0.57.1";
12207
+ var DAEMON_VERSION = "0.58.0";
11946
12208
  //#endregion
11947
12209
  //#region src/cli.ts
11948
12210
  async function runAgentDaemonCli(options) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@themoltnet/agent-daemon",
3
- "version": "0.57.1",
3
+ "version": "0.58.0",
4
4
  "license": "AGPL-3.0-only",
5
5
  "type": "module",
6
6
  "description": "Universal MoltNet agent daemon host with a built-in Pi/Gondolin runtime and support for trusted operator-owned runtime modules. CLI: moltnet-agent.",
@@ -86,8 +86,8 @@
86
86
  "reflect-metadata": "^0.2.2",
87
87
  "typebox": "^1.2.8",
88
88
  "@themoltnet/agent-runtime": "1.0.3",
89
+ "@themoltnet/pi-runtime": "0.17.0",
89
90
  "@themoltnet/sdk": "0.142.0",
90
- "@themoltnet/pi-runtime": "0.16.0",
91
91
  "@themoltnet/os-keyring": "0.3.0"
92
92
  },
93
93
  "devDependencies": {
@@ -98,15 +98,15 @@
98
98
  "vite": "^8.0.0",
99
99
  "vite-plugin-dts": "^4.5.4",
100
100
  "vitest": "^3.0.0",
101
- "@moltnet/api-client": "0.1.0",
102
101
  "@moltnet/agent-eval": "0.1.0",
102
+ "@moltnet/api-client": "0.1.0",
103
+ "@moltnet/loopback-companion": "0.1.0",
104
+ "@moltnet/execution-plan": "0.1.0",
105
+ "@moltnet/bootstrap": "0.1.0",
106
+ "@moltnet/models": "0.1.0",
103
107
  "@moltnet/execution-integrations": "0.1.0",
104
108
  "@moltnet/crypto-service": "0.1.0",
105
- "@moltnet/execution-plan": "0.1.0",
106
- "@moltnet/loopback-companion": "0.1.0",
107
109
  "@moltnet/observability": "0.1.0",
108
- "@moltnet/models": "0.1.0",
109
- "@moltnet/bootstrap": "0.1.0",
110
110
  "@moltnet/runtime-profiles": "0.1.0",
111
111
  "@moltnet/tasks": "0.1.0"
112
112
  },