@themoltnet/agent-daemon 0.61.0 → 0.63.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 +37 -10
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +296 -395
- package/package.json +10 -10
package/README.md
CHANGED
|
@@ -15,17 +15,18 @@ from [themolt.net/download](https://themolt.net/download). Opening the app:
|
|
|
15
15
|
|
|
16
16
|
1. installs or updates the publisher-verified Agent CLI bundle under
|
|
17
17
|
`~/.local/share/moltnet/agent`;
|
|
18
|
-
2.
|
|
19
|
-
|
|
20
|
-
|
|
18
|
+
2. starts a foreground, supervised Agent Server over a private native socket;
|
|
19
|
+
and
|
|
20
|
+
3. opens the browser only when OAuth approval is required.
|
|
21
21
|
|
|
22
22
|
Closing the status window hides it. **Quit and Stop Server** stops the owned
|
|
23
|
-
server process before the app exits.
|
|
24
|
-
operator, identities, and
|
|
23
|
+
server process before the app exits. Desktop re-establishes its private native
|
|
24
|
+
control channel after a server restart. The local operator, identities, and
|
|
25
|
+
provider configuration persist.
|
|
25
26
|
|
|
26
27
|
Agent CLI updates and desktop-app updates use independent signed channels and
|
|
27
28
|
always require consent. Removing the Agent CLI bundle preserves
|
|
28
|
-
`~/.config/moltnet`.
|
|
29
|
+
`~/.config/moltnet`.
|
|
29
30
|
|
|
30
31
|
### Agent CLI
|
|
31
32
|
|
|
@@ -123,8 +124,8 @@ For config-based runs, the agent's `moltnet.json` lives in the central store
|
|
|
123
124
|
(`~/.config/moltnet/identities/<agent>/`) or, for an external config, next to
|
|
124
125
|
its gitconfig in `.moltnet/<agent>/`. Three paths create it:
|
|
125
126
|
|
|
126
|
-
-
|
|
127
|
-
|
|
127
|
+
- MoltNet Agent creates a managed agent through the Agent Server: keypair and
|
|
128
|
+
agent key generated on this machine, stored under the
|
|
128
129
|
store's `secrets/` directory, no CLI involved.
|
|
129
130
|
- [`moltnet register`](../../docs/start/install-and-initialize.md#register-an-agent)
|
|
130
131
|
creates an OAuth2 identity from the CLI; add a stored agent key with
|
|
@@ -209,7 +210,7 @@ carries them -- read access to the agent's teams and their diaries, and
|
|
|
209
210
|
enrollment into a team. Startup does not check for them: a key without them
|
|
210
211
|
claims and runs work, and the daemon reports which are absent.
|
|
211
212
|
|
|
212
|
-
|
|
213
|
+
Desktop selects the full set by default. Knowledge-enabled workers must add
|
|
213
214
|
`diary:write`, `pack:read`, and `pack:write` when the key is issued.
|
|
214
215
|
|
|
215
216
|
`crypto:sign` is in the minimum because host-capability signing runs on the
|
|
@@ -648,10 +649,36 @@ Server configuration root. **Reset to release defaults** clears those overrides
|
|
|
648
649
|
when applied; it does not delete agent credentials. Launch environment values
|
|
649
650
|
have priority and appear read-only. Operators who explicitly set both
|
|
650
651
|
`MOLTNET_OPERATOR_API_URL` and `MOLTNET_OPERATOR_OAUTH_ISSUER` should also use
|
|
651
|
-
`
|
|
652
|
+
`MOLTNET_HOME` to select a dedicated environment root.
|
|
652
653
|
|
|
653
654
|
Release defaults use `https://api.themolt.net`, `https://auth.themolt.net`, and
|
|
654
655
|
public client IDs `moltnet-native` / `moltnet-console`. Hosted client registration
|
|
655
656
|
and coordinated configuration are tracked in
|
|
656
657
|
[moltnet-operations #8](https://github.com/getlarge/moltnet-operations/issues/8).
|
|
657
658
|
These IDs are public configuration; they do not contain a client secret.
|
|
659
|
+
|
|
660
|
+
### Isolated local development
|
|
661
|
+
|
|
662
|
+
Use the development targets to keep development state and installations in
|
|
663
|
+
separate directories:
|
|
664
|
+
|
|
665
|
+
```bash
|
|
666
|
+
pnpm exec nx run @moltnet/agent-desktop:tauri:dev
|
|
667
|
+
pnpm exec nx run @themoltnet/agent-daemon:dev -- server
|
|
668
|
+
```
|
|
669
|
+
|
|
670
|
+
The launchers choose `~/.local/share/moltnet/development/<worktree-id>/store`
|
|
671
|
+
and the sibling `agent` installation directory. The worktree ID is a stable
|
|
672
|
+
hash of the canonical checkout path. Repeated launches reuse that environment;
|
|
673
|
+
another worktree gets its own environment. Use `MOLTNET_DEV_HOME` and `MOLTNET_DEV_AGENT_HOME` to override these development
|
|
674
|
+
locations. Use absolute overrides: relative paths start at the Nx target’s
|
|
675
|
+
working directory (`apps/agent-daemon` for daemon `dev`, the repository root
|
|
676
|
+
for Desktop `tauri:dev`). All daemon `dev` commands, including `poll`, use
|
|
677
|
+
this isolated store.
|
|
678
|
+
The launchers ignore inherited production store and installation selectors.
|
|
679
|
+
The renderer-only Desktop `dev` target starts Vite without a native server.
|
|
680
|
+
|
|
681
|
+
For direct CLI or SDK development commands, set `MOLTNET_HOME` explicitly. See
|
|
682
|
+
[Store selection and keyring namespaces](../../docs/reference/agent-configuration.md#store-selection-and-keyring-namespaces)
|
|
683
|
+
for precedence, secret namespaces and daemon discovery. Automated tests use fresh
|
|
684
|
+
temporary roots.
|
package/dist/cli.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"AAaA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,cAAc,CAAC;AAGzD,wBAAsB,iBAAiB,CAAC,OAAO,EAAE;IAC/C,OAAO,EAAE,oBAAoB,CAAC;IAC9B,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;CACjB,GAAG,OAAO,CAAC,MAAM,CAAC,
|
|
1
|
+
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"AAaA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,cAAc,CAAC;AAGzD,wBAAsB,iBAAiB,CAAC,OAAO,EAAE;IAC/C,OAAO,EAAE,oBAAoB,CAAC;IAC9B,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;CACjB,GAAG,OAAO,CAAC,MAAM,CAAC,CA+ClB"}
|
package/dist/cli.js
CHANGED
|
@@ -11,12 +11,12 @@ import "typebox/value";
|
|
|
11
11
|
import { execFile, spawn } from "node:child_process";
|
|
12
12
|
import { basename, dirname, isAbsolute, join, resolve, sep } from "node:path";
|
|
13
13
|
import { parseArgs, parseEnv, promisify } from "node:util";
|
|
14
|
-
import { CredentialPersistenceError, EnrollmentRecoveryError, FILE_SECRET_PROVIDER, FileSecretProvider, ProjectConfigError, ProvisioningNotStartedError, RegisterIdentityError, WORKSPACE_STRATEGIES, boundedIdentitySignal, canonicalDirectory, connect, createNodeSecretProviderRegistry, enrollTeam, getProjectConfigPath, readProjectConfig, register, resolveProjectBinding } from "@themoltnet/sdk/node";
|
|
14
|
+
import { CredentialPersistenceError, EnrollmentRecoveryError, FILE_SECRET_PROVIDER, FileSecretProvider, ProjectConfigError, ProvisioningNotStartedError, RegisterIdentityError, WORKSPACE_STRATEGIES, boundedIdentitySignal, canonicalDirectory, canonicalStoreRoot, connect, createNodeSecretProviderRegistry, defaultStoreRoot, enrollTeam, getProjectConfigPath, isDefaultStore, readProjectConfig, register, resolveProjectBinding, resolveStoreRoot, resolveStoreSelection } from "@themoltnet/sdk/node";
|
|
15
15
|
import { constants, copyFileSync, createReadStream, createWriteStream, existsSync, lstatSync, mkdirSync, mkdtempSync, openSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, statSync, symlinkSync, writeFileSync } from "node:fs";
|
|
16
16
|
import { AuthenticationError, IDENTITY_ALIAS_PATTERN, MoltNetError, assertIdentityAlias, assertTrustedConfigApiUrl, createExecutorAttestor, formatSecretReferenceString, getConfigDir, getIdentityDir, hasAgentKeyConfiguration, isCanonicalConfig, parseSecretReferenceString, readConfig, requireSecureCredentialApiUrl, resolveAgentKey, resolveEnvSecretReference, resolveIdentitySeed, selectAgentKeyReference, signBytes } from "@themoltnet/sdk";
|
|
17
17
|
import { AgentRuntime, ApiTaskReporter, ApiTaskSource, PollingApiTaskSource, RuntimeProfilePrerequisiteError, createLocalSeedSigner, resolveAgentIdentity, resolveRuntimeProfile, resolveRuntimeProfiles, validateRuntimeProfilePrerequisites } from "@themoltnet/agent-runtime";
|
|
18
18
|
import { GuestEnvironmentBoundaryError, assertGuestEnvironmentBoundary, createPiRetryTriage, findMainWorktree, isResolvedPathInsideRoot, normalizeRetryTriageResult, redactRetryTriageSecrets, resolveRuntimeProfileModel } from "@themoltnet/pi-runtime";
|
|
19
|
-
import {
|
|
19
|
+
import { createHash, randomBytes, randomUUID, timingSafeEqual } from "node:crypto";
|
|
20
20
|
import { once } from "node:events";
|
|
21
21
|
import { metrics } from "@opentelemetry/api";
|
|
22
22
|
import { OTLPMetricExporter } from "@opentelemetry/exporter-metrics-otlp-proto";
|
|
@@ -28,7 +28,7 @@ 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
30
|
import { PI_MODEL_MODALITIES, writePiConfig } from "@themoltnet/pi-runtime/pi-config";
|
|
31
|
-
import { mkdir, open, readFile, readdir, realpath,
|
|
31
|
+
import { chmod, lstat, mkdir, open, readFile, readdir, realpath, stat, writeFile } from "node:fs/promises";
|
|
32
32
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
33
33
|
import { pipeline } from "node:stream/promises";
|
|
34
34
|
import { createInterface } from "node:readline/promises";
|
|
@@ -45,8 +45,6 @@ import { pathToFileURL } from "node:url";
|
|
|
45
45
|
import { StringDecoder } from "node:string_decoder";
|
|
46
46
|
import rateLimit from "@fastify/rate-limit";
|
|
47
47
|
import Fastify from "fastify";
|
|
48
|
-
import "reflect-metadata";
|
|
49
|
-
import { BasicConstraintsExtension, ExtendedKeyUsage, ExtendedKeyUsageExtension, IP, KeyUsageFlags, KeyUsagesExtension, SubjectAlternativeNameExtension, X509CertificateGenerator } from "@peculiar/x509";
|
|
50
48
|
import { createGzip } from "node:zlib";
|
|
51
49
|
//#region ../../libs/tasks/src/rubric.ts
|
|
52
50
|
/**
|
|
@@ -3759,6 +3757,10 @@ async function resolveSelectionApiUrl(agentName, options) {
|
|
|
3759
3757
|
* here, so the rest of the daemon imports typed values rather than
|
|
3760
3758
|
* sprinkling string lookups across the codebase.
|
|
3761
3759
|
*/
|
|
3760
|
+
/** All daemon commands announce the same legacy alias policy as Go and Desktop. */
|
|
3761
|
+
function legacyStoreNotice() {
|
|
3762
|
+
return process.env["MOLTNET_AGENT_SERVER_ROOT"] !== void 0 ? "MOLTNET_AGENT_SERVER_ROOT is deprecated; use MOLTNET_HOME. Both select the entire store; no data is migrated." : void 0;
|
|
3763
|
+
}
|
|
3762
3764
|
function loadConfig() {
|
|
3763
3765
|
assertSingleCredentialForm("MOLTNET_AGENT_KEY", "MOLTNET_AGENT_KEY_REF");
|
|
3764
3766
|
assertSingleCredentialForm("MOLTNET_PRIVATE_KEY", "MOLTNET_PRIVATE_KEY_REF");
|
|
@@ -3769,7 +3771,7 @@ function loadConfig() {
|
|
|
3769
3771
|
profilePrerequisiteEnv: process.env,
|
|
3770
3772
|
profilePrerequisitePath: process.env.PATH ?? "",
|
|
3771
3773
|
piCodingAgentDir: process.env["PI_CODING_AGENT_DIR"] ?? "",
|
|
3772
|
-
agentServerRoot:
|
|
3774
|
+
agentServerRoot: resolveStoreRoot(),
|
|
3773
3775
|
credentialSource: detectCredentialSource(process.env),
|
|
3774
3776
|
apiUrl: process.env["MOLTNET_API_URL"] ?? "",
|
|
3775
3777
|
signingPrivateKey: process.env["MOLTNET_PRIVATE_KEY"] ?? "",
|
|
@@ -3817,7 +3819,8 @@ function activatePiCodingAgentDir(path, env = {}) {
|
|
|
3817
3819
|
process.env["PI_CODING_AGENT_DIR"] = path;
|
|
3818
3820
|
Object.assign(process.env, env);
|
|
3819
3821
|
}
|
|
3820
|
-
function loadAgentServerEnvConfig() {
|
|
3822
|
+
function loadAgentServerEnvConfig(root) {
|
|
3823
|
+
const selection = resolveStoreSelection({ root });
|
|
3821
3824
|
const issuer = process.env["MOLTNET_OPERATOR_OAUTH_ISSUER"];
|
|
3822
3825
|
const publicUrl = process.env["MOLTNET_OPERATOR_OAUTH_PUBLIC_URL"] ?? issuer;
|
|
3823
3826
|
const nativeClientId = process.env["MOLTNET_NATIVE_OAUTH_CLIENT_ID"];
|
|
@@ -3833,7 +3836,8 @@ function loadAgentServerEnvConfig() {
|
|
|
3833
3836
|
},
|
|
3834
3837
|
port: process.env["MOLTNET_AGENT_SERVER_PORT"] ?? "",
|
|
3835
3838
|
allowedOrigins: process.env["MOLTNET_AGENT_SERVER_ALLOWED_ORIGINS"] ?? "",
|
|
3836
|
-
root:
|
|
3839
|
+
root: selection.root,
|
|
3840
|
+
rootSource: selection.source,
|
|
3837
3841
|
apiUrl: process.env["MOLTNET_API_URL"] ?? "",
|
|
3838
3842
|
logLevel: process.env["LOG_LEVEL"] ?? "",
|
|
3839
3843
|
activeIdentity: process.env["MOLTNET_ACTIVE_IDENTITY"] ?? ""
|
|
@@ -3843,21 +3847,22 @@ function loadAgentServerEnvConfig() {
|
|
|
3843
3847
|
function processEnvSnapshot() {
|
|
3844
3848
|
return process.env;
|
|
3845
3849
|
}
|
|
3846
|
-
function loadUpdateEnvConfig() {
|
|
3850
|
+
function loadUpdateEnvConfig(root) {
|
|
3847
3851
|
return {
|
|
3852
|
+
...!isDefaultStore({ root }) ? { storeRoot: resolveStoreRoot({ root }) } : {},
|
|
3848
3853
|
xdgCacheHome: process.env["XDG_CACHE_HOME"] ?? "",
|
|
3849
3854
|
localAppData: process.env["LOCALAPPDATA"] ?? ""
|
|
3850
3855
|
};
|
|
3851
3856
|
}
|
|
3852
3857
|
//#endregion
|
|
3853
3858
|
//#region src/lib/run-project-selection.ts
|
|
3854
|
-
var execFileAsync$
|
|
3859
|
+
var execFileAsync$1 = promisify(execFile);
|
|
3855
3860
|
var GIT_TIMEOUT_MS = 1e4;
|
|
3856
3861
|
var GIT_MAX_OUTPUT_BYTES = 64 * 1024;
|
|
3857
3862
|
async function validateGitSource(source) {
|
|
3858
3863
|
const inherited = processEnvSnapshot();
|
|
3859
3864
|
try {
|
|
3860
|
-
const { stdout } = await execFileAsync$
|
|
3865
|
+
const { stdout } = await execFileAsync$1("git", [
|
|
3861
3866
|
"rev-parse",
|
|
3862
3867
|
"--show-toplevel",
|
|
3863
3868
|
"--verify",
|
|
@@ -4015,11 +4020,9 @@ Commands:
|
|
|
4015
4020
|
once Claim and execute one specific queued task by id, then exit.
|
|
4016
4021
|
drain Poll until the queue has nothing claimable, then exit.
|
|
4017
4022
|
Useful for batch eval runs and demos.
|
|
4018
|
-
server
|
|
4019
|
-
agent/provider config store, and start/stop of poll/drain
|
|
4020
|
-
|
|
4021
|
-
server trust
|
|
4022
|
-
Install the per-user macOS local-HTTPS CA after explicit consent.
|
|
4023
|
+
server Supervisor for managed runs: authorized standalone local control,
|
|
4024
|
+
agent/provider config store, and start/stop of poll/drain child
|
|
4025
|
+
processes. Binds 127.0.0.1 or a private native socket.
|
|
4023
4026
|
providers Manage configured endpoints and Pi OAuth subscriptions without
|
|
4024
4027
|
starting the Agent Server. See \`agent-daemon providers --help\`.
|
|
4025
4028
|
sync-sessions
|
|
@@ -4169,33 +4172,33 @@ function isHelpFlag(args) {
|
|
|
4169
4172
|
return args.includes("--help") || args.includes("-h");
|
|
4170
4173
|
}
|
|
4171
4174
|
var AGENT_SERVER_HELP = `\
|
|
4172
|
-
agent-daemon server —
|
|
4175
|
+
agent-daemon server — local supervisor for managed runs.
|
|
4173
4176
|
|
|
4174
|
-
|
|
4175
|
-
|
|
4176
|
-
|
|
4177
|
+
Standalone mode binds 127.0.0.1 for authorized local-control clients. MoltNet
|
|
4178
|
+
Agent Desktop instead uses a private Unix socket with a process-scoped grant.
|
|
4179
|
+
Both modes configure agents and providers and start/stop child runs.
|
|
4177
4180
|
|
|
4178
4181
|
Options:
|
|
4179
|
-
--port <n> Loopback port. Default: 17374.
|
|
4182
|
+
--port <n> Loopback port. Default store: 17374; isolated stores: an available port.
|
|
4180
4183
|
Env: MOLTNET_AGENT_SERVER_PORT.
|
|
4181
|
-
--allowed-origins <csv> Exact
|
|
4184
|
+
--allowed-origins <csv> Exact browser-controller origins allowed
|
|
4185
|
+
local control.
|
|
4182
4186
|
Default: https://console.themolt.net.
|
|
4183
4187
|
Env: MOLTNET_AGENT_SERVER_ALLOWED_ORIGINS.
|
|
4184
4188
|
--root <path> Config root. Default: ~/.config/moltnet
|
|
4185
|
-
(or MOLTNET_AGENT_SERVER_ROOT).
|
|
4189
|
+
(or MOLTNET_HOME; legacy MOLTNET_AGENT_SERVER_ROOT).
|
|
4186
4190
|
--api-url <url> Default MoltNet API for new managed agents.
|
|
4187
4191
|
Default: https://api.themolt.net.
|
|
4188
4192
|
--heartbeat-interval-ms <n> Child reporter heartbeat cadence. Default: 60000.
|
|
4189
4193
|
--warm-retention-sec <n> Child session/workspace retention. Default: 1800.
|
|
4190
4194
|
--supervised Also stop gracefully when stdin reaches EOF.
|
|
4195
|
+
--native-socket <path> Private native-only socket (requires --supervised).
|
|
4196
|
+
Absolute, at most 100 bytes, with a new socket in
|
|
4197
|
+
a caller-owned 0700 directory. TCP flags are not
|
|
4198
|
+
accepted; inherited TCP env settings are ignored.
|
|
4191
4199
|
|
|
4192
|
-
|
|
4193
|
-
|
|
4194
|
-
server trust --status --json
|
|
4195
|
-
server trust --yes --json
|
|
4196
|
-
server trust --remove --yes --json
|
|
4197
|
-
Run \`agent-daemon server trust --remove\` interactively to remove that exact
|
|
4198
|
-
CA. Linux continues to use the Chromium PNA HTTP path.
|
|
4200
|
+
Standalone mode uses loopback HTTP on every platform. Desktop socket mode does
|
|
4201
|
+
not open a TCP listener.
|
|
4199
4202
|
`;
|
|
4200
4203
|
var PROVIDERS_HELP = `\
|
|
4201
4204
|
moltnet-agent providers — manage local model providers.
|
|
@@ -4212,8 +4215,8 @@ Usage:
|
|
|
4212
4215
|
[--root <path>]
|
|
4213
4216
|
moltnet-agent providers logout <id> [--yes] [--root <path>]
|
|
4214
4217
|
|
|
4215
|
-
The default root is ~/.config/moltnet.
|
|
4216
|
-
|
|
4218
|
+
The default root is ~/.config/moltnet. MOLTNET_HOME selects another store.
|
|
4219
|
+
MOLTNET_AGENT_SERVER_ROOT is a legacy alias; conflicting values fail. API keys are accepted only from redirected stdin; they
|
|
4217
4220
|
are stored separately and providers.json contains only a secret reference.
|
|
4218
4221
|
|
|
4219
4222
|
--model declares a text-only model. --model-input declares a model together
|
|
@@ -4258,7 +4261,7 @@ async function resolveDaemonAgentIdentity(input) {
|
|
|
4258
4261
|
}
|
|
4259
4262
|
//#endregion
|
|
4260
4263
|
//#region src/lib/correlation.ts
|
|
4261
|
-
var execFileAsync
|
|
4264
|
+
var execFileAsync = promisify(execFile);
|
|
4262
4265
|
var CORRELATION_TRAILER_KEY = "Moltnet-Correlation-Id";
|
|
4263
4266
|
var CORRELATION_MARKER_RE = /<!--\s*moltnet-correlation:\s*([\w-]+)\s*-->/i;
|
|
4264
4267
|
new RegExp(`^${CORRELATION_TRAILER_KEY}:\\s*(\\S+)\\s*$`, "m");
|
|
@@ -4305,7 +4308,7 @@ function makePrBodyAnchorWriter(deps) {
|
|
|
4305
4308
|
function createGhCliClient() {
|
|
4306
4309
|
return {
|
|
4307
4310
|
async get({ owner, repo, number }) {
|
|
4308
|
-
const { stdout } = await execFileAsync
|
|
4311
|
+
const { stdout } = await execFileAsync("gh", [
|
|
4309
4312
|
"api",
|
|
4310
4313
|
`repos/${owner}/${repo}/pulls/${number}`,
|
|
4311
4314
|
"--jq",
|
|
@@ -4314,7 +4317,7 @@ function createGhCliClient() {
|
|
|
4314
4317
|
return JSON.parse(stdout);
|
|
4315
4318
|
},
|
|
4316
4319
|
async patch({ owner, repo, number }, body) {
|
|
4317
|
-
await execFileAsync
|
|
4320
|
+
await execFileAsync("gh", [
|
|
4318
4321
|
"api",
|
|
4319
4322
|
"-X",
|
|
4320
4323
|
"PATCH",
|
|
@@ -5659,20 +5662,9 @@ var AgentServerStoreError = class extends Error {
|
|
|
5659
5662
|
this.code = code;
|
|
5660
5663
|
}
|
|
5661
5664
|
};
|
|
5662
|
-
/**
|
|
5663
|
-
* `MOLTNET_AGENT_SERVER_ROOT` override, else `~/.config/moltnet`.
|
|
5664
|
-
*
|
|
5665
|
-
* Deliberately does NOT consult `XDG_CONFIG_HOME`. The Go CLI's GetConfigDir
|
|
5666
|
-
* and @moltnet/agent-config's getConfigDir both resolve `~/.config/moltnet`,
|
|
5667
|
-
* so honouring XDG here gave one application two config roots: on a machine
|
|
5668
|
-
* with the variable set, the daemon wrote identities the CLI and SDK could not
|
|
5669
|
-
* read. `MOLTNET_AGENT_SERVER_ROOT` remains the explicit escape hatch for a
|
|
5670
|
-
* genuinely custom location.
|
|
5671
|
-
*/
|
|
5665
|
+
/** Explicit root, MOLTNET_HOME, legacy alias, then the unchanged default store. */
|
|
5672
5666
|
function resolveAgentServerRoot(input) {
|
|
5673
|
-
|
|
5674
|
-
if (override) return override;
|
|
5675
|
-
return getConfigDir();
|
|
5667
|
+
return getConfigDir(input);
|
|
5676
5668
|
}
|
|
5677
5669
|
/**
|
|
5678
5670
|
* Copy a model entry, dropping an empty `input` so it never reaches the wire.
|
|
@@ -6106,7 +6098,7 @@ async function resolvePiAgentDir(cfg, agentRoot, profiles, options = {}) {
|
|
|
6106
6098
|
};
|
|
6107
6099
|
}
|
|
6108
6100
|
const repoPiDir = join(agentRoot, ".pi");
|
|
6109
|
-
const store = new AgentServerStore(resolveAgentServerRoot({ root: cfg.agentServerRoot }));
|
|
6101
|
+
const store = new AgentServerStore(resolveAgentServerRoot({ root: cfg.agentServerRoot || void 0 }));
|
|
6110
6102
|
const providers = store.readProviders();
|
|
6111
6103
|
const storeHasAuth = existsSync(store.piAuthJsonPath);
|
|
6112
6104
|
if (Object.keys(providers).length === 0 && !storeHasAuth) {
|
|
@@ -6187,7 +6179,7 @@ function mergePiModels(store, repo) {
|
|
|
6187
6179
|
//#endregion
|
|
6188
6180
|
//#region src/lib/state-dir.ts
|
|
6189
6181
|
function ensureDaemonStateDirs(mountPath) {
|
|
6190
|
-
const rootDir =
|
|
6182
|
+
const rootDir = canonicalStoreRoot(join(mountPath, ".moltnet", "d"));
|
|
6191
6183
|
const piSessionsDir = join(rootDir, "pi-sessions");
|
|
6192
6184
|
mkdirSync(rootDir, {
|
|
6193
6185
|
recursive: true,
|
|
@@ -6202,17 +6194,6 @@ function ensureDaemonStateDirs(mountPath) {
|
|
|
6202
6194
|
piSessionsDir: realpathSync(piSessionsDir)
|
|
6203
6195
|
};
|
|
6204
6196
|
}
|
|
6205
|
-
function canonicalStatePath(path) {
|
|
6206
|
-
let parent = resolve(path);
|
|
6207
|
-
const missing = [];
|
|
6208
|
-
while (!existsSync(parent)) {
|
|
6209
|
-
missing.unshift(basename(parent));
|
|
6210
|
-
const next = dirname(parent);
|
|
6211
|
-
if (next === parent) throw new Error(`Cannot resolve daemon state root ${path}`);
|
|
6212
|
-
parent = next;
|
|
6213
|
-
}
|
|
6214
|
-
return join(realpathSync(parent), ...missing);
|
|
6215
|
-
}
|
|
6216
6197
|
//#endregion
|
|
6217
6198
|
//#region src/lib/prepare-runtime-profile.ts
|
|
6218
6199
|
/** Validate and prepare a profile through the shared daemon execution path. */
|
|
@@ -8869,12 +8850,13 @@ function parseProviderArgs(command, args) {
|
|
|
8869
8850
|
}
|
|
8870
8851
|
}
|
|
8871
8852
|
async function createContext(rootFlag, dependencies, installInterruptHandler) {
|
|
8872
|
-
const
|
|
8853
|
+
const root = resolveAgentServerRoot({ root: rootFlag ?? dependencies.envRoot ?? loadAgentServerEnvConfig().root });
|
|
8854
|
+
const store = new AgentServerStore(root).ensure();
|
|
8873
8855
|
const secrets = new FileSecretProvider({
|
|
8874
8856
|
root: store.secretsDir,
|
|
8875
8857
|
writable: true
|
|
8876
8858
|
});
|
|
8877
|
-
const secretProviders = createNodeSecretProviderRegistry().register(secrets);
|
|
8859
|
+
const secretProviders = createNodeSecretProviderRegistry({ store: { root } }).register(secrets);
|
|
8878
8860
|
const stderr = dependencies.stderr ?? console.error;
|
|
8879
8861
|
const logger = createCliLogger(stderr);
|
|
8880
8862
|
const configuration = dependencies.configuration ?? new ProviderConfigurationService({
|
|
@@ -9362,11 +9344,67 @@ var ConnectionSettingsStore = class {
|
|
|
9362
9344
|
};
|
|
9363
9345
|
/** A custom service gets its own identities, keys and runtime configuration. */
|
|
9364
9346
|
function connectionStateRoot(root, settings) {
|
|
9365
|
-
const identity = [settings.apiUrl.replace(
|
|
9347
|
+
const identity = [settings.apiUrl.replace(/\/+$/u, ""), settings.issuer.replace(/\/+$/u, "")];
|
|
9366
9348
|
if (identity[0] === RELEASE_CONNECTION.apiUrl && identity[1] === RELEASE_CONNECTION.issuer) return root;
|
|
9367
9349
|
return join(root, "environments", createHash("sha256").update(JSON.stringify(identity)).digest("hex"));
|
|
9368
9350
|
}
|
|
9369
9351
|
//#endregion
|
|
9352
|
+
//#region src/lib/agent-server/endpoint.ts
|
|
9353
|
+
function defaultAgentServerPort(root) {
|
|
9354
|
+
return isDefaultStore({ root }) ? OPERATOR_OAUTH.serverPort : 0;
|
|
9355
|
+
}
|
|
9356
|
+
function endpointPath(root) {
|
|
9357
|
+
return join(root, "agent-server-endpoint.json");
|
|
9358
|
+
}
|
|
9359
|
+
function validateUrl(value) {
|
|
9360
|
+
const url = new URL(value);
|
|
9361
|
+
if (url.protocol !== "http:" || url.hostname !== "127.0.0.1" || url.port === "0" || url.username || url.password || url.pathname !== "/" || url.search || url.hash || !/^http:\/\/127\.0\.0\.1(?::[1-9]\d{0,4})?$/.test(value)) throw new Error("Agent Server discovery requires a loopback HTTP origin");
|
|
9362
|
+
}
|
|
9363
|
+
function readAgentServerEndpoint(root) {
|
|
9364
|
+
let raw;
|
|
9365
|
+
try {
|
|
9366
|
+
raw = readFileSync(endpointPath(root), "utf8");
|
|
9367
|
+
} catch (cause) {
|
|
9368
|
+
if (cause.code === "ENOENT") return null;
|
|
9369
|
+
throw cause;
|
|
9370
|
+
}
|
|
9371
|
+
try {
|
|
9372
|
+
const value = JSON.parse(raw);
|
|
9373
|
+
if (value.version !== 1 || typeof value.instanceId !== "string" || value.instanceId.length !== 36 || !/^([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})$/iu.test(value.instanceId) || typeof value.url !== "string") throw new Error("Invalid Agent Server discovery metadata");
|
|
9374
|
+
validateUrl(value.url);
|
|
9375
|
+
if (value.pid !== void 0) {
|
|
9376
|
+
if (!Number.isSafeInteger(value.pid) || value.pid <= 0 || value.pid > 2147483647) throw new Error("Invalid process ID");
|
|
9377
|
+
try {
|
|
9378
|
+
process.kill(value.pid, 0);
|
|
9379
|
+
} catch (cause) {
|
|
9380
|
+
if (cause.code === "ESRCH") return null;
|
|
9381
|
+
}
|
|
9382
|
+
}
|
|
9383
|
+
return value;
|
|
9384
|
+
} catch (cause) {
|
|
9385
|
+
throw new Error(`Invalid Agent Server discovery at ${endpointPath(root)}: ${cause instanceof Error ? cause.message : String(cause)}`, { cause });
|
|
9386
|
+
}
|
|
9387
|
+
}
|
|
9388
|
+
/** Publish and release only while holding the selected store's singleton lock. */
|
|
9389
|
+
function publishAgentServerEndpoint(root, url) {
|
|
9390
|
+
validateUrl(url);
|
|
9391
|
+
const endpoint = {
|
|
9392
|
+
version: 1,
|
|
9393
|
+
instanceId: randomUUID(),
|
|
9394
|
+
pid: process.pid,
|
|
9395
|
+
url
|
|
9396
|
+
};
|
|
9397
|
+
writeJsonAtomic(endpointPath(root), endpoint);
|
|
9398
|
+
return {
|
|
9399
|
+
record: endpoint,
|
|
9400
|
+
release() {
|
|
9401
|
+
try {
|
|
9402
|
+
if (readAgentServerEndpoint(root)?.instanceId === endpoint.instanceId) rmSync(endpointPath(root), { force: true });
|
|
9403
|
+
} catch {}
|
|
9404
|
+
}
|
|
9405
|
+
};
|
|
9406
|
+
}
|
|
9407
|
+
//#endregion
|
|
9370
9408
|
//#region src/lib/agent-server/lock.ts
|
|
9371
9409
|
var AgentServerLockError = class extends Error {
|
|
9372
9410
|
name = "AgentServerLockError";
|
|
@@ -9400,7 +9438,14 @@ async function acquireAgentServerLock(root, options = {}) {
|
|
|
9400
9438
|
}
|
|
9401
9439
|
});
|
|
9402
9440
|
} catch (cause) {
|
|
9403
|
-
if (cause.code === "ELOCKED")
|
|
9441
|
+
if (cause.code === "ELOCKED") {
|
|
9442
|
+
let holder = "";
|
|
9443
|
+
try {
|
|
9444
|
+
const endpoint = readAgentServerEndpoint(root);
|
|
9445
|
+
if (endpoint?.pid) holder = ` (PID ${endpoint.pid}, instance ${endpoint.instanceId})`;
|
|
9446
|
+
} catch {}
|
|
9447
|
+
throw new AgentServerLockError("held", `another moltnet-agent server process already owns ${path}${holder}; stop that process before retrying`, { cause });
|
|
9448
|
+
}
|
|
9404
9449
|
throw new AgentServerLockError("failed", `could not acquire Agent Server lock ${path}: ${cause.message}`, { cause });
|
|
9405
9450
|
}
|
|
9406
9451
|
let released = false;
|
|
@@ -9415,11 +9460,15 @@ async function acquireAgentServerLock(root, options = {}) {
|
|
|
9415
9460
|
}
|
|
9416
9461
|
/** Always release after normal completion or startup/runtime failure. */
|
|
9417
9462
|
async function withAgentServerLock(root, work, options) {
|
|
9418
|
-
const held =
|
|
9463
|
+
const held = [];
|
|
9419
9464
|
try {
|
|
9465
|
+
held.push(await acquireAgentServerLock(root, options));
|
|
9466
|
+
if (options?.stateRoot && options.stateRoot !== root) held.push(await acquireAgentServerLock(options.stateRoot, options));
|
|
9420
9467
|
return await work();
|
|
9421
9468
|
} finally {
|
|
9422
|
-
|
|
9469
|
+
for (const entry of held.reverse()) await entry.release().catch((error) => {
|
|
9470
|
+
console.error(`Could not release Agent Server lock ${entry.path}: ${String(error)}`);
|
|
9471
|
+
});
|
|
9423
9472
|
}
|
|
9424
9473
|
}
|
|
9425
9474
|
//#endregion
|
|
@@ -9454,6 +9503,10 @@ function applyNativeClientGrant(options) {
|
|
|
9454
9503
|
nativeGrant.grantNative(token);
|
|
9455
9504
|
return true;
|
|
9456
9505
|
}
|
|
9506
|
+
//#endregion
|
|
9507
|
+
//#region src/lib/agent-server/native-grant-service.ts
|
|
9508
|
+
/** Process-only native grant. Browser authorization belongs to OperatorOAuth. */
|
|
9509
|
+
var NATIVE_CLIENT_ORIGIN = "moltnet-agent-desktop://native";
|
|
9457
9510
|
var NativeGrantError = class extends Error {
|
|
9458
9511
|
code = "native_token_invalid";
|
|
9459
9512
|
};
|
|
@@ -9469,6 +9522,29 @@ var NativeGrantService = class {
|
|
|
9469
9522
|
}
|
|
9470
9523
|
};
|
|
9471
9524
|
//#endregion
|
|
9525
|
+
//#region src/lib/agent-server/native-socket.ts
|
|
9526
|
+
/** The supervisor owns the private directory; the server never removes peers. */
|
|
9527
|
+
async function validateNativeSocket(path) {
|
|
9528
|
+
if (!isAbsolute(path) || Buffer.byteLength(path) > 100) throw new Error("Native socket must be an absolute path of at most 100 bytes");
|
|
9529
|
+
const parent = dirname(path);
|
|
9530
|
+
const metadata = await lstat(parent);
|
|
9531
|
+
if (metadata.isSymbolicLink()) throw new Error(`Native socket parent contains a symlink: ${parent}`);
|
|
9532
|
+
if (!metadata.isDirectory()) throw new Error(`Native socket parent is not a directory: ${parent}`);
|
|
9533
|
+
const expectedUid = process.getuid?.();
|
|
9534
|
+
if (expectedUid === void 0 || metadata.uid !== expectedUid) throw new Error(`Native socket parent has uid ${metadata.uid}; expected ${String(expectedUid)}: ${parent}`);
|
|
9535
|
+
const mode = metadata.mode & 511;
|
|
9536
|
+
if (mode !== 448) throw new Error(`Native socket parent has mode ${mode.toString(8)}; expected 700: ${parent}`);
|
|
9537
|
+
const canonical = await realpath(parent);
|
|
9538
|
+
if (canonical !== resolve(parent)) throw new Error(`Native socket parent contains a symlink: ${parent} resolves to ${canonical}`);
|
|
9539
|
+
try {
|
|
9540
|
+
await lstat(path);
|
|
9541
|
+
} catch (error) {
|
|
9542
|
+
if (error.code === "ENOENT") return;
|
|
9543
|
+
throw error;
|
|
9544
|
+
}
|
|
9545
|
+
throw new Error("Native socket path already exists; it was left untouched");
|
|
9546
|
+
}
|
|
9547
|
+
//#endregion
|
|
9472
9548
|
//#region src/lib/agent-server/operator-oauth.ts
|
|
9473
9549
|
var InvalidOperatorGrantError = class extends Error {};
|
|
9474
9550
|
var LOCAL_SCOPE = OPERATOR_OAUTH.localControlScope;
|
|
@@ -9671,8 +9747,8 @@ var OperatorOAuth = class {
|
|
|
9671
9747
|
/**
|
|
9672
9748
|
* Subscription-provider OAuth brokering for Agent Server (#2061 slice 4).
|
|
9673
9749
|
*
|
|
9674
|
-
*
|
|
9675
|
-
* `ModelRuntime.login()` (which owns persistence into the shared
|
|
9750
|
+
* An authorized controller starts the flow; Agent Server runs Pi OAuth
|
|
9751
|
+
* host-side via `ModelRuntime.login()` (which owns persistence into the shared
|
|
9676
9752
|
* `pi/auth.json` and token rotation thereafter). The browser only ever sees
|
|
9677
9753
|
* the provider's authorize URL or device code — never tokens.
|
|
9678
9754
|
*
|
|
@@ -10566,6 +10642,8 @@ var RunManager = class {
|
|
|
10566
10642
|
const homeDir = join(dirname(piDir), "home");
|
|
10567
10643
|
const env = {
|
|
10568
10644
|
HOME: homeDir,
|
|
10645
|
+
MOLTNET_DEFAULT_STORE_ROOT: defaultStoreRoot({ env: this.options.baseEnv }),
|
|
10646
|
+
MOLTNET_HOME: getConfigDir({ root: this.options.storeRoot ?? this.store.root }),
|
|
10569
10647
|
PI_CODING_AGENT_DIR: piDir,
|
|
10570
10648
|
XDG_CACHE_HOME: join(homeDir, ".cache"),
|
|
10571
10649
|
XDG_CONFIG_HOME: join(homeDir, ".config"),
|
|
@@ -10609,7 +10687,7 @@ var RunManager = class {
|
|
|
10609
10687
|
env["MOLTNET_AGENT_KEY"] = requireCredentialSnapshot(agent).agentKey;
|
|
10610
10688
|
env["MOLTNET_API_URL"] = activation.apiUrl ?? (activation.source === "external" ? activation.configApiUrl : "");
|
|
10611
10689
|
if (activation.source === "managed") {
|
|
10612
|
-
if (
|
|
10690
|
+
if (config.keys.private_key_ref?.provider !== "file") throw new AgentServerRunError("invalid_store", "The managed identity store is inconsistent: its signing key must be file-backed. Restore the identity store or use credential recovery.");
|
|
10613
10691
|
env["MOLTNET_PRIVATE_KEY_REF"] = formatSecretReferenceString(config.keys.private_key_ref);
|
|
10614
10692
|
env["MOLTNET_SECRET_ROOT"] = this.store.secretsDir;
|
|
10615
10693
|
} else try {
|
|
@@ -11212,6 +11290,20 @@ function writeRegistry(path, entries) {
|
|
|
11212
11290
|
renameSync(temp, path);
|
|
11213
11291
|
}
|
|
11214
11292
|
//#endregion
|
|
11293
|
+
//#region src/lib/agent-server/secret-providers.ts
|
|
11294
|
+
function createAgentServerSecretProviders(settings, store) {
|
|
11295
|
+
const secrets = new FileSecretProvider({
|
|
11296
|
+
root: store.secretsDir,
|
|
11297
|
+
writable: true
|
|
11298
|
+
});
|
|
11299
|
+
const registry = () => createNodeSecretProviderRegistry({ store: { root: settings.root } });
|
|
11300
|
+
return {
|
|
11301
|
+
secrets,
|
|
11302
|
+
secretProviders: registry().register(secrets),
|
|
11303
|
+
externalSecretProviders: registry()
|
|
11304
|
+
};
|
|
11305
|
+
}
|
|
11306
|
+
//#endregion
|
|
11215
11307
|
//#region src/lib/agent-server/readiness.ts
|
|
11216
11308
|
/**
|
|
11217
11309
|
* Whether a runtime profile can execute on *this* machine.
|
|
@@ -11406,17 +11498,17 @@ async function enrollIdentityTeam(options) {
|
|
|
11406
11498
|
/**
|
|
11407
11499
|
* The identity-wide team/diary binding, read from `<identityDir>/env`.
|
|
11408
11500
|
*
|
|
11409
|
-
* This mirrors the Go CLI's `identityDefaultBinding` (`
|
|
11501
|
+
* This mirrors the Go CLI's `identityDefaultBinding` (`project_selection.go`),
|
|
11410
11502
|
* which is the fallback the CLI uses when a working directory has no
|
|
11411
|
-
*
|
|
11503
|
+
* registered project binding.
|
|
11412
11504
|
*
|
|
11413
11505
|
* The desktop cannot use the CLI's *location* bindings at all — its composer
|
|
11414
11506
|
* has no working directory to key on — but it can honour this identity-wide
|
|
11415
|
-
* default, so an operator
|
|
11416
|
-
*
|
|
11507
|
+
* default, so an operator sees their familiar team preselected rather than an
|
|
11508
|
+
* arbitrary first entry.
|
|
11417
11509
|
*
|
|
11418
11510
|
* Like the CLI, a half-filled pair is treated as no binding: a team without a
|
|
11419
|
-
* diary is
|
|
11511
|
+
* diary is ignored.
|
|
11420
11512
|
*/
|
|
11421
11513
|
function readIdentityDefaultBinding(identityDir) {
|
|
11422
11514
|
let contents;
|
|
@@ -11978,24 +12070,23 @@ function requestOperationSignal(request, shutdownSignal) {
|
|
|
11978
12070
|
else request.raw.once("aborted", () => disconnected.abort({ source: "request" }));
|
|
11979
12071
|
return shutdownSignal ? AbortSignal.any([disconnected.signal, shutdownSignal]) : disconnected.signal;
|
|
11980
12072
|
}
|
|
11981
|
-
function buildAgentServer(
|
|
12073
|
+
function buildAgentServer(input) {
|
|
12074
|
+
const options = { ...input };
|
|
11982
12075
|
const { nativeGrant } = options;
|
|
11983
12076
|
const oauth = options.operatorOAuth;
|
|
11984
12077
|
let restartRequired = false;
|
|
11985
|
-
const fastifyOptions = {
|
|
11986
|
-
bodyLimit: BODY_LIMIT,
|
|
11987
|
-
...options.tls ? { https: options.tls } : {}
|
|
11988
|
-
};
|
|
12078
|
+
const fastifyOptions = { bodyLimit: BODY_LIMIT };
|
|
11989
12079
|
const app = options.logger ? Fastify({
|
|
11990
12080
|
...fastifyOptions,
|
|
11991
12081
|
loggerInstance: options.logger
|
|
11992
12082
|
}) : Fastify(fastifyOptions);
|
|
12083
|
+
app.addHook("onListen", async () => {
|
|
12084
|
+
const address = app.server.address();
|
|
12085
|
+
if (address && typeof address !== "string") options.selfOrigin = `http://127.0.0.1:${address.port}`;
|
|
12086
|
+
});
|
|
11993
12087
|
options.registerOpenApi?.(app);
|
|
11994
12088
|
for (const schema of AGENT_SERVER_SCHEMAS) app.addSchema(schema);
|
|
11995
|
-
const
|
|
11996
|
-
registerLoopbackSecurity(app, {
|
|
11997
|
-
isOriginAllowed: (origin) => origin === "moltnet-agent-desktop://native" || browserOrigins.has(origin),
|
|
11998
|
-
...options.selfOrigin ? { selfOrigins: [options.selfOrigin] } : {},
|
|
12089
|
+
const securityOptions = {
|
|
11999
12090
|
allowedHeaders: [AGENT_SERVER_TOKEN_HEADER],
|
|
12000
12091
|
methods: [
|
|
12001
12092
|
"GET",
|
|
@@ -12004,7 +12095,18 @@ function buildAgentServer(options) {
|
|
|
12004
12095
|
"DELETE",
|
|
12005
12096
|
"OPTIONS"
|
|
12006
12097
|
]
|
|
12098
|
+
};
|
|
12099
|
+
if (options.nativeOnly) registerLoopbackSecurity(app, {
|
|
12100
|
+
...securityOptions,
|
|
12101
|
+
isOriginAllowed: (origin) => origin === NATIVE_CLIENT_ORIGIN
|
|
12007
12102
|
});
|
|
12103
|
+
else {
|
|
12104
|
+
const browserOrigins = new OriginAllowlist(options.allowedOrigins);
|
|
12105
|
+
registerLoopbackSecurity(app, {
|
|
12106
|
+
...securityOptions,
|
|
12107
|
+
isOriginAllowed: (origin) => origin === "moltnet-agent-desktop://native" || origin === options.selfOrigin || browserOrigins.has(origin)
|
|
12108
|
+
});
|
|
12109
|
+
}
|
|
12008
12110
|
let verificationWindow = Date.now();
|
|
12009
12111
|
let verifications = 0;
|
|
12010
12112
|
const browserVerification = /* @__PURE__ */ new WeakMap();
|
|
@@ -12021,6 +12123,31 @@ function buildAgentServer(options) {
|
|
|
12021
12123
|
browserVerification.set(request, pending);
|
|
12022
12124
|
return pending;
|
|
12023
12125
|
}
|
|
12126
|
+
function hasValidNativeGrant(origin, token) {
|
|
12127
|
+
if (origin !== "moltnet-agent-desktop://native" || typeof token !== "string" || token.length === 0) return false;
|
|
12128
|
+
try {
|
|
12129
|
+
nativeGrant.verify(origin, token);
|
|
12130
|
+
return true;
|
|
12131
|
+
} catch (error) {
|
|
12132
|
+
if (error instanceof NativeGrantError) return false;
|
|
12133
|
+
throw error;
|
|
12134
|
+
}
|
|
12135
|
+
}
|
|
12136
|
+
const nativeVerification = /* @__PURE__ */ new WeakSet();
|
|
12137
|
+
function requireNativeGrant(request) {
|
|
12138
|
+
if (nativeVerification.has(request)) return;
|
|
12139
|
+
const origin = request.headers.origin;
|
|
12140
|
+
const token = request.headers[AGENT_SERVER_TOKEN_HEADER];
|
|
12141
|
+
if (hasValidNativeGrant(origin, token)) {
|
|
12142
|
+
nativeVerification.add(request);
|
|
12143
|
+
return;
|
|
12144
|
+
}
|
|
12145
|
+
request.log.warn({
|
|
12146
|
+
stage: "native-control-authorization",
|
|
12147
|
+
outcome: "rejected"
|
|
12148
|
+
}, "Native control authorization failed");
|
|
12149
|
+
throw new AgentServerHttpError(401, "native_token_invalid", "Native authorization required");
|
|
12150
|
+
}
|
|
12024
12151
|
app.register(rateLimit, {
|
|
12025
12152
|
global: true,
|
|
12026
12153
|
max: options.rateLimitMax ?? RATE_LIMIT_MAX,
|
|
@@ -12032,15 +12159,15 @@ function buildAgentServer(options) {
|
|
|
12032
12159
|
const presented = request.headers[AGENT_SERVER_TOKEN_HEADER];
|
|
12033
12160
|
let authenticated = false;
|
|
12034
12161
|
if (typeof presented === "string" && presented.length > 0) try {
|
|
12035
|
-
if (origin === "moltnet-agent-desktop://native")
|
|
12162
|
+
if (origin === "moltnet-agent-desktop://native") authenticated = hasValidNativeGrant(origin, presented);
|
|
12036
12163
|
else {
|
|
12037
12164
|
if (!oauth) return `unauth:${origin}:${request.ip}`;
|
|
12038
12165
|
await verifyBrowser(request, presented);
|
|
12166
|
+
authenticated = true;
|
|
12039
12167
|
}
|
|
12040
|
-
authenticated = true;
|
|
12041
12168
|
} catch (error) {
|
|
12042
12169
|
if (error instanceof AgentServerHttpError && error.statusCode === 429) throw error;
|
|
12043
|
-
if (origin === "moltnet-agent-desktop://native"
|
|
12170
|
+
if (origin === "moltnet-agent-desktop://native") throw error;
|
|
12044
12171
|
}
|
|
12045
12172
|
return authenticated ? `origin:${origin}` : `unauth:${origin}:${request.ip}`;
|
|
12046
12173
|
}
|
|
@@ -12050,7 +12177,7 @@ function buildAgentServer(options) {
|
|
|
12050
12177
|
const origin = requireOriginHeader(request.headers);
|
|
12051
12178
|
const token = request.headers[AGENT_SERVER_TOKEN_HEADER];
|
|
12052
12179
|
if (typeof token !== "string" || token.length === 0) throw new AgentServerHttpError(401, "authorization_required", "Local control token is required");
|
|
12053
|
-
if (origin === "moltnet-agent-desktop://native")
|
|
12180
|
+
if (origin === "moltnet-agent-desktop://native") requireNativeGrant(request);
|
|
12054
12181
|
else try {
|
|
12055
12182
|
if (!oauth) throw new AgentServerHttpError(503, "oauth_unavailable", "Local OAuth is not configured");
|
|
12056
12183
|
const admission = browserVerification.get(request);
|
|
@@ -12079,6 +12206,9 @@ function buildAgentServer(options) {
|
|
|
12079
12206
|
return origin;
|
|
12080
12207
|
};
|
|
12081
12208
|
app.after(() => {
|
|
12209
|
+
if (options.nativeOnly) app.addHook("preParsing", async (request) => {
|
|
12210
|
+
requireNativeGrant(request);
|
|
12211
|
+
});
|
|
12082
12212
|
app.get("/health", { schema: AgentServerRouteSchemas.health }, async () => ({ status: "ok" }));
|
|
12083
12213
|
app.get("/v1/native/connection-settings", { schema: { hide: true } }, async (request) => {
|
|
12084
12214
|
if (await requireAuthorizedOrigin(request) !== "moltnet-agent-desktop://native" || !options.connectionSettings) throw new AgentServerHttpError(403, "native_required", "Native administration required");
|
|
@@ -12626,7 +12756,7 @@ function normalizeAgentServerError(error) {
|
|
|
12626
12756
|
message: error.message
|
|
12627
12757
|
};
|
|
12628
12758
|
if (error instanceof AgentServerRunError) return {
|
|
12629
|
-
statusCode: error.code === "run_not_found" ? 404 : 400,
|
|
12759
|
+
statusCode: error.code === "run_not_found" ? 404 : error.code === "invalid_store" ? 409 : 400,
|
|
12630
12760
|
code: error.code,
|
|
12631
12761
|
message: error.message
|
|
12632
12762
|
};
|
|
@@ -12657,196 +12787,40 @@ function normalizeAgentServerError(error) {
|
|
|
12657
12787
|
};
|
|
12658
12788
|
}
|
|
12659
12789
|
//#endregion
|
|
12660
|
-
//#region src/lib/agent-server/tls.ts
|
|
12661
|
-
var execFileAsync = promisify(execFile);
|
|
12662
|
-
var CA_COMMON_NAME = "MoltNet Local Agent CA";
|
|
12663
|
-
var LEAF_COMMON_NAME = "MoltNet Local Agent";
|
|
12664
|
-
var RENEW_BEFORE_MS = 720 * 60 * 60 * 1e3;
|
|
12665
|
-
function loginKeychainPath() {
|
|
12666
|
-
return join(homedir(), "Library", "Keychains", "login.keychain-db");
|
|
12667
|
-
}
|
|
12668
|
-
function pemPrivateKey(key) {
|
|
12669
|
-
return webcrypto.subtle.exportKey("pkcs8", key).then((der) => createPrivateKey({
|
|
12670
|
-
key: Buffer.from(der),
|
|
12671
|
-
format: "der",
|
|
12672
|
-
type: "pkcs8"
|
|
12673
|
-
}).export({
|
|
12674
|
-
format: "pem",
|
|
12675
|
-
type: "pkcs8"
|
|
12676
|
-
}).toString());
|
|
12677
|
-
}
|
|
12678
|
-
async function localTlsMaterialFromDirectory(dir) {
|
|
12679
|
-
try {
|
|
12680
|
-
const [key, cert, ca] = await Promise.all([
|
|
12681
|
-
readFile(join(dir, "loopback-key.pem"), "utf8"),
|
|
12682
|
-
readFile(join(dir, "loopback-cert.pem"), "utf8"),
|
|
12683
|
-
readFile(join(dir, "local-ca.pem"), "utf8")
|
|
12684
|
-
]);
|
|
12685
|
-
const parsed = new X509Certificate(cert);
|
|
12686
|
-
const authority = new X509Certificate(ca);
|
|
12687
|
-
if (Date.parse(parsed.validTo) - Date.now() > RENEW_BEFORE_MS && Date.parse(authority.validTo) - Date.now() > RENEW_BEFORE_MS && authority.keyUsage?.includes("1.3.6.1.5.5.7.3.1") && parsed.checkIP("127.0.0.1") === "127.0.0.1" && parsed.verify(authority.publicKey) && parsed.publicKey.equals(createPublicKey(key))) return {
|
|
12688
|
-
key,
|
|
12689
|
-
cert,
|
|
12690
|
-
ca,
|
|
12691
|
-
fingerprint: new X509Certificate(ca).fingerprint256
|
|
12692
|
-
};
|
|
12693
|
-
} catch {}
|
|
12694
|
-
return null;
|
|
12695
|
-
}
|
|
12696
|
-
/** Read-only inspection for native status polling. */
|
|
12697
|
-
async function inspectLocalTlsMaterial(root) {
|
|
12698
|
-
if (await hasStoredSigningKey(join(root, "tls"))) return null;
|
|
12699
|
-
return localTlsMaterialFromDirectory(join(root, "tls"));
|
|
12700
|
-
}
|
|
12701
|
-
async function hasStoredSigningKey(dir) {
|
|
12702
|
-
return stat(join(dir, "local-ca-key.pem")).then(() => true, (error) => {
|
|
12703
|
-
if (error.code === "ENOENT") return false;
|
|
12704
|
-
throw error;
|
|
12705
|
-
});
|
|
12706
|
-
}
|
|
12707
|
-
/** Creates a per-user CA and loopback-only leaf certificate under a 0700 directory. */
|
|
12708
|
-
async function ensureLocalTlsMaterial(root) {
|
|
12709
|
-
const dir = join(root, "tls");
|
|
12710
|
-
await mkdir(dir, {
|
|
12711
|
-
recursive: true,
|
|
12712
|
-
mode: 448
|
|
12713
|
-
});
|
|
12714
|
-
const previousSigningKey = join(dir, "local-ca-key.pem");
|
|
12715
|
-
const hasPreviousSigningKey = await hasStoredSigningKey(dir);
|
|
12716
|
-
const existing = await localTlsMaterialFromDirectory(dir);
|
|
12717
|
-
if (existing && !hasPreviousSigningKey) return existing;
|
|
12718
|
-
if (isMacos() && await isLocalCaTrusted(root)) await removeLocalCa(root);
|
|
12719
|
-
const caKeys = await webcrypto.subtle.generateKey({
|
|
12720
|
-
name: "ECDSA",
|
|
12721
|
-
namedCurve: "P-256"
|
|
12722
|
-
}, false, ["sign", "verify"]);
|
|
12723
|
-
const ca = (await X509CertificateGenerator.createSelfSigned({
|
|
12724
|
-
name: `CN=${CA_COMMON_NAME}`,
|
|
12725
|
-
keys: caKeys,
|
|
12726
|
-
notAfter: new Date(Date.now() + 366 * 24 * 60 * 60 * 1e3),
|
|
12727
|
-
extensions: [
|
|
12728
|
-
new BasicConstraintsExtension(true, 0, true),
|
|
12729
|
-
new KeyUsagesExtension(KeyUsageFlags.keyCertSign, true),
|
|
12730
|
-
new ExtendedKeyUsageExtension([ExtendedKeyUsage.serverAuth], true)
|
|
12731
|
-
]
|
|
12732
|
-
})).toString("pem");
|
|
12733
|
-
const leafKeys = await webcrypto.subtle.generateKey({
|
|
12734
|
-
name: "ECDSA",
|
|
12735
|
-
namedCurve: "P-256"
|
|
12736
|
-
}, true, ["sign", "verify"]);
|
|
12737
|
-
const leafCert = await X509CertificateGenerator.create({
|
|
12738
|
-
subject: `CN=${LEAF_COMMON_NAME}`,
|
|
12739
|
-
issuer: `CN=${CA_COMMON_NAME}`,
|
|
12740
|
-
publicKey: leafKeys.publicKey,
|
|
12741
|
-
signingKey: caKeys.privateKey,
|
|
12742
|
-
notAfter: new Date(Date.now() + 365 * 24 * 60 * 60 * 1e3),
|
|
12743
|
-
extensions: [
|
|
12744
|
-
new BasicConstraintsExtension(false, void 0, true),
|
|
12745
|
-
new KeyUsagesExtension(KeyUsageFlags.digitalSignature, true),
|
|
12746
|
-
new ExtendedKeyUsageExtension([ExtendedKeyUsage.serverAuth]),
|
|
12747
|
-
new SubjectAlternativeNameExtension([{
|
|
12748
|
-
type: IP,
|
|
12749
|
-
value: "127.0.0.1"
|
|
12750
|
-
}], true)
|
|
12751
|
-
]
|
|
12752
|
-
});
|
|
12753
|
-
const key = await pemPrivateKey(leafKeys.privateKey);
|
|
12754
|
-
const cert = leafCert.toString("pem");
|
|
12755
|
-
const writes = [writeFile(join(dir, "loopback-key.pem"), key, { mode: 384 }), writeFile(join(dir, "loopback-cert.pem"), cert, { mode: 384 })];
|
|
12756
|
-
writes.push(writeFile(join(dir, "local-ca.pem"), ca, { mode: 384 }));
|
|
12757
|
-
await Promise.all(writes);
|
|
12758
|
-
await rm(previousSigningKey, { force: true });
|
|
12759
|
-
return {
|
|
12760
|
-
key,
|
|
12761
|
-
cert,
|
|
12762
|
-
ca,
|
|
12763
|
-
fingerprint: new X509Certificate(ca).fingerprint256
|
|
12764
|
-
};
|
|
12765
|
-
}
|
|
12766
|
-
async function trustLocalCa(root) {
|
|
12767
|
-
const caPath = join(root, "tls", "local-ca.pem");
|
|
12768
|
-
await execFileAsync("/usr/bin/security", [
|
|
12769
|
-
"add-trusted-cert",
|
|
12770
|
-
"-r",
|
|
12771
|
-
"trustRoot",
|
|
12772
|
-
"-p",
|
|
12773
|
-
"ssl",
|
|
12774
|
-
"-s",
|
|
12775
|
-
"127.0.0.1",
|
|
12776
|
-
"-k",
|
|
12777
|
-
loginKeychainPath(),
|
|
12778
|
-
caPath
|
|
12779
|
-
]);
|
|
12780
|
-
}
|
|
12781
|
-
async function isLocalCaTrusted(root) {
|
|
12782
|
-
try {
|
|
12783
|
-
const ca = await readFile(join(root, "tls", "local-ca.pem"), "utf8");
|
|
12784
|
-
const { stdout } = await execFileAsync("/usr/bin/security", [
|
|
12785
|
-
"find-certificate",
|
|
12786
|
-
"-a",
|
|
12787
|
-
"-p",
|
|
12788
|
-
"-c",
|
|
12789
|
-
CA_COMMON_NAME,
|
|
12790
|
-
loginKeychainPath()
|
|
12791
|
-
]);
|
|
12792
|
-
return stdout.includes(ca.trim());
|
|
12793
|
-
} catch (error) {
|
|
12794
|
-
if (error && typeof error === "object" && "code" in error && error.code === "ENOENT") return false;
|
|
12795
|
-
const stderr = error && typeof error === "object" && "stderr" in error ? error.stderr : void 0;
|
|
12796
|
-
if (typeof stderr === "string" && stderr.trim() === "security: SecKeychainSearchCopyNext: The specified item could not be found in the keychain.") return false;
|
|
12797
|
-
throw error;
|
|
12798
|
-
}
|
|
12799
|
-
}
|
|
12800
|
-
async function removeLocalCa(root) {
|
|
12801
|
-
const fingerprint = new X509Certificate(await readFile(join(root, "tls", "local-ca.pem"), "utf8")).fingerprint256.replaceAll(":", "");
|
|
12802
|
-
const caPath = join(root, "tls", "local-ca.pem");
|
|
12803
|
-
await removeTrustSettings([
|
|
12804
|
-
"remove-trusted-cert",
|
|
12805
|
-
"-d",
|
|
12806
|
-
caPath
|
|
12807
|
-
]);
|
|
12808
|
-
await removeTrustSettings(["remove-trusted-cert", caPath]);
|
|
12809
|
-
await execFileAsync("/usr/bin/security", [
|
|
12810
|
-
"delete-certificate",
|
|
12811
|
-
"-Z",
|
|
12812
|
-
fingerprint,
|
|
12813
|
-
loginKeychainPath()
|
|
12814
|
-
]);
|
|
12815
|
-
}
|
|
12816
|
-
function isMacos() {
|
|
12817
|
-
return process.platform === "darwin";
|
|
12818
|
-
}
|
|
12819
|
-
async function removeTrustSettings(args) {
|
|
12820
|
-
try {
|
|
12821
|
-
await execFileAsync("/usr/bin/security", args, { env: { LC_ALL: "C" } });
|
|
12822
|
-
} catch (error) {
|
|
12823
|
-
const stderr = error && typeof error === "object" && "stderr" in error ? error.stderr : void 0;
|
|
12824
|
-
if (typeof stderr === "string" && ["SecTrustSettingsRemoveTrustSettings: No Trust Settings were found.", "SecTrustSettingsRemoveTrustSettings: The specified item could not be found in the keychain."].includes(stderr.trim())) return;
|
|
12825
|
-
throw error;
|
|
12826
|
-
}
|
|
12827
|
-
}
|
|
12828
|
-
//#endregion
|
|
12829
12790
|
//#region src/cli/server.ts
|
|
12830
12791
|
/**
|
|
12831
12792
|
* `moltnet-agent server` — per-user loopback supervisor (#2061).
|
|
12832
12793
|
*
|
|
12833
|
-
* Starts nothing on its own: it binds 127.0.0.1
|
|
12834
|
-
*
|
|
12794
|
+
* Starts nothing on its own: it binds 127.0.0.1 or a private native socket and
|
|
12795
|
+
* waits for an authorized controller to configure agents/providers and
|
|
12796
|
+
* start or stop runs.
|
|
12835
12797
|
*/
|
|
12836
|
-
var DEFAULT_PORT = OPERATOR_OAUTH.serverPort;
|
|
12837
12798
|
var DEFAULT_ALLOWED_ORIGINS = "https://console.themolt.net";
|
|
12838
12799
|
var SHUTDOWN_TIMEOUT_MS = 15e3;
|
|
12800
|
+
var LOCK_HELD_EXIT_CODE = 75;
|
|
12801
|
+
function agentServerLockExitCode(error) {
|
|
12802
|
+
return error.code === "held" ? LOCK_HELD_EXIT_CODE : 1;
|
|
12803
|
+
}
|
|
12804
|
+
function validateNativeSocketOptions(options) {
|
|
12805
|
+
if (!options.nativeSocket) return void 0;
|
|
12806
|
+
if (!options.supervised) return "--native-socket requires --supervised";
|
|
12807
|
+
if (options.port || options.allowedOrigins) return "--native-socket cannot be combined with TCP options";
|
|
12808
|
+
}
|
|
12809
|
+
function nativeSocketValidationOptions(input) {
|
|
12810
|
+
return {
|
|
12811
|
+
...input.nativeSocket ? { nativeSocket: input.nativeSocket } : {},
|
|
12812
|
+
...input.supervised ? { supervised: true } : {},
|
|
12813
|
+
...input.cliPort ? { port: input.cliPort } : {},
|
|
12814
|
+
...input.cliAllowedOrigins ? { allowedOrigins: input.cliAllowedOrigins } : {}
|
|
12815
|
+
};
|
|
12816
|
+
}
|
|
12839
12817
|
async function runAgentServer(argv) {
|
|
12840
12818
|
if (isHelpFlag(argv)) {
|
|
12841
12819
|
console.log(AGENT_SERVER_HELP);
|
|
12842
12820
|
return 0;
|
|
12843
12821
|
}
|
|
12844
|
-
const trustRequested = argv[0] === "trust";
|
|
12845
|
-
const commandArgs = trustRequested ? argv.slice(1) : argv;
|
|
12846
|
-
const envConfig = loadAgentServerEnvConfig();
|
|
12847
|
-
if (trustRequested) return runTrustCommand(commandArgs, resolveAgentServerRoot({ root: envConfig.root }));
|
|
12848
12822
|
const { values } = parseArgs({
|
|
12849
|
-
args:
|
|
12823
|
+
args: argv,
|
|
12850
12824
|
options: {
|
|
12851
12825
|
port: { type: "string" },
|
|
12852
12826
|
"allowed-origins": { type: "string" },
|
|
@@ -12854,16 +12828,31 @@ async function runAgentServer(argv) {
|
|
|
12854
12828
|
"api-url": { type: "string" },
|
|
12855
12829
|
"heartbeat-interval-ms": { type: "string" },
|
|
12856
12830
|
"warm-retention-sec": { type: "string" },
|
|
12857
|
-
supervised: { type: "boolean" }
|
|
12831
|
+
supervised: { type: "boolean" },
|
|
12832
|
+
"native-socket": { type: "string" }
|
|
12858
12833
|
}
|
|
12859
12834
|
});
|
|
12860
|
-
const
|
|
12861
|
-
|
|
12835
|
+
const envConfig = loadAgentServerEnvConfig(values.root);
|
|
12836
|
+
const nativeSocket = values["native-socket"];
|
|
12837
|
+
const nativeSocketError = validateNativeSocketOptions(nativeSocketValidationOptions({
|
|
12838
|
+
...nativeSocket ? { nativeSocket } : {},
|
|
12839
|
+
...values.supervised ? { supervised: true } : {},
|
|
12840
|
+
...values.port ? { cliPort: values.port } : {},
|
|
12841
|
+
...values["allowed-origins"] ? { cliAllowedOrigins: values["allowed-origins"] } : {},
|
|
12842
|
+
...envConfig.port ? { envPort: envConfig.port } : {},
|
|
12843
|
+
...envConfig.allowedOrigins ? { envAllowedOrigins: envConfig.allowedOrigins } : {}
|
|
12844
|
+
}));
|
|
12845
|
+
if (nativeSocketError) {
|
|
12846
|
+
console.error(nativeSocketError);
|
|
12847
|
+
return 1;
|
|
12848
|
+
}
|
|
12849
|
+
const port = Number.parseInt(values.port ?? (envConfig.port || `${defaultAgentServerPort(envConfig.root)}`), 10);
|
|
12850
|
+
if (!Number.isInteger(port) || port < 0 || port > 65535) {
|
|
12862
12851
|
console.error(`Invalid --port: ${String(values.port)}`);
|
|
12863
12852
|
return 1;
|
|
12864
12853
|
}
|
|
12865
12854
|
const allowedOrigins = parseAllowedOrigins(values["allowed-origins"] ?? (envConfig.allowedOrigins || DEFAULT_ALLOWED_ORIGINS));
|
|
12866
|
-
const settingsRoot =
|
|
12855
|
+
const settingsRoot = envConfig.root;
|
|
12867
12856
|
const connectionSettings = new ConnectionSettingsStore(settingsRoot, {
|
|
12868
12857
|
...envConfig.operatorOAuth,
|
|
12869
12858
|
...values["api-url"] || envConfig.apiUrl ? { apiUrl: values["api-url"] || envConfig.apiUrl } : {}
|
|
@@ -12879,20 +12868,15 @@ async function runAgentServer(argv) {
|
|
|
12879
12868
|
});
|
|
12880
12869
|
try {
|
|
12881
12870
|
try {
|
|
12882
|
-
return await withAgentServerLock(
|
|
12883
|
-
const secrets =
|
|
12884
|
-
root: store.secretsDir,
|
|
12885
|
-
writable: true
|
|
12886
|
-
});
|
|
12887
|
-
const secretProviders = createNodeSecretProviderRegistry().register(secrets);
|
|
12888
|
-
const externalSecretProviders = createNodeSecretProviderRegistry();
|
|
12871
|
+
return await withAgentServerLock(settingsRoot, async () => {
|
|
12872
|
+
const { secrets, secretProviders, externalSecretProviders } = createAgentServerSecretProviders(connectionSettings, store);
|
|
12889
12873
|
const nativeGrant = new NativeGrantService();
|
|
12890
12874
|
const nativeClient = applyNativeClientGrant({
|
|
12891
12875
|
nativeGrant,
|
|
12892
12876
|
env: processEnvSnapshot()
|
|
12893
12877
|
});
|
|
12894
12878
|
if (Boolean(values.supervised) && !nativeClient) {
|
|
12895
|
-
console.error(`A supervised Agent Server requires ${NATIVE_TOKEN_ENV}. Start it from MoltNet Agent, or omit --supervised
|
|
12879
|
+
console.error(`A supervised Agent Server requires ${NATIVE_TOKEN_ENV}. Start it from MoltNet Agent, or omit --supervised for an authorized standalone controller.`);
|
|
12896
12880
|
return 1;
|
|
12897
12881
|
}
|
|
12898
12882
|
const shutdownController = new AbortController();
|
|
@@ -12909,6 +12893,7 @@ async function runAgentServer(argv) {
|
|
|
12909
12893
|
const runtimeRegistry = new RuntimeRegistry(store.root);
|
|
12910
12894
|
const runs = new RunManager({
|
|
12911
12895
|
store,
|
|
12896
|
+
storeRoot: settingsRoot,
|
|
12912
12897
|
secretProviders,
|
|
12913
12898
|
externalSecretProviders,
|
|
12914
12899
|
baseEnv: processEnvSnapshot(),
|
|
@@ -12916,8 +12901,8 @@ async function runAgentServer(argv) {
|
|
|
12916
12901
|
runtimeRegistry,
|
|
12917
12902
|
runtimeSettings
|
|
12918
12903
|
});
|
|
12919
|
-
|
|
12920
|
-
const selfOrigin =
|
|
12904
|
+
if (nativeSocket) await validateNativeSocket(nativeSocket);
|
|
12905
|
+
const selfOrigin = nativeSocket ? void 0 : `http://127.0.0.1:${port}`;
|
|
12921
12906
|
const app = buildAgentServer({
|
|
12922
12907
|
operatorOAuth: new OperatorOAuth({
|
|
12923
12908
|
issuer: connection.issuer,
|
|
@@ -12928,6 +12913,7 @@ async function runAgentServer(argv) {
|
|
|
12928
12913
|
consoleClientId: connection.consoleClientId,
|
|
12929
12914
|
callbackPort: OPERATOR_OAUTH.callbackPort
|
|
12930
12915
|
}, root),
|
|
12916
|
+
nativeOnly: Boolean(nativeSocket),
|
|
12931
12917
|
connectionSettings,
|
|
12932
12918
|
operatorApiUrl: connection.apiUrl,
|
|
12933
12919
|
store,
|
|
@@ -12939,12 +12925,8 @@ async function runAgentServer(argv) {
|
|
|
12939
12925
|
subscriptions,
|
|
12940
12926
|
providers,
|
|
12941
12927
|
runtimeRegistry,
|
|
12942
|
-
allowedOrigins,
|
|
12943
|
-
selfOrigin,
|
|
12944
|
-
...tls ? { tls: {
|
|
12945
|
-
key: tls.key,
|
|
12946
|
-
cert: tls.cert
|
|
12947
|
-
} } : {},
|
|
12928
|
+
allowedOrigins: nativeSocket ? [] : allowedOrigins,
|
|
12929
|
+
...selfOrigin ? { selfOrigin } : {},
|
|
12948
12930
|
defaultApiUrl,
|
|
12949
12931
|
runtimeSettings,
|
|
12950
12932
|
...envConfig.activeIdentity ? { activeIdentity: envConfig.activeIdentity } : {},
|
|
@@ -12952,30 +12934,43 @@ async function runAgentServer(argv) {
|
|
|
12952
12934
|
logger,
|
|
12953
12935
|
shutdownSignal: shutdownController.signal
|
|
12954
12936
|
});
|
|
12937
|
+
let endpoint;
|
|
12955
12938
|
try {
|
|
12956
|
-
const address = await app.listen({
|
|
12939
|
+
const address = await app.listen(nativeSocket ? { path: nativeSocket } : {
|
|
12957
12940
|
host: "127.0.0.1",
|
|
12958
12941
|
port
|
|
12959
12942
|
});
|
|
12943
|
+
if (nativeSocket) await chmod(nativeSocket, 384);
|
|
12944
|
+
if (!nativeSocket) {
|
|
12945
|
+
endpoint = publishAgentServerEndpoint(settingsRoot, address);
|
|
12946
|
+
console.error(`discovery: ${join(settingsRoot, "agent-server-endpoint.json")}`);
|
|
12947
|
+
}
|
|
12960
12948
|
console.error(`moltnet-agent server listening on ${address}`);
|
|
12961
|
-
console.error(`
|
|
12962
|
-
console.error(`
|
|
12949
|
+
console.error(`store root: ${settingsRoot} (${envConfig.rootSource})`);
|
|
12950
|
+
console.error(`connection state: ${root}`);
|
|
12951
|
+
if (nativeSocket) console.error(`native control socket: ${nativeSocket}`);
|
|
12952
|
+
else console.error(`allowed origins: ${allowedOrigins.join(", ")}`);
|
|
12963
12953
|
if (nativeClient) console.error("native desktop client: authorized");
|
|
12964
|
-
console.error("
|
|
12954
|
+
if (!nativeSocket) console.error("Connect from an allowed local-control client after operator authorization.");
|
|
12965
12955
|
return await waitForAgentServerShutdown(runs, app, shutdownController, Boolean(values.supervised));
|
|
12966
12956
|
} catch (cause) {
|
|
12967
12957
|
await app.close().catch(() => void 0);
|
|
12968
12958
|
throw cause;
|
|
12959
|
+
} finally {
|
|
12960
|
+
endpoint?.release();
|
|
12961
|
+
}
|
|
12962
|
+
}, {
|
|
12963
|
+
stateRoot: root,
|
|
12964
|
+
onCompromised: (error) => {
|
|
12965
|
+
console.error(error.message);
|
|
12966
|
+
process.exitCode = 1;
|
|
12967
|
+
process.kill(process.pid, "SIGTERM");
|
|
12969
12968
|
}
|
|
12970
|
-
}
|
|
12971
|
-
console.error(error.message);
|
|
12972
|
-
process.exitCode = 1;
|
|
12973
|
-
process.kill(process.pid, "SIGTERM");
|
|
12974
|
-
} });
|
|
12969
|
+
});
|
|
12975
12970
|
} catch (cause) {
|
|
12976
12971
|
if (cause instanceof AgentServerLockError) {
|
|
12977
12972
|
console.error(cause.message);
|
|
12978
|
-
return
|
|
12973
|
+
return agentServerLockExitCode(cause);
|
|
12979
12974
|
}
|
|
12980
12975
|
throw cause;
|
|
12981
12976
|
}
|
|
@@ -12983,99 +12978,6 @@ async function runAgentServer(argv) {
|
|
|
12983
12978
|
await shutdownLogger();
|
|
12984
12979
|
}
|
|
12985
12980
|
}
|
|
12986
|
-
async function runTrustCommand(argv, defaultRoot) {
|
|
12987
|
-
try {
|
|
12988
|
-
const { values } = parseArgs({
|
|
12989
|
-
args: argv,
|
|
12990
|
-
options: {
|
|
12991
|
-
root: { type: "string" },
|
|
12992
|
-
remove: { type: "boolean" },
|
|
12993
|
-
status: { type: "boolean" },
|
|
12994
|
-
yes: { type: "boolean" },
|
|
12995
|
-
json: { type: "boolean" }
|
|
12996
|
-
}
|
|
12997
|
-
});
|
|
12998
|
-
const root = values.root ?? defaultRoot;
|
|
12999
|
-
const statusRequested = Boolean(values.status);
|
|
13000
|
-
const removeRequested = Boolean(values.remove);
|
|
13001
|
-
const yes = Boolean(values.yes);
|
|
13002
|
-
const json = Boolean(values.json);
|
|
13003
|
-
if (statusRequested && (removeRequested || yes)) {
|
|
13004
|
-
console.error("Usage: moltnet-agent server trust --status [--json]");
|
|
13005
|
-
return 1;
|
|
13006
|
-
}
|
|
13007
|
-
if (!isMacos()) {
|
|
13008
|
-
if (json) {
|
|
13009
|
-
printTrustStatus({
|
|
13010
|
-
supported: false,
|
|
13011
|
-
trusted: false,
|
|
13012
|
-
fingerprint: null
|
|
13013
|
-
});
|
|
13014
|
-
return 0;
|
|
13015
|
-
}
|
|
13016
|
-
console.error("Local HTTPS trust setup is currently supported on macOS only.");
|
|
13017
|
-
return 1;
|
|
13018
|
-
}
|
|
13019
|
-
if (statusRequested) {
|
|
13020
|
-
const material = await inspectLocalTlsMaterial(root);
|
|
13021
|
-
const trusted = material !== null && await isLocalCaTrusted(root);
|
|
13022
|
-
if (json) printTrustStatus({
|
|
13023
|
-
supported: true,
|
|
13024
|
-
trusted,
|
|
13025
|
-
fingerprint: material?.fingerprint ?? null
|
|
13026
|
-
});
|
|
13027
|
-
else console.log(trusted ? `MoltNet local CA ${material?.fingerprint ?? "(not prepared)"} is trusted.` : `MoltNet local CA ${material?.fingerprint ?? "(not prepared)"} is not trusted.`);
|
|
13028
|
-
return 0;
|
|
13029
|
-
}
|
|
13030
|
-
if (json && !yes) {
|
|
13031
|
-
console.error("Machine-readable trust changes require --yes after native app consent.");
|
|
13032
|
-
return 1;
|
|
13033
|
-
}
|
|
13034
|
-
if (removeRequested) {
|
|
13035
|
-
await removeLocalCa(root);
|
|
13036
|
-
if (json) printTrustStatus({
|
|
13037
|
-
supported: true,
|
|
13038
|
-
trusted: false,
|
|
13039
|
-
fingerprint: null
|
|
13040
|
-
});
|
|
13041
|
-
else console.log("Removed the MoltNet local CA from your login keychain.");
|
|
13042
|
-
return 0;
|
|
13043
|
-
}
|
|
13044
|
-
const material = await ensureLocalTlsMaterial(root);
|
|
13045
|
-
if (yes) await trustLocalCa(root);
|
|
13046
|
-
else await ensureTrustedLocalTls(root);
|
|
13047
|
-
if (json) printTrustStatus({
|
|
13048
|
-
supported: true,
|
|
13049
|
-
trusted: await isLocalCaTrusted(root),
|
|
13050
|
-
fingerprint: material.fingerprint
|
|
13051
|
-
});
|
|
13052
|
-
else console.log("MoltNet local HTTPS trust is ready for this macOS user.");
|
|
13053
|
-
return 0;
|
|
13054
|
-
} catch (cause) {
|
|
13055
|
-
console.error(`Agent Server trust command failed: ${cause instanceof Error ? cause.message : String(cause)}`);
|
|
13056
|
-
return 1;
|
|
13057
|
-
}
|
|
13058
|
-
}
|
|
13059
|
-
function printTrustStatus(status) {
|
|
13060
|
-
console.log(JSON.stringify(status));
|
|
13061
|
-
}
|
|
13062
|
-
async function ensureTrustedLocalTls(root) {
|
|
13063
|
-
const material = await ensureLocalTlsMaterial(root);
|
|
13064
|
-
if (await isLocalCaTrusted(root)) return material;
|
|
13065
|
-
if (!process.stdin.isTTY || !process.stdout.isTTY) throw new Error("Local HTTPS trust is not configured. Run `moltnet-agent server trust` from an interactive terminal.");
|
|
13066
|
-
const prompt = createInterface({
|
|
13067
|
-
input: process.stdin,
|
|
13068
|
-
output: process.stdout
|
|
13069
|
-
});
|
|
13070
|
-
try {
|
|
13071
|
-
const answer = await prompt.question(`Trust MoltNet's local CA (${material.fingerprint}) in this macOS login keychain? [y/N] `);
|
|
13072
|
-
if (!/^y(es)?$/i.test(answer.trim())) throw new Error("Local HTTPS trust was not approved.");
|
|
13073
|
-
} finally {
|
|
13074
|
-
prompt.close();
|
|
13075
|
-
}
|
|
13076
|
-
await trustLocalCa(root);
|
|
13077
|
-
return material;
|
|
13078
|
-
}
|
|
13079
12981
|
function waitForAgentServerShutdown(runs, app, shutdownController, supervised) {
|
|
13080
12982
|
return new Promise((resolvePromise) => {
|
|
13081
12983
|
let shuttingDown = false;
|
|
@@ -13469,6 +13371,7 @@ function compareVersions(a, b) {
|
|
|
13469
13371
|
}
|
|
13470
13372
|
function cachePath() {
|
|
13471
13373
|
const env = loadUpdateEnvConfig();
|
|
13374
|
+
if (env.storeRoot) return join(env.storeRoot, "cache", "updates", "agent.json");
|
|
13472
13375
|
return join(env.xdgCacheHome || (platform() === "win32" ? env.localAppData || join(homedir(), "AppData", "Local") : join(homedir(), ".cache")), "moltnet", "updates", "agent.json");
|
|
13473
13376
|
}
|
|
13474
13377
|
async function readCache() {
|
|
@@ -13488,16 +13391,14 @@ async function writeCache(cache) {
|
|
|
13488
13391
|
}
|
|
13489
13392
|
//#endregion
|
|
13490
13393
|
//#region src/version.ts
|
|
13491
|
-
var DAEMON_VERSION = "0.
|
|
13394
|
+
var DAEMON_VERSION = "0.63.0";
|
|
13492
13395
|
//#endregion
|
|
13493
13396
|
//#region src/cli.ts
|
|
13494
13397
|
async function runAgentDaemonCli(options) {
|
|
13398
|
+
const notice = legacyStoreNotice();
|
|
13399
|
+
if (notice) console.error(notice);
|
|
13495
13400
|
const [subcommand, ...rest] = options.argv ?? process.argv.slice(2);
|
|
13496
|
-
if ([
|
|
13497
|
-
"poll",
|
|
13498
|
-
"drain",
|
|
13499
|
-
"serve"
|
|
13500
|
-
].includes(subcommand ?? "") && !isWorkspaceInvocation()) checkDaemonUpdate({ currentVersion: DAEMON_VERSION }).then((update) => {
|
|
13401
|
+
if (["poll", "drain"].includes(subcommand ?? "") && !isWorkspaceInvocation()) checkDaemonUpdate({ currentVersion: DAEMON_VERSION }).then((update) => {
|
|
13501
13402
|
if (update.updateAvailable) pino({ name: "moltnet.agent-daemon" }).info({
|
|
13502
13403
|
event: "agent-daemon.update_available",
|
|
13503
13404
|
update
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@themoltnet/agent-daemon",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.63.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.",
|
|
@@ -75,7 +75,6 @@
|
|
|
75
75
|
"@opentelemetry/sdk-trace-base": "^2.5.1",
|
|
76
76
|
"@opentelemetry/sdk-trace-node": "^2.5.1",
|
|
77
77
|
"@opentelemetry/semantic-conventions": "^1.39.0",
|
|
78
|
-
"@peculiar/x509": "^2.0.0",
|
|
79
78
|
"fastify": "^5.8.5",
|
|
80
79
|
"fastify-plugin": "^5.1.0",
|
|
81
80
|
"jose": "^6.1.3",
|
|
@@ -84,12 +83,11 @@
|
|
|
84
83
|
"pino-opentelemetry-transport": "^3.0.0",
|
|
85
84
|
"pino-pretty": "^13.1.3",
|
|
86
85
|
"proper-lockfile": "4.1.2",
|
|
87
|
-
"reflect-metadata": "^0.2.2",
|
|
88
86
|
"typebox": "^1.2.8",
|
|
89
|
-
"@themoltnet/agent-runtime": "1.
|
|
90
|
-
"@themoltnet/
|
|
91
|
-
"@themoltnet/
|
|
92
|
-
"@themoltnet/
|
|
87
|
+
"@themoltnet/agent-runtime": "1.3.0",
|
|
88
|
+
"@themoltnet/pi-runtime": "0.18.3",
|
|
89
|
+
"@themoltnet/sdk": "0.145.0",
|
|
90
|
+
"@themoltnet/os-keyring": "0.4.0"
|
|
93
91
|
},
|
|
94
92
|
"devDependencies": {
|
|
95
93
|
"@fastify/swagger": "^9.6.1",
|
|
@@ -100,12 +98,12 @@
|
|
|
100
98
|
"vite-plugin-dts": "^4.5.4",
|
|
101
99
|
"vitest": "^3.0.0",
|
|
102
100
|
"@moltnet/agent-eval": "0.1.0",
|
|
103
|
-
"@moltnet/crypto-service": "0.1.0",
|
|
104
101
|
"@moltnet/api-client": "0.1.0",
|
|
105
|
-
"@moltnet/
|
|
102
|
+
"@moltnet/crypto-service": "0.1.0",
|
|
106
103
|
"@moltnet/execution-integrations": "0.1.0",
|
|
107
104
|
"@moltnet/execution-plan": "0.1.0",
|
|
108
105
|
"@moltnet/loopback-companion": "0.1.0",
|
|
106
|
+
"@moltnet/bootstrap": "0.1.0",
|
|
109
107
|
"@moltnet/models": "0.1.0",
|
|
110
108
|
"@moltnet/observability": "0.1.0",
|
|
111
109
|
"@moltnet/runtime-profiles": "0.1.0",
|
|
@@ -139,6 +137,8 @@
|
|
|
139
137
|
"inputs": [
|
|
140
138
|
"default",
|
|
141
139
|
"^production",
|
|
140
|
+
"{workspaceRoot}/test-fixtures/agent-server-endpoint.json",
|
|
141
|
+
"{workspaceRoot}/test-fixtures/connection-state-root.json",
|
|
142
142
|
"{workspaceRoot}/docs/operate/*.md",
|
|
143
143
|
"{workspaceRoot}/docs/.vitepress/config.ts"
|
|
144
144
|
]
|
|
@@ -147,7 +147,7 @@
|
|
|
147
147
|
},
|
|
148
148
|
"scripts": {
|
|
149
149
|
"cli": "tsx src/main.ts",
|
|
150
|
-
"dev": "tsx watch src/main.ts",
|
|
150
|
+
"dev": "tsx ../../tools/src/desktop-development.ts tsx watch src/main.ts",
|
|
151
151
|
"start": "node dist/main.js",
|
|
152
152
|
"build": "vite build",
|
|
153
153
|
"generate:openapi": "tsx scripts/generate-openapi.ts",
|