@themoltnet/agent-daemon 0.62.0 → 0.63.1

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 CHANGED
@@ -649,10 +649,36 @@ Server configuration root. **Reset to release defaults** clears those overrides
649
649
  when applied; it does not delete agent credentials. Launch environment values
650
650
  have priority and appear read-only. Operators who explicitly set both
651
651
  `MOLTNET_OPERATOR_API_URL` and `MOLTNET_OPERATOR_OAUTH_ISSUER` should also use
652
- `MOLTNET_AGENT_SERVER_ROOT` to select a dedicated environment root.
652
+ `MOLTNET_HOME` to select a dedicated environment root.
653
653
 
654
654
  Release defaults use `https://api.themolt.net`, `https://auth.themolt.net`, and
655
655
  public client IDs `moltnet-native` / `moltnet-console`. Hosted client registration
656
656
  and coordinated configuration are tracked in
657
657
  [moltnet-operations #8](https://github.com/getlarge/moltnet-operations/issues/8).
658
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,CA6ClB"}
1
+ {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"AAiBA,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,CAyDlB"}
package/dist/cli.js CHANGED
@@ -11,7 +11,7 @@ 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";
@@ -3757,6 +3757,13 @@ async function resolveSelectionApiUrl(agentName, options) {
3757
3757
  * here, so the rest of the daemon imports typed values rather than
3758
3758
  * sprinkling string lookups across the codebase.
3759
3759
  */
3760
+ /** All daemon commands announce the same legacy alias policy as Go and Desktop. */
3761
+ function markLegacyStoreNoticeShown() {
3762
+ process.env["MOLTNET_LEGACY_STORE_NOTICE_SHOWN"] = "1";
3763
+ }
3764
+ function legacyStoreNotice() {
3765
+ return process.env["MOLTNET_AGENT_SERVER_ROOT"] !== void 0 && process.env["MOLTNET_LEGACY_STORE_NOTICE_SHOWN"] !== "1" ? "MOLTNET_AGENT_SERVER_ROOT is deprecated; use MOLTNET_HOME. Both select the entire store; no data is migrated." : void 0;
3766
+ }
3760
3767
  function loadConfig() {
3761
3768
  assertSingleCredentialForm("MOLTNET_AGENT_KEY", "MOLTNET_AGENT_KEY_REF");
3762
3769
  assertSingleCredentialForm("MOLTNET_PRIVATE_KEY", "MOLTNET_PRIVATE_KEY_REF");
@@ -3767,7 +3774,7 @@ function loadConfig() {
3767
3774
  profilePrerequisiteEnv: process.env,
3768
3775
  profilePrerequisitePath: process.env.PATH ?? "",
3769
3776
  piCodingAgentDir: process.env["PI_CODING_AGENT_DIR"] ?? "",
3770
- agentServerRoot: process.env["MOLTNET_AGENT_SERVER_ROOT"] ?? "",
3777
+ agentServerRoot: resolveStoreRoot(),
3771
3778
  credentialSource: detectCredentialSource(process.env),
3772
3779
  apiUrl: process.env["MOLTNET_API_URL"] ?? "",
3773
3780
  signingPrivateKey: process.env["MOLTNET_PRIVATE_KEY"] ?? "",
@@ -3815,7 +3822,8 @@ function activatePiCodingAgentDir(path, env = {}) {
3815
3822
  process.env["PI_CODING_AGENT_DIR"] = path;
3816
3823
  Object.assign(process.env, env);
3817
3824
  }
3818
- function loadAgentServerEnvConfig() {
3825
+ function loadAgentServerEnvConfig(root) {
3826
+ const selection = resolveStoreSelection({ root });
3819
3827
  const issuer = process.env["MOLTNET_OPERATOR_OAUTH_ISSUER"];
3820
3828
  const publicUrl = process.env["MOLTNET_OPERATOR_OAUTH_PUBLIC_URL"] ?? issuer;
3821
3829
  const nativeClientId = process.env["MOLTNET_NATIVE_OAUTH_CLIENT_ID"];
@@ -3831,7 +3839,8 @@ function loadAgentServerEnvConfig() {
3831
3839
  },
3832
3840
  port: process.env["MOLTNET_AGENT_SERVER_PORT"] ?? "",
3833
3841
  allowedOrigins: process.env["MOLTNET_AGENT_SERVER_ALLOWED_ORIGINS"] ?? "",
3834
- root: process.env["MOLTNET_AGENT_SERVER_ROOT"] ?? "",
3842
+ root: selection.root,
3843
+ rootSource: selection.source,
3835
3844
  apiUrl: process.env["MOLTNET_API_URL"] ?? "",
3836
3845
  logLevel: process.env["LOG_LEVEL"] ?? "",
3837
3846
  activeIdentity: process.env["MOLTNET_ACTIVE_IDENTITY"] ?? ""
@@ -3841,8 +3850,9 @@ function loadAgentServerEnvConfig() {
3841
3850
  function processEnvSnapshot() {
3842
3851
  return process.env;
3843
3852
  }
3844
- function loadUpdateEnvConfig() {
3853
+ function loadUpdateEnvConfig(root) {
3845
3854
  return {
3855
+ ...!isDefaultStore({ root }) ? { storeRoot: resolveStoreRoot({ root }) } : {},
3846
3856
  xdgCacheHome: process.env["XDG_CACHE_HOME"] ?? "",
3847
3857
  localAppData: process.env["LOCALAPPDATA"] ?? ""
3848
3858
  };
@@ -4162,7 +4172,9 @@ Example:
4162
4172
  --agent legreffier \\
4163
4173
  --state idle`;
4164
4174
  function isHelpFlag(args) {
4165
- return args.includes("--help") || args.includes("-h");
4175
+ const separator = args.indexOf("--");
4176
+ const flags = separator === -1 ? args : args.slice(0, separator);
4177
+ return flags.includes("--help") || flags.includes("-h");
4166
4178
  }
4167
4179
  var AGENT_SERVER_HELP = `\
4168
4180
  agent-daemon server — local supervisor for managed runs.
@@ -4172,14 +4184,14 @@ Agent Desktop instead uses a private Unix socket with a process-scoped grant.
4172
4184
  Both modes configure agents and providers and start/stop child runs.
4173
4185
 
4174
4186
  Options:
4175
- --port <n> Loopback port. Default: 17374.
4187
+ --port <n> Loopback port. Default store: 17374; isolated stores: an available port.
4176
4188
  Env: MOLTNET_AGENT_SERVER_PORT.
4177
4189
  --allowed-origins <csv> Exact browser-controller origins allowed
4178
4190
  local control.
4179
4191
  Default: https://console.themolt.net.
4180
4192
  Env: MOLTNET_AGENT_SERVER_ALLOWED_ORIGINS.
4181
4193
  --root <path> Config root. Default: ~/.config/moltnet
4182
- (or MOLTNET_AGENT_SERVER_ROOT).
4194
+ (or MOLTNET_HOME; legacy MOLTNET_AGENT_SERVER_ROOT).
4183
4195
  --api-url <url> Default MoltNet API for new managed agents.
4184
4196
  Default: https://api.themolt.net.
4185
4197
  --heartbeat-interval-ms <n> Child reporter heartbeat cadence. Default: 60000.
@@ -4208,8 +4220,8 @@ Usage:
4208
4220
  [--root <path>]
4209
4221
  moltnet-agent providers logout <id> [--yes] [--root <path>]
4210
4222
 
4211
- The default root is ~/.config/moltnet. MOLTNET_AGENT_SERVER_ROOT remains the
4212
- environment override. API keys are accepted only from redirected stdin; they
4223
+ The default root is ~/.config/moltnet. MOLTNET_HOME selects another store.
4224
+ MOLTNET_AGENT_SERVER_ROOT is a legacy alias; conflicting values fail. API keys are accepted only from redirected stdin; they
4213
4225
  are stored separately and providers.json contains only a secret reference.
4214
4226
 
4215
4227
  --model declares a text-only model. --model-input declares a model together
@@ -5655,20 +5667,9 @@ var AgentServerStoreError = class extends Error {
5655
5667
  this.code = code;
5656
5668
  }
5657
5669
  };
5658
- /**
5659
- * `MOLTNET_AGENT_SERVER_ROOT` override, else `~/.config/moltnet`.
5660
- *
5661
- * Deliberately does NOT consult `XDG_CONFIG_HOME`. The Go CLI's GetConfigDir
5662
- * and @moltnet/agent-config's getConfigDir both resolve `~/.config/moltnet`,
5663
- * so honouring XDG here gave one application two config roots: on a machine
5664
- * with the variable set, the daemon wrote identities the CLI and SDK could not
5665
- * read. `MOLTNET_AGENT_SERVER_ROOT` remains the explicit escape hatch for a
5666
- * genuinely custom location.
5667
- */
5670
+ /** Explicit root, MOLTNET_HOME, legacy alias, then the unchanged default store. */
5668
5671
  function resolveAgentServerRoot(input) {
5669
- const override = input.root?.trim();
5670
- if (override) return override;
5671
- return getConfigDir();
5672
+ return getConfigDir(input);
5672
5673
  }
5673
5674
  /**
5674
5675
  * Copy a model entry, dropping an empty `input` so it never reaches the wire.
@@ -6102,7 +6103,7 @@ async function resolvePiAgentDir(cfg, agentRoot, profiles, options = {}) {
6102
6103
  };
6103
6104
  }
6104
6105
  const repoPiDir = join(agentRoot, ".pi");
6105
- const store = new AgentServerStore(resolveAgentServerRoot({ root: cfg.agentServerRoot }));
6106
+ const store = new AgentServerStore(resolveAgentServerRoot({ root: cfg.agentServerRoot || void 0 }));
6106
6107
  const providers = store.readProviders();
6107
6108
  const storeHasAuth = existsSync(store.piAuthJsonPath);
6108
6109
  if (Object.keys(providers).length === 0 && !storeHasAuth) {
@@ -6183,7 +6184,7 @@ function mergePiModels(store, repo) {
6183
6184
  //#endregion
6184
6185
  //#region src/lib/state-dir.ts
6185
6186
  function ensureDaemonStateDirs(mountPath) {
6186
- const rootDir = canonicalStatePath(join(mountPath, ".moltnet", "d"));
6187
+ const rootDir = canonicalStoreRoot(join(mountPath, ".moltnet", "d"));
6187
6188
  const piSessionsDir = join(rootDir, "pi-sessions");
6188
6189
  mkdirSync(rootDir, {
6189
6190
  recursive: true,
@@ -6198,17 +6199,6 @@ function ensureDaemonStateDirs(mountPath) {
6198
6199
  piSessionsDir: realpathSync(piSessionsDir)
6199
6200
  };
6200
6201
  }
6201
- function canonicalStatePath(path) {
6202
- let parent = resolve(path);
6203
- const missing = [];
6204
- while (!existsSync(parent)) {
6205
- missing.unshift(basename(parent));
6206
- const next = dirname(parent);
6207
- if (next === parent) throw new Error(`Cannot resolve daemon state root ${path}`);
6208
- parent = next;
6209
- }
6210
- return join(realpathSync(parent), ...missing);
6211
- }
6212
6202
  //#endregion
6213
6203
  //#region src/lib/prepare-runtime-profile.ts
6214
6204
  /** Validate and prepare a profile through the shared daemon execution path. */
@@ -8865,12 +8855,13 @@ function parseProviderArgs(command, args) {
8865
8855
  }
8866
8856
  }
8867
8857
  async function createContext(rootFlag, dependencies, installInterruptHandler) {
8868
- const store = new AgentServerStore(resolveAgentServerRoot({ root: rootFlag ?? dependencies.envRoot ?? loadAgentServerEnvConfig().root })).ensure();
8858
+ const root = resolveAgentServerRoot({ root: rootFlag ?? dependencies.envRoot ?? loadAgentServerEnvConfig().root });
8859
+ const store = new AgentServerStore(root).ensure();
8869
8860
  const secrets = new FileSecretProvider({
8870
8861
  root: store.secretsDir,
8871
8862
  writable: true
8872
8863
  });
8873
- const secretProviders = createNodeSecretProviderRegistry().register(secrets);
8864
+ const secretProviders = createNodeSecretProviderRegistry({ store: { root } }).register(secrets);
8874
8865
  const stderr = dependencies.stderr ?? console.error;
8875
8866
  const logger = createCliLogger(stderr);
8876
8867
  const configuration = dependencies.configuration ?? new ProviderConfigurationService({
@@ -9358,11 +9349,67 @@ var ConnectionSettingsStore = class {
9358
9349
  };
9359
9350
  /** A custom service gets its own identities, keys and runtime configuration. */
9360
9351
  function connectionStateRoot(root, settings) {
9361
- const identity = [settings.apiUrl.replace(/\/$/u, ""), settings.issuer.replace(/\/$/u, "")];
9352
+ const identity = [settings.apiUrl.replace(/\/+$/u, ""), settings.issuer.replace(/\/+$/u, "")];
9362
9353
  if (identity[0] === RELEASE_CONNECTION.apiUrl && identity[1] === RELEASE_CONNECTION.issuer) return root;
9363
9354
  return join(root, "environments", createHash("sha256").update(JSON.stringify(identity)).digest("hex"));
9364
9355
  }
9365
9356
  //#endregion
9357
+ //#region src/lib/agent-server/endpoint.ts
9358
+ function defaultAgentServerPort(root) {
9359
+ return isDefaultStore({ root }) ? OPERATOR_OAUTH.serverPort : 0;
9360
+ }
9361
+ function endpointPath(root) {
9362
+ return join(root, "agent-server-endpoint.json");
9363
+ }
9364
+ function validateUrl(value) {
9365
+ const url = new URL(value);
9366
+ 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");
9367
+ }
9368
+ function readAgentServerEndpoint(root) {
9369
+ let raw;
9370
+ try {
9371
+ raw = readFileSync(endpointPath(root), "utf8");
9372
+ } catch (cause) {
9373
+ if (cause.code === "ENOENT") return null;
9374
+ throw cause;
9375
+ }
9376
+ try {
9377
+ const value = JSON.parse(raw);
9378
+ 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");
9379
+ validateUrl(value.url);
9380
+ if (value.pid !== void 0) {
9381
+ if (!Number.isSafeInteger(value.pid) || value.pid <= 0 || value.pid > 2147483647) throw new Error("Invalid process ID");
9382
+ try {
9383
+ process.kill(value.pid, 0);
9384
+ } catch (cause) {
9385
+ if (cause.code === "ESRCH") return null;
9386
+ }
9387
+ }
9388
+ return value;
9389
+ } catch (cause) {
9390
+ throw new Error(`Invalid Agent Server discovery at ${endpointPath(root)}: ${cause instanceof Error ? cause.message : String(cause)}`, { cause });
9391
+ }
9392
+ }
9393
+ /** Publish and release only while holding the selected store's singleton lock. */
9394
+ function publishAgentServerEndpoint(root, url) {
9395
+ validateUrl(url);
9396
+ const endpoint = {
9397
+ version: 1,
9398
+ instanceId: randomUUID(),
9399
+ pid: process.pid,
9400
+ url
9401
+ };
9402
+ writeJsonAtomic(endpointPath(root), endpoint);
9403
+ return {
9404
+ record: endpoint,
9405
+ release() {
9406
+ try {
9407
+ if (readAgentServerEndpoint(root)?.instanceId === endpoint.instanceId) rmSync(endpointPath(root), { force: true });
9408
+ } catch {}
9409
+ }
9410
+ };
9411
+ }
9412
+ //#endregion
9366
9413
  //#region src/lib/agent-server/lock.ts
9367
9414
  var AgentServerLockError = class extends Error {
9368
9415
  name = "AgentServerLockError";
@@ -9396,7 +9443,14 @@ async function acquireAgentServerLock(root, options = {}) {
9396
9443
  }
9397
9444
  });
9398
9445
  } catch (cause) {
9399
- if (cause.code === "ELOCKED") throw new AgentServerLockError("held", `another moltnet-agent server process already owns ${path}`, { cause });
9446
+ if (cause.code === "ELOCKED") {
9447
+ let holder = "";
9448
+ try {
9449
+ const endpoint = readAgentServerEndpoint(root);
9450
+ if (endpoint?.pid) holder = ` (PID ${endpoint.pid}, instance ${endpoint.instanceId})`;
9451
+ } catch {}
9452
+ throw new AgentServerLockError("held", `another moltnet-agent server process already owns ${path}${holder}; stop that process before retrying`, { cause });
9453
+ }
9400
9454
  throw new AgentServerLockError("failed", `could not acquire Agent Server lock ${path}: ${cause.message}`, { cause });
9401
9455
  }
9402
9456
  let released = false;
@@ -9411,11 +9465,15 @@ async function acquireAgentServerLock(root, options = {}) {
9411
9465
  }
9412
9466
  /** Always release after normal completion or startup/runtime failure. */
9413
9467
  async function withAgentServerLock(root, work, options) {
9414
- const held = await acquireAgentServerLock(root, options);
9468
+ const held = [];
9415
9469
  try {
9470
+ held.push(await acquireAgentServerLock(root, options));
9471
+ if (options?.stateRoot && options.stateRoot !== root) held.push(await acquireAgentServerLock(options.stateRoot, options));
9416
9472
  return await work();
9417
9473
  } finally {
9418
- await held.release();
9474
+ for (const entry of held.reverse()) await entry.release().catch((error) => {
9475
+ console.error(`Could not release Agent Server lock ${entry.path}: ${String(error)}`);
9476
+ });
9419
9477
  }
9420
9478
  }
9421
9479
  //#endregion
@@ -9450,6 +9508,10 @@ function applyNativeClientGrant(options) {
9450
9508
  nativeGrant.grantNative(token);
9451
9509
  return true;
9452
9510
  }
9511
+ //#endregion
9512
+ //#region src/lib/agent-server/native-grant-service.ts
9513
+ /** Process-only native grant. Browser authorization belongs to OperatorOAuth. */
9514
+ var NATIVE_CLIENT_ORIGIN = "moltnet-agent-desktop://native";
9453
9515
  var NativeGrantError = class extends Error {
9454
9516
  code = "native_token_invalid";
9455
9517
  };
@@ -10585,6 +10647,8 @@ var RunManager = class {
10585
10647
  const homeDir = join(dirname(piDir), "home");
10586
10648
  const env = {
10587
10649
  HOME: homeDir,
10650
+ MOLTNET_DEFAULT_STORE_ROOT: defaultStoreRoot({ env: this.options.baseEnv }),
10651
+ MOLTNET_HOME: getConfigDir({ root: this.options.storeRoot ?? this.store.root }),
10588
10652
  PI_CODING_AGENT_DIR: piDir,
10589
10653
  XDG_CACHE_HOME: join(homeDir, ".cache"),
10590
10654
  XDG_CONFIG_HOME: join(homeDir, ".config"),
@@ -10628,7 +10692,7 @@ var RunManager = class {
10628
10692
  env["MOLTNET_AGENT_KEY"] = requireCredentialSnapshot(agent).agentKey;
10629
10693
  env["MOLTNET_API_URL"] = activation.apiUrl ?? (activation.source === "external" ? activation.configApiUrl : "");
10630
10694
  if (activation.source === "managed") {
10631
- if (!config.keys.private_key_ref) throw new AgentServerRunError("invalid_spec", "The managed signing key reference is missing");
10695
+ 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.");
10632
10696
  env["MOLTNET_PRIVATE_KEY_REF"] = formatSecretReferenceString(config.keys.private_key_ref);
10633
10697
  env["MOLTNET_SECRET_ROOT"] = this.store.secretsDir;
10634
10698
  } else try {
@@ -11231,6 +11295,20 @@ function writeRegistry(path, entries) {
11231
11295
  renameSync(temp, path);
11232
11296
  }
11233
11297
  //#endregion
11298
+ //#region src/lib/agent-server/secret-providers.ts
11299
+ function createAgentServerSecretProviders(settings, store) {
11300
+ const secrets = new FileSecretProvider({
11301
+ root: store.secretsDir,
11302
+ writable: true
11303
+ });
11304
+ const registry = () => createNodeSecretProviderRegistry({ store: { root: settings.root } });
11305
+ return {
11306
+ secrets,
11307
+ secretProviders: registry().register(secrets),
11308
+ externalSecretProviders: registry()
11309
+ };
11310
+ }
11311
+ //#endregion
11234
11312
  //#region src/lib/agent-server/readiness.ts
11235
11313
  /**
11236
11314
  * Whether a runtime profile can execute on *this* machine.
@@ -11997,7 +12075,8 @@ function requestOperationSignal(request, shutdownSignal) {
11997
12075
  else request.raw.once("aborted", () => disconnected.abort({ source: "request" }));
11998
12076
  return shutdownSignal ? AbortSignal.any([disconnected.signal, shutdownSignal]) : disconnected.signal;
11999
12077
  }
12000
- function buildAgentServer(options) {
12078
+ function buildAgentServer(input) {
12079
+ const options = { ...input };
12001
12080
  const { nativeGrant } = options;
12002
12081
  const oauth = options.operatorOAuth;
12003
12082
  let restartRequired = false;
@@ -12006,12 +12085,13 @@ function buildAgentServer(options) {
12006
12085
  ...fastifyOptions,
12007
12086
  loggerInstance: options.logger
12008
12087
  }) : Fastify(fastifyOptions);
12088
+ app.addHook("onListen", async () => {
12089
+ const address = app.server.address();
12090
+ if (address && typeof address !== "string") options.selfOrigin = `http://127.0.0.1:${address.port}`;
12091
+ });
12009
12092
  options.registerOpenApi?.(app);
12010
12093
  for (const schema of AGENT_SERVER_SCHEMAS) app.addSchema(schema);
12011
- const browserOrigins = new OriginAllowlist(options.allowedOrigins);
12012
- registerLoopbackSecurity(app, {
12013
- isOriginAllowed: (origin) => origin === "moltnet-agent-desktop://native" || browserOrigins.has(origin),
12014
- ...options.selfOrigin ? { selfOrigins: [options.selfOrigin] } : {},
12094
+ const securityOptions = {
12015
12095
  allowedHeaders: [AGENT_SERVER_TOKEN_HEADER],
12016
12096
  methods: [
12017
12097
  "GET",
@@ -12020,7 +12100,18 @@ function buildAgentServer(options) {
12020
12100
  "DELETE",
12021
12101
  "OPTIONS"
12022
12102
  ]
12103
+ };
12104
+ if (options.nativeOnly) registerLoopbackSecurity(app, {
12105
+ ...securityOptions,
12106
+ isOriginAllowed: (origin) => origin === NATIVE_CLIENT_ORIGIN
12023
12107
  });
12108
+ else {
12109
+ const browserOrigins = new OriginAllowlist(options.allowedOrigins);
12110
+ registerLoopbackSecurity(app, {
12111
+ ...securityOptions,
12112
+ isOriginAllowed: (origin) => origin === "moltnet-agent-desktop://native" || origin === options.selfOrigin || browserOrigins.has(origin)
12113
+ });
12114
+ }
12024
12115
  let verificationWindow = Date.now();
12025
12116
  let verifications = 0;
12026
12117
  const browserVerification = /* @__PURE__ */ new WeakMap();
@@ -12641,7 +12732,7 @@ function corsHeadersFor(request, options) {
12641
12732
  return {};
12642
12733
  }
12643
12734
  function isConfiguredOrigin(origin, options) {
12644
- return typeof origin === "string" && (origin === "moltnet-agent-desktop://native" || options.allowedOrigins.includes(origin) || origin === options.selfOrigin);
12735
+ return typeof origin === "string" && (origin === "moltnet-agent-desktop://native" || !options.nativeOnly && (options.allowedOrigins.includes(origin) || origin === options.selfOrigin));
12645
12736
  }
12646
12737
  function normalizeAgentServerError(error) {
12647
12738
  if (error instanceof TeamCredentialError) return {
@@ -12670,7 +12761,7 @@ function normalizeAgentServerError(error) {
12670
12761
  message: error.message
12671
12762
  };
12672
12763
  if (error instanceof AgentServerRunError) return {
12673
- statusCode: error.code === "run_not_found" ? 404 : 400,
12764
+ statusCode: error.code === "run_not_found" ? 404 : error.code === "invalid_store" ? 409 : 400,
12674
12765
  code: error.code,
12675
12766
  message: error.message
12676
12767
  };
@@ -12709,7 +12800,6 @@ function normalizeAgentServerError(error) {
12709
12800
  * waits for an authorized controller to configure agents/providers and
12710
12801
  * start or stop runs.
12711
12802
  */
12712
- var DEFAULT_PORT = OPERATOR_OAUTH.serverPort;
12713
12803
  var DEFAULT_ALLOWED_ORIGINS = "https://console.themolt.net";
12714
12804
  var SHUTDOWN_TIMEOUT_MS = 15e3;
12715
12805
  var LOCK_HELD_EXIT_CODE = 75;
@@ -12734,7 +12824,6 @@ async function runAgentServer(argv) {
12734
12824
  console.log(AGENT_SERVER_HELP);
12735
12825
  return 0;
12736
12826
  }
12737
- const envConfig = loadAgentServerEnvConfig();
12738
12827
  const { values } = parseArgs({
12739
12828
  args: argv,
12740
12829
  options: {
@@ -12748,6 +12837,7 @@ async function runAgentServer(argv) {
12748
12837
  "native-socket": { type: "string" }
12749
12838
  }
12750
12839
  });
12840
+ const envConfig = loadAgentServerEnvConfig(values.root);
12751
12841
  const nativeSocket = values["native-socket"];
12752
12842
  const nativeSocketError = validateNativeSocketOptions(nativeSocketValidationOptions({
12753
12843
  ...nativeSocket ? { nativeSocket } : {},
@@ -12761,13 +12851,14 @@ async function runAgentServer(argv) {
12761
12851
  console.error(nativeSocketError);
12762
12852
  return 1;
12763
12853
  }
12764
- const port = Number.parseInt(values.port ?? (envConfig.port || `${DEFAULT_PORT}`), 10);
12765
- if (!Number.isInteger(port) || port < 1 || port > 65535) {
12854
+ const port = Number.parseInt(values.port ?? (envConfig.port || `${defaultAgentServerPort(envConfig.root)}`), 10);
12855
+ if (!Number.isInteger(port) || port < 0 || port > 65535) {
12766
12856
  console.error(`Invalid --port: ${String(values.port)}`);
12767
12857
  return 1;
12768
12858
  }
12769
12859
  const allowedOrigins = parseAllowedOrigins(values["allowed-origins"] ?? (envConfig.allowedOrigins || DEFAULT_ALLOWED_ORIGINS));
12770
- const connectionSettings = new ConnectionSettingsStore(values.root ?? resolveAgentServerRoot({ root: envConfig.root }), {
12860
+ const settingsRoot = envConfig.root;
12861
+ const connectionSettings = new ConnectionSettingsStore(settingsRoot, {
12771
12862
  ...envConfig.operatorOAuth,
12772
12863
  ...values["api-url"] || envConfig.apiUrl ? { apiUrl: values["api-url"] || envConfig.apiUrl } : {}
12773
12864
  });
@@ -12780,15 +12871,11 @@ async function runAgentServer(argv) {
12780
12871
  name: "agent-daemon.server",
12781
12872
  level: envConfig.logLevel || "info"
12782
12873
  });
12874
+ const shutdownDisposers = [];
12783
12875
  try {
12784
12876
  try {
12785
- return await withAgentServerLock(root, async () => {
12786
- const secrets = new FileSecretProvider({
12787
- root: store.secretsDir,
12788
- writable: true
12789
- });
12790
- const secretProviders = createNodeSecretProviderRegistry().register(secrets);
12791
- const externalSecretProviders = createNodeSecretProviderRegistry();
12877
+ return await withAgentServerLock(settingsRoot, async () => {
12878
+ const { secrets, secretProviders, externalSecretProviders } = createAgentServerSecretProviders(connectionSettings, store);
12792
12879
  const nativeGrant = new NativeGrantService();
12793
12880
  const nativeClient = applyNativeClientGrant({
12794
12881
  nativeGrant,
@@ -12812,6 +12899,7 @@ async function runAgentServer(argv) {
12812
12899
  const runtimeRegistry = new RuntimeRegistry(store.root);
12813
12900
  const runs = new RunManager({
12814
12901
  store,
12902
+ storeRoot: settingsRoot,
12815
12903
  secretProviders,
12816
12904
  externalSecretProviders,
12817
12905
  baseEnv: processEnvSnapshot(),
@@ -12852,28 +12940,39 @@ async function runAgentServer(argv) {
12852
12940
  logger,
12853
12941
  shutdownSignal: shutdownController.signal
12854
12942
  });
12943
+ let endpoint;
12855
12944
  try {
12856
12945
  const address = await app.listen(nativeSocket ? { path: nativeSocket } : {
12857
12946
  host: "127.0.0.1",
12858
12947
  port
12859
12948
  });
12860
12949
  if (nativeSocket) await chmod(nativeSocket, 384);
12950
+ if (!nativeSocket) {
12951
+ endpoint = publishAgentServerEndpoint(settingsRoot, address);
12952
+ console.error(`discovery: ${join(settingsRoot, "agent-server-endpoint.json")}`);
12953
+ }
12861
12954
  console.error(`moltnet-agent server listening on ${address}`);
12862
- console.error(`config root: ${root}`);
12955
+ console.error(`store root: ${settingsRoot} (${envConfig.rootSource})`);
12956
+ console.error(`connection state: ${root}`);
12863
12957
  if (nativeSocket) console.error(`native control socket: ${nativeSocket}`);
12864
12958
  else console.error(`allowed origins: ${allowedOrigins.join(", ")}`);
12865
12959
  if (nativeClient) console.error("native desktop client: authorized");
12866
12960
  if (!nativeSocket) console.error("Connect from an allowed local-control client after operator authorization.");
12867
- return await waitForAgentServerShutdown(runs, app, shutdownController, Boolean(values.supervised));
12961
+ return await waitForAgentServerShutdown(runs, app, shutdownController, Boolean(values.supervised), (dispose) => shutdownDisposers.push(dispose));
12868
12962
  } catch (cause) {
12869
12963
  await app.close().catch(() => void 0);
12870
12964
  throw cause;
12965
+ } finally {
12966
+ endpoint?.release();
12871
12967
  }
12872
- }, { onCompromised: (error) => {
12873
- console.error(error.message);
12874
- process.exitCode = 1;
12875
- process.kill(process.pid, "SIGTERM");
12876
- } });
12968
+ }, {
12969
+ stateRoot: root,
12970
+ onCompromised: (error) => {
12971
+ console.error(error.message);
12972
+ process.exitCode = 1;
12973
+ process.kill(process.pid, "SIGTERM");
12974
+ }
12975
+ });
12877
12976
  } catch (cause) {
12878
12977
  if (cause instanceof AgentServerLockError) {
12879
12978
  console.error(cause.message);
@@ -12882,10 +12981,14 @@ async function runAgentServer(argv) {
12882
12981
  throw cause;
12883
12982
  }
12884
12983
  } finally {
12885
- await shutdownLogger();
12984
+ try {
12985
+ await shutdownLogger();
12986
+ } finally {
12987
+ for (const dispose of shutdownDisposers) dispose();
12988
+ }
12886
12989
  }
12887
12990
  }
12888
- function waitForAgentServerShutdown(runs, app, shutdownController, supervised) {
12991
+ function waitForAgentServerShutdown(runs, app, shutdownController, supervised, registerDisposer) {
12889
12992
  return new Promise((resolvePromise) => {
12890
12993
  let shuttingDown = false;
12891
12994
  const shutdown = (source) => {
@@ -12916,8 +13019,6 @@ function waitForAgentServerShutdown(runs, app, shutdownController, supervised) {
12916
13019
  if (forcedExitTimer) clearTimeout(forcedExitTimer);
12917
13020
  const failures = results.filter((result) => result.status === "rejected");
12918
13021
  for (const failure of failures) console.error(`shutdown cleanup failed: ${failure.reason.message}`);
12919
- handlers.dispose();
12920
- stdinGuard.dispose();
12921
13022
  const exitCode = typeof process.exitCode === "number" ? process.exitCode : 0;
12922
13023
  resolvePromise(failures.length > 0 ? 1 : exitCode);
12923
13024
  })();
@@ -12930,6 +13031,10 @@ function waitForAgentServerShutdown(runs, app, shutdownController, supervised) {
12930
13031
  enabled: supervised,
12931
13032
  shutdown: () => shutdown("stdin")
12932
13033
  });
13034
+ registerDisposer(() => {
13035
+ handlers.dispose();
13036
+ stdinGuard.dispose();
13037
+ });
12933
13038
  });
12934
13039
  }
12935
13040
  /** Makes the supervising process's stdin pipe part of the server lifecycle. */
@@ -13278,6 +13383,7 @@ function compareVersions(a, b) {
13278
13383
  }
13279
13384
  function cachePath() {
13280
13385
  const env = loadUpdateEnvConfig();
13386
+ if (env.storeRoot) return join(env.storeRoot, "cache", "updates", "agent.json");
13281
13387
  return join(env.xdgCacheHome || (platform() === "win32" ? env.localAppData || join(homedir(), "AppData", "Local") : join(homedir(), ".cache")), "moltnet", "updates", "agent.json");
13282
13388
  }
13283
13389
  async function readCache() {
@@ -13297,16 +13403,25 @@ async function writeCache(cache) {
13297
13403
  }
13298
13404
  //#endregion
13299
13405
  //#region src/version.ts
13300
- var DAEMON_VERSION = "0.62.0";
13406
+ var DAEMON_VERSION = "0.63.1";
13301
13407
  //#endregion
13302
13408
  //#region src/cli.ts
13303
13409
  async function runAgentDaemonCli(options) {
13304
13410
  const [subcommand, ...rest] = options.argv ?? process.argv.slice(2);
13305
- if ([
13411
+ const help = subcommand === "--help" || subcommand === "-h" || [
13306
13412
  "poll",
13307
13413
  "drain",
13308
- "serve"
13309
- ].includes(subcommand ?? "") && !isWorkspaceInvocation()) checkDaemonUpdate({ currentVersion: DAEMON_VERSION }).then((update) => {
13414
+ "once",
13415
+ "server",
13416
+ "providers",
13417
+ "sync-sessions"
13418
+ ].includes(subcommand ?? "") && isHelpFlag(rest);
13419
+ const notice = legacyStoreNotice();
13420
+ if (notice && !help) {
13421
+ console.error(notice);
13422
+ markLegacyStoreNoticeShown();
13423
+ }
13424
+ if (["poll", "drain"].includes(subcommand ?? "") && !isWorkspaceInvocation()) checkDaemonUpdate({ currentVersion: DAEMON_VERSION }).then((update) => {
13310
13425
  if (update.updateAvailable) pino({ name: "moltnet.agent-daemon" }).info({
13311
13426
  event: "agent-daemon.update_available",
13312
13427
  update
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@themoltnet/agent-daemon",
3
- "version": "0.62.0",
3
+ "version": "0.63.1",
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.",
@@ -84,10 +84,10 @@
84
84
  "pino-pretty": "^13.1.3",
85
85
  "proper-lockfile": "4.1.2",
86
86
  "typebox": "^1.2.8",
87
- "@themoltnet/agent-runtime": "1.2.0",
88
- "@themoltnet/pi-runtime": "0.18.2",
89
- "@themoltnet/os-keyring": "0.3.0",
90
- "@themoltnet/sdk": "0.144.0"
87
+ "@themoltnet/os-keyring": "0.4.0",
88
+ "@themoltnet/agent-runtime": "1.3.0",
89
+ "@themoltnet/pi-runtime": "0.18.3",
90
+ "@themoltnet/sdk": "0.145.0"
91
91
  },
92
92
  "devDependencies": {
93
93
  "@fastify/swagger": "^9.6.1",
@@ -97,16 +97,16 @@
97
97
  "vite": "^8.0.0",
98
98
  "vite-plugin-dts": "^4.5.4",
99
99
  "vitest": "^3.0.0",
100
- "@moltnet/api-client": "0.1.0",
101
100
  "@moltnet/agent-eval": "0.1.0",
102
- "@moltnet/bootstrap": "0.1.0",
103
101
  "@moltnet/crypto-service": "0.1.0",
104
102
  "@moltnet/execution-integrations": "0.1.0",
105
- "@moltnet/models": "0.1.0",
106
- "@moltnet/observability": "0.1.0",
107
103
  "@moltnet/loopback-companion": "0.1.0",
104
+ "@moltnet/bootstrap": "0.1.0",
105
+ "@moltnet/api-client": "0.1.0",
106
+ "@moltnet/models": "0.1.0",
108
107
  "@moltnet/execution-plan": "0.1.0",
109
108
  "@moltnet/runtime-profiles": "0.1.0",
109
+ "@moltnet/observability": "0.1.0",
110
110
  "@moltnet/tasks": "0.1.0"
111
111
  },
112
112
  "nx": {
@@ -137,6 +137,8 @@
137
137
  "inputs": [
138
138
  "default",
139
139
  "^production",
140
+ "{workspaceRoot}/test-fixtures/agent-server-endpoint.json",
141
+ "{workspaceRoot}/test-fixtures/connection-state-root.json",
140
142
  "{workspaceRoot}/docs/operate/*.md",
141
143
  "{workspaceRoot}/docs/.vitepress/config.ts"
142
144
  ]
@@ -145,7 +147,7 @@
145
147
  },
146
148
  "scripts": {
147
149
  "cli": "tsx src/main.ts",
148
- "dev": "tsx watch src/main.ts",
150
+ "dev": "tsx ../../tools/src/desktop-development.ts tsx watch src/main.ts",
149
151
  "start": "node dist/main.js",
150
152
  "build": "vite build",
151
153
  "generate:openapi": "tsx scripts/generate-openapi.ts",