@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.
- package/README.md +21 -0
- package/dist/cli.js +299 -37
- 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.
|
|
3659
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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:
|
|
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:
|
|
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 (
|
|
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:
|
|
8111
|
+
modelCount: models.length,
|
|
8021
8112
|
providerId
|
|
8022
8113
|
}, "Provider model discovery completed");
|
|
8023
|
-
return { 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
|
-
|
|
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:
|
|
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:
|
|
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:
|
|
10647
|
+
models: ProviderModelList,
|
|
10484
10648
|
apiKey: Type.Optional(Type.String())
|
|
10485
10649
|
});
|
|
10486
|
-
var DiscoverModelsSchema = Type.Object({ models:
|
|
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:
|
|
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
|
-
|
|
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,
|
|
11537
|
-
|
|
11538
|
-
|
|
11539
|
-
|
|
11540
|
-
|
|
11541
|
-
|
|
11542
|
-
|
|
11543
|
-
|
|
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
|
-
|
|
11547
|
-
|
|
11548
|
-
|
|
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.
|
|
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.
|
|
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
|
},
|