@themoltnet/agent-daemon 0.53.0 → 0.54.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +39 -20
  2. package/dist/cli.js +416 -127
  3. package/package.json +20 -12
package/README.md CHANGED
@@ -81,31 +81,50 @@ All config flows from environment variables. The daemon reads them in
81
81
 
82
82
  ### MoltNet identity
83
83
 
84
- | Var | Required | Purpose |
85
- | --------------------- | ------------------------------------- | -------------------------------------------------------------------------- |
86
- | `GIT_CONFIG_GLOBAL` | OAuth2/local | Optional git identity path; not needed for configless agent-key startup. |
87
- | `MOLTNET_AGENT_NAME` | yes | Agent name (matches `.moltnet/<name>/`). |
88
- | `MOLTNET_API_URL` | agent-key only | Explicit API endpoint; key mode never reads it from `moltnet.json`. |
89
- | `MOLTNET_AGENT_KEY` | no | Team- or identity-scoped agent key. Set to authenticate instead of OAuth2. |
90
- | `MOLTNET_PRIVATE_KEY` | agent-key `once`, `poll`, and `drain` | Base64 Ed25519 seed used by daemon-owned executor attestation. |
91
-
92
- For OAuth2/local mode, the agent's `moltnet.json` and gitconfig live next to
84
+ | Var | Required | Purpose |
85
+ | --------------------- | ---------------------------------- | ------------------------------------------------------------------------- |
86
+ | `GIT_CONFIG_GLOBAL` | config-based | Optional git identity path; not needed for configless startup. |
87
+ | `MOLTNET_AGENT_NAME` | yes | Agent name (matches `.moltnet/<name>/`). |
88
+ | `MOLTNET_API_URL` | configless only | Explicit API endpoint; configless runs never read it from `moltnet.json`. |
89
+ | `MOLTNET_AGENT_KEY` | no | Team- or identity-scoped agent key. Overrides `moltnet.json`. |
90
+ | `MOLTNET_PRIVATE_KEY` | configless `once`, `poll`, `drain` | Base64 Ed25519 seed used by daemon-owned executor attestation. |
91
+
92
+ For config-based runs, the agent's `moltnet.json` and gitconfig live next to
93
93
  each other in `.moltnet/<agent>/`. Provision them once via
94
94
  [`moltnet agents init`](../../docs/start/install-and-initialize.md#initialize-an-autonomous-agent).
95
95
 
96
- **Auth mode.** When `MOLTNET_AGENT_KEY` is set the daemon authenticates with
97
- that key as an opaque bearer token (no OAuth2 exchange); otherwise it uses the
98
- OAuth2 client-credentials from `moltnet.json`. The key is read from the
99
- environment only never store it in `moltnet.json`. The daemon reconciles a
100
- team-bound key against `--team` at startup; an identity-scoped key may select
101
- any team where the agent is authorized. It fails fast if the key is rejected,
102
- is not an agent, or a team binding mismatches. See
96
+ **The daemon runs on an agent key only.** OAuth2 client_credentials is not
97
+ accepted: it hands the daemon the full 17-scope agent grant against a six-scope
98
+ need, and a Hydra token cannot be a Talos derivation parent. A `moltnet.json`
99
+ without `agent_key_ref` is refused at startup with the command that fixes it.
100
+
101
+ The key reaches the daemon two ways, and `MOLTNET_AGENT_KEY` wins when both are
102
+ present:
103
+
104
+ - **Configless** — `MOLTNET_AGENT_KEY` (or `MOLTNET_AGENT_KEY_REF`) in the
105
+ environment. No agent files are read at all.
106
+ - **From `moltnet.json`** — an `agent_key_ref` pointing at a secret provider,
107
+ which is what `moltnet agents keys create --store` writes. The plaintext
108
+ secret never lands in the file.
109
+
110
+ Mint or rotate the key with the CLI, which is separate operator tooling and
111
+ keeps using OAuth2 for its own authentication:
112
+
113
+ ```bash
114
+ moltnet agents keys create --agent-id <uuid> --team-id <uuid> \
115
+ --name <agent>-daemon --store
116
+ moltnet agents keys rotate <key-id> --team-id <uuid> --store
117
+ ```
118
+
119
+ The daemon reconciles a team-bound key against `--team` at startup; an
120
+ identity-scoped key may select any team where the agent is authorized. It fails
121
+ fast if the key is rejected, is not an agent, or a team binding mismatches. See
103
122
  [Run the daemon with an agent key](../../docs/operate/agent-keys.md#run-the-daemon-with-an-agent-key).
104
123
 
105
- Daemon authentication and the guest boundary are two separate concerns. How the
106
- daemon authenticates (an agent key, or OAuth2 resolved from
107
- `.moltnet/<agent>/moltnet.json` through the host secret provider) decides how the
108
- host-side SDK `Agent` is built. The guest boundary is fixed: **the guest never
124
+ Daemon authentication and the guest boundary are two separate concerns. Where
125
+ the agent key comes from (the environment, or an `agent_key_ref` in
126
+ `.moltnet/<agent>/moltnet.json` resolved through the host secret provider)
127
+ decides how the host-side SDK `Agent` is built. The guest boundary is fixed: **the guest never
109
128
  receives MoltNet credential material.** No `.moltnet` file, gitconfig, SSH
110
129
  signing key, GitHub App PEM, or MoltNet environment credential is injected into
111
130
  Gondolin, and mounted `.moltnet` paths are hidden. Structured MoltNet tools
package/dist/cli.js CHANGED
@@ -13,9 +13,9 @@ import { parseArgs, promisify } from "node:util";
13
13
  import { AgentRuntime, ApiTaskReporter, ApiTaskSource, PollingApiTaskSource, createLocalSeedSigner, resolveAgentIdentity, resolveProfileWarmSessionTtlSec, resolveRuntimeProfile, resolveRuntimeProfiles, validateRuntimeProfilePrerequisites } from "@themoltnet/agent-runtime";
14
14
  import { GuestEnvironmentBoundaryError, assertGuestEnvironmentBoundary, createPiRetryTriage, findMainWorktree, isResolvedPathInsideRoot, normalizeRetryTriageResult, redactRetryTriageSecrets, resolveRuntimeProfileModel } from "@themoltnet/pi-runtime";
15
15
  import { FILE_SECRET_PROVIDER, FileSecretProvider, connect, createNodeSecretProviderRegistry } from "@themoltnet/sdk/node";
16
- import { execFile, execFileSync, spawn } from "node:child_process";
17
- import { constants, createReadStream, createWriteStream, existsSync, lstatSync, mkdirSync, openSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
18
- import { AuthenticationError, MoltNetError, agentKeyKey, assertTrustedConfigApiUrl, createExecutorAttestor, deriveMcpUrl, formatSecretReferenceString, identitySeedKey, parseSecretReferenceString, readConfig, register, requireSecureCredentialApiUrl, resolveAgentKey, resolveEnvSecretReference, resolveIdentitySeed, resolveOAuth2ClientSecret } from "@themoltnet/sdk";
16
+ import { constants, cpSync, createReadStream, createWriteStream, existsSync, lstatSync, mkdirSync, openSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
17
+ import { AuthenticationError, IDENTITY_ALIAS_PATTERN, MoltNetError, agentKeyKey, assertIdentityAlias, assertTrustedConfigApiUrl, createExecutorAttestor, deriveMcpUrl, formatSecretReferenceString, getConfigDir, getIdentityDir, identitySeedKey, parseSecretReferenceString, readConfig, register, requireSecureCredentialApiUrl, resolveAgentKey, resolveEnvSecretReference, resolveIdentitySeed, resolveOAuth2ClientSecret } from "@themoltnet/sdk";
18
+ import { execFile, spawn } from "node:child_process";
19
19
  import { X509Certificate, createHash, createPrivateKey, createPublicKey, randomBytes, randomUUID, timingSafeEqual, webcrypto } from "node:crypto";
20
20
  import { once } from "node:events";
21
21
  import { metrics } from "@opentelemetry/api";
@@ -36,13 +36,13 @@ import { lock, lockSync } from "proper-lockfile";
36
36
  import { ModelRuntime, readStoredCredential } from "@earendil-works/pi-coding-agent";
37
37
  import { Transform, Writable } from "node:stream";
38
38
  import { writePiConfig } from "@themoltnet/pi-runtime/pi-config";
39
- import { homedir, platform } from "node:os";
40
39
  import { pathToFileURL } from "node:url";
41
40
  import { StringDecoder } from "node:string_decoder";
42
41
  import rateLimit from "@fastify/rate-limit";
43
42
  import Fastify from "fastify";
44
43
  import { isIP } from "node:net";
45
44
  import "reflect-metadata";
45
+ import { homedir, platform } from "node:os";
46
46
  import { BasicConstraintsExtension, ExtendedKeyUsage, ExtendedKeyUsageExtension, IP, KeyUsageFlags, KeyUsagesExtension, SubjectAlternativeNameExtension, X509CertificateGenerator } from "@peculiar/x509";
47
47
  import { createGzip } from "node:zlib";
48
48
  //#region ../../libs/tasks/src/rubric.ts
@@ -795,7 +795,14 @@ var AGENT_CREDENTIAL_SCOPES = [
795
795
  CREDENTIAL_SCOPES.TaskExecute
796
796
  ];
797
797
  Object.freeze(ALL_CREDENTIAL_SCOPES.filter((scope) => scope !== CREDENTIAL_SCOPES.HumanProfile));
798
- [
798
+ /**
799
+ * REST capabilities exercised by the current MCP tool surface.
800
+ *
801
+ * Intentionally excludes connector invocation, key management, runtime
802
+ * management/read, and task claiming because MCP exposes none of those
803
+ * operations.
804
+ */
805
+ var MCP_CLIENT_SCOPES = [
799
806
  CREDENTIAL_SCOPES.AgentProfile,
800
807
  CREDENTIAL_SCOPES.CryptoSign,
801
808
  CREDENTIAL_SCOPES.DiaryManage,
@@ -809,7 +816,13 @@ Object.freeze(ALL_CREDENTIAL_SCOPES.filter((scope) => scope !== CREDENTIAL_SCOPE
809
816
  CREDENTIAL_SCOPES.TaskRead,
810
817
  CREDENTIAL_SCOPES.TeamManage,
811
818
  CREDENTIAL_SCOPES.TeamRead
812
- ].filter((scope) => scope !== CREDENTIAL_SCOPES.HumanProfile);
819
+ ];
820
+ MCP_CLIENT_SCOPES.filter((scope) => scope !== CREDENTIAL_SCOPES.HumanProfile);
821
+ Object.freeze([...[
822
+ "openid",
823
+ "offline",
824
+ "offline_access"
825
+ ], ...MCP_CLIENT_SCOPES]);
813
826
  //#endregion
814
827
  //#region ../../libs/models/src/preview-sign.ts
815
828
  function schemaRef$2(schema, id) {
@@ -3451,9 +3464,19 @@ Commands:
3451
3464
  Run \`agent-daemon <command> --help\` for command-specific flags.
3452
3465
 
3453
3466
  Prerequisites:
3454
- - agent-key mode: MOLTNET_AGENT_KEY (or MOLTNET_AGENT_KEY_REF) and
3467
+ - configless: MOLTNET_AGENT_KEY (or MOLTNET_AGENT_KEY_REF) and
3455
3468
  MOLTNET_PRIVATE_KEY (or MOLTNET_PRIVATE_KEY_REF); no agent files
3456
- - OAuth2 and sync-sessions: <agent-root>/.moltnet/<agent>/moltnet.json
3469
+ - config-based and sync-sessions: <agent-root>/.moltnet/<agent>/moltnet.json
3470
+ carrying agent_key_ref (OAuth2 client credentials are not accepted)
3471
+
3472
+ No key yet? Mint one with the CLI (--store writes agent_key_ref into
3473
+ moltnet.json and keeps the secret in a provider):
3474
+
3475
+ moltnet teams list # find the team id
3476
+ moltnet agents keys create --team-id <team-uuid> \\
3477
+ --name <agent>-daemon --store
3478
+
3479
+ https://docs.themolt.net/operate/agent-keys#run-the-daemon-with-an-agent-key
3457
3480
  - --profile — remote runtime profile supplies provider/model/sandbox
3458
3481
  policy and CWD is used as the VM mountPath.
3459
3482
 
@@ -3617,18 +3640,30 @@ function assessIdentityPin(current, expected) {
3617
3640
  //#endregion
3618
3641
  //#region src/lib/agent-context.ts
3619
3642
  /**
3620
- * Report which auth mode `connect()` will use, without ever reading the secret
3621
- * value into anything logged. Agent-key mode is selected when
3622
- * `MOLTNET_AGENT_KEY` or `MOLTNET_AGENT_KEY_REF` holds a non-blank value
3623
- * mirroring the SDK precedence
3624
- * where an environment key opts into key mode ahead of the config-file OAuth2
3625
- * credentials. The daemon never passes explicit in-code credentials to
3626
- * `connect()`, so this env-only check matches what `connect()` actually does.
3643
+ * Where an operator goes after the daemon refuses to start. The published site
3644
+ * rather than a GitHub blob: it renders, and it tracks the deployed docs
3645
+ * instead of whatever `main` happens to say. Sidebar entry lives in
3646
+ * `docs/.vitepress/config.ts`.
3647
+ */
3648
+ var AGENT_KEYS_DOC_URL = "https://docs.themolt.net/operate/agent-keys#run-the-daemon-with-an-agent-key";
3649
+ /** Scopes a daemon key must carry; mirrors `DAEMON_REQUIRED_SCOPES`. */
3650
+ var DAEMON_KEY_SCOPES = AGENT_CREDENTIAL_SCOPES;
3651
+ /**
3652
+ * Report where `connect()` will find the key, without ever reading the secret
3653
+ * value into anything logged. A non-blank `MOLTNET_AGENT_KEY` or
3654
+ * `MOLTNET_AGENT_KEY_REF` means configless; otherwise the key comes from
3655
+ * `moltnet.json`. This mirrors the SDK precedence, where an environment key
3656
+ * wins over the config file.
3657
+ *
3658
+ * It reports the *source*, not what `connect()` would pick on its own: the
3659
+ * config path resolves the key and passes it explicitly, because ambient
3660
+ * resolution ranks environment OAuth2 credentials above a configured
3661
+ * `agent_key_ref` (see `resolveAgentContext`).
3627
3662
  *
3628
3663
  * Pure: `env` is passed in (the config module owns the `process.env` read).
3629
3664
  */
3630
- function detectAuthMode(env) {
3631
- return env.MOLTNET_AGENT_KEY?.trim() || env.MOLTNET_AGENT_KEY_REF?.trim() ? "agent-key" : "oauth2";
3665
+ function detectCredentialSource(env) {
3666
+ return env.MOLTNET_AGENT_KEY?.trim() || env.MOLTNET_AGENT_KEY_REF?.trim() ? "environment" : "config";
3632
3667
  }
3633
3668
  /**
3634
3669
  * Pure check: may the identity described by `whoami` operate the daemon as
@@ -3684,40 +3719,111 @@ async function validateStartupBinding(options) {
3684
3719
  /**
3685
3720
  * Resolve the agent's MoltNet credentials directory and connect via SDK.
3686
3721
  *
3687
- * Looks under an explicit agent root first, then falls back to the git root
3688
- * when available. Fails fast if the dir is missing credentials are required,
3689
- * the daemon never falls back to unauthenticated calls.
3722
+ * The daemon selects the same central identity directory as the CLI. It never
3723
+ * inspects a repository, Git state, or legacy agent bundle for credentials.
3690
3724
  */
3691
3725
  async function resolveAgentContext(agentName, options = {}) {
3692
- if (!/^[a-zA-Z0-9_-]+$/.test(agentName)) throw new Error(`Invalid agent name "${agentName}": must match /^[a-zA-Z0-9_-]+$/`);
3693
- const roots = resolveCredentialRoots(options.agentRootDir);
3694
- if (options.authMode === "agent-key") {
3695
- const rootDir = roots[0] ?? process.cwd();
3696
- return {
3697
- agentDir: join(rootDir, ".moltnet", agentName),
3698
- agentRootDir: rootDir,
3699
- agent: await connect({ secretProviders: createNodeSecretProviderRegistry() }),
3700
- credentialSource: "environment",
3701
- authMechanism: "agent-key"
3702
- };
3703
- }
3704
- const located = locateAgentConfig(roots, agentName);
3705
- if (located) {
3706
- const agent = await connect({
3707
- configDir: located.agentDir,
3708
- secretProviders: createNodeSecretProviderRegistry()
3709
- });
3710
- const config = await readConfig(located.agentDir);
3711
- return {
3712
- agentDir: located.agentDir,
3713
- agentRootDir: located.rootDir,
3714
- agent,
3715
- credentialSource: "config",
3716
- authMechanism: config?.agent_key_ref ? "agent-key" : "oauth2"
3726
+ assertIdentityAlias(agentName);
3727
+ const { agentDir, agentRootDir } = resolveIdentityLocation(agentName, options.agentRootDir, { requireConfig: options.credentialSource !== "environment" });
3728
+ if (options.credentialSource === "environment") return {
3729
+ agentDir,
3730
+ agentRootDir,
3731
+ agent: await connect({ secretProviders: createNodeSecretProviderRegistry() }),
3732
+ credentialSource: "environment"
3733
+ };
3734
+ const config = await readConfig(agentDir);
3735
+ if (!config?.agent_key_ref) throw new Error(agentKeyRequiredMessage(agentDir, agentName));
3736
+ const secretProviders = createNodeSecretProviderRegistry();
3737
+ const agentKey = await resolveAgentKey(config, secretProviders);
3738
+ if (!agentKey) throw new Error(agentKeyRequiredMessage(agentDir, agentName));
3739
+ return {
3740
+ agentDir,
3741
+ agentRootDir,
3742
+ agent: await connect({
3743
+ configDir: agentDir,
3744
+ secretProviders,
3745
+ agentKey,
3746
+ apiUrl: resolveConfigApiUrl(config, options.envApiUrl)
3747
+ }),
3748
+ credentialSource: "config"
3749
+ };
3750
+ }
3751
+ /**
3752
+ * The daemon runs on an agent key only. A `moltnet.json` carrying OAuth2
3753
+ * client credentials but no `agent_key_ref` is the pre-#2160 shape, and
3754
+ * `connect()` would happily authenticate it — so this has to be refused here
3755
+ * rather than left to surface as an over-scoped token later.
3756
+ */
3757
+ function agentKeyRequiredMessage(agentDir, agentName) {
3758
+ return [
3759
+ `${join(agentDir, "moltnet.json")} has no "agent_key_ref". The daemon`,
3760
+ `requires a team-bound agent key; OAuth2 client_credentials is no longer`,
3761
+ `accepted.`,
3762
+ ``,
3763
+ `Mint one (--agent-id defaults to the agent you authenticate as):`,
3764
+ ``,
3765
+ ` moltnet teams list # find the team id`,
3766
+ ` moltnet agents keys create \\`,
3767
+ ` --team-id <team-uuid> \\`,
3768
+ ` --name ${agentName}-daemon \\`,
3769
+ ` --scopes ${DAEMON_KEY_SCOPES.join(",")} \\`,
3770
+ ` --store`,
3771
+ ``,
3772
+ `--store writes "agent_key_ref" into moltnet.json and keeps the secret in`,
3773
+ `a provider, so the key itself never lands in the file. Omit --scopes to`,
3774
+ `get the same daemon minimum by default.`,
3775
+ ``,
3776
+ `To run configless instead, set MOLTNET_AGENT_KEY (or`,
3777
+ `MOLTNET_AGENT_KEY_REF) and skip the config entirely.`,
3778
+ ``,
3779
+ `Full guide: ${AGENT_KEYS_DOC_URL}`
3780
+ ].join("\n");
3781
+ }
3782
+ /**
3783
+ * The central identity directory, unless an explicit `--agent-root` names a
3784
+ * legacy bundle that actually exists.
3785
+ *
3786
+ * The flag is documented as "Directory that owns .moltnet/<agent>" and is still
3787
+ * accepted by `once`, `poll` and `sync-sessions`. Ignoring it silently sent
3788
+ * every caller that passes one — sandboxed runs, the e2e harness — to a
3789
+ * central store they never populated, and failed with a bare "No credentials
3790
+ * found".
3791
+ */
3792
+ /**
3793
+ * `agentDir` is where credentials live; `agentRootDir` is the directory that
3794
+ * OWNS it and is mounted into the sandbox. They differ for a legacy bundle
3795
+ * (`<root>/.moltnet/<agent>` inside `<root>`) and coincide for a central
3796
+ * identity, which owns nothing above itself. Collapsing them mounted the
3797
+ * credentials directory itself into the guest.
3798
+ */
3799
+ function resolveIdentityLocation(agentName, explicitRootDir, { requireConfig }) {
3800
+ const root = explicitRootDir?.trim();
3801
+ if (root) {
3802
+ const bundle = join(root, ".moltnet", agentName);
3803
+ if (!requireConfig || existsSync(join(bundle, "moltnet.json"))) return {
3804
+ agentDir: bundle,
3805
+ agentRootDir: root
3717
3806
  };
3718
3807
  }
3719
- const tried = roots.map((root) => join(root, ".moltnet", agentName));
3720
- throw new Error(`Missing credentials for ${agentName}. Checked ${tried.join(", ")}. Run the agent onboarding flow first.`);
3808
+ const central = getIdentityDir(agentName);
3809
+ return {
3810
+ agentDir: central,
3811
+ agentRootDir: central
3812
+ };
3813
+ }
3814
+ /**
3815
+ * Pick the API URL for an explicitly-keyed connect, preserving the checks
3816
+ * ambient config resolution would have applied. An explicit `apiUrl` skips
3817
+ * ambient's own normalisation, so a URL taken from `moltnet.json` still has to
3818
+ * clear the config-trust and transport checks before it is used.
3819
+ */
3820
+ function resolveConfigApiUrl(config, envApiUrl) {
3821
+ if (envApiUrl?.trim()) return void 0;
3822
+ const fromConfig = config.endpoints?.api?.trim();
3823
+ if (!fromConfig) return void 0;
3824
+ assertTrustedConfigApiUrl(fromConfig);
3825
+ requireSecureCredentialApiUrl(fromConfig);
3826
+ return fromConfig;
3721
3827
  }
3722
3828
  function isTransientWhoamiError(error) {
3723
3829
  if (error instanceof TypeError) return true;
@@ -3725,26 +3831,6 @@ function isTransientWhoamiError(error) {
3725
3831
  const statusCode = error.statusCode;
3726
3832
  return typeof statusCode === "number" && (statusCode === 408 || statusCode === 429 || statusCode >= 500);
3727
3833
  }
3728
- function locateAgentConfig(roots, agentName) {
3729
- for (const rootDir of roots) {
3730
- const agentDir = join(rootDir, ".moltnet", agentName);
3731
- if (existsSync(join(agentDir, "moltnet.json"))) return {
3732
- rootDir,
3733
- agentDir
3734
- };
3735
- }
3736
- }
3737
- function resolveCredentialRoots(agentRootDir) {
3738
- const roots = agentRootDir ? [agentRootDir] : [];
3739
- try {
3740
- const gitRoot = execFileSync("git", ["rev-parse", "--show-toplevel"], {
3741
- encoding: "utf8",
3742
- stdio: "pipe"
3743
- }).trim();
3744
- if (!roots.includes(gitRoot)) roots.push(gitRoot);
3745
- } catch {}
3746
- return roots;
3747
- }
3748
3834
  //#endregion
3749
3835
  //#region src/config.ts
3750
3836
  /**
@@ -3764,7 +3850,8 @@ function loadConfig() {
3764
3850
  profilePrerequisiteEnv: process.env,
3765
3851
  profilePrerequisitePath: process.env.PATH ?? "",
3766
3852
  piCodingAgentDir: process.env["PI_CODING_AGENT_DIR"] ?? "",
3767
- authMode: detectAuthMode(process.env),
3853
+ credentialSource: detectCredentialSource(process.env),
3854
+ apiUrl: process.env["MOLTNET_API_URL"] ?? "",
3768
3855
  signingPrivateKey: process.env["MOLTNET_PRIVATE_KEY"] ?? "",
3769
3856
  signingPrivateKeyRef: process.env["MOLTNET_PRIVATE_KEY_REF"] ?? "",
3770
3857
  gitAuthor: process.env["MOLTNET_GIT_AUTHOR"] ?? "",
@@ -3839,13 +3926,13 @@ async function abortActiveAttemptOnSignal(opts) {
3839
3926
  //#region src/lib/agent-identity.ts
3840
3927
  /**
3841
3928
  * Build the non-secret identity projected into guests. Host git config is a
3842
- * non-secret input and is consulted only on OAuth2 hosts, which already read
3843
- * that configuration for the signing seed; configless agent-key hosts never
3844
- * touch a config directory.
3929
+ * non-secret input and is consulted only on config-based hosts, which already
3930
+ * read that configuration for the signing seed; configless hosts never touch a
3931
+ * config directory.
3845
3932
  */
3846
3933
  async function resolveDaemonAgentIdentity(input) {
3847
3934
  let hostGit;
3848
- if (input.gitAuthor === void 0 && input.authMode === "oauth2") {
3935
+ if (input.gitAuthor === void 0 && input.credentialSource === "config") {
3849
3936
  const config = await readConfig(input.agentDir);
3850
3937
  hostGit = config?.git ? {
3851
3938
  name: config.git.name,
@@ -4390,26 +4477,26 @@ function recoverScratchWorkspacePath(producer, stateDirs) {
4390
4477
  //#region src/lib/executor-attestation.ts
4391
4478
  var DAEMON_REQUIRED_SCOPES = AGENT_CREDENTIAL_SCOPES;
4392
4479
  async function resolveExecutorSigningPrivateKey(input) {
4393
- if (input.authMode === "agent-key") {
4480
+ if (input.credentialSource === "environment") {
4394
4481
  const privateKey = input.configuredPrivateKey.trim();
4395
4482
  if (privateKey) return privateKey;
4396
4483
  const reference = input.configuredPrivateKeyRef?.trim();
4397
- if (!reference) throw new Error("Agent-key daemon startup requires MOLTNET_PRIVATE_KEY (or MOLTNET_PRIVATE_KEY_REF) containing the base64-encoded Ed25519 private key seed.");
4484
+ if (!reference) throw new Error("Configless daemon startup requires MOLTNET_PRIVATE_KEY (or MOLTNET_PRIVATE_KEY_REF) containing the base64-encoded Ed25519 private key seed.");
4398
4485
  let resolved;
4399
4486
  try {
4400
4487
  resolved = await resolveEnvSecretReference(reference, createNodeSecretProviderRegistry());
4401
4488
  } catch (cause) {
4402
- throw new Error(`Agent-key daemon startup could not resolve MOLTNET_PRIVATE_KEY_REF: ${cause.message}`, { cause });
4489
+ throw new Error(`Configless daemon startup could not resolve MOLTNET_PRIVATE_KEY_REF: ${cause.message}`, { cause });
4403
4490
  }
4404
4491
  if (Buffer.from(resolved, "base64").length !== 32) throw new Error("MOLTNET_PRIVATE_KEY_REF must resolve to a base64-encoded 32-byte Ed25519 seed.");
4405
4492
  return resolved;
4406
4493
  }
4407
4494
  const config = await readConfig(input.agentDir);
4408
- if (!config) throw new Error(`OAuth2 daemon startup requires ${input.agentDir}/moltnet.json.`);
4495
+ if (!config) throw new Error(`Config-based daemon startup requires ${input.agentDir}/moltnet.json.`);
4409
4496
  try {
4410
4497
  return await resolveIdentitySeed(config, createNodeSecretProviderRegistry());
4411
4498
  } catch (cause) {
4412
- throw new Error(`OAuth2 daemon startup could not resolve the signing seed from ${input.agentDir}/moltnet.json (keys.private_key or keys.private_key_ref): ${cause.message}`, { cause });
4499
+ throw new Error(`Config-based daemon startup could not resolve the signing seed from ${input.agentDir}/moltnet.json (keys.private_key or keys.private_key_ref): ${cause.message}`, { cause });
4413
4500
  }
4414
4501
  }
4415
4502
  function validateDaemonScopes(whoami) {
@@ -5105,7 +5192,7 @@ async function logDaemonStartupFailure(input) {
5105
5192
  event: "agent-daemon.startup_failed",
5106
5193
  gate: input.gate,
5107
5194
  agent: input.agent,
5108
- authMode: input.authMode
5195
+ credentialSource: input.credentialSource
5109
5196
  }, "Agent daemon startup validation failed");
5110
5197
  await shutdown();
5111
5198
  }
@@ -5924,13 +6011,14 @@ async function runPolling(opts) {
5924
6011
  bindings: cfg.credentialBindings
5925
6012
  };
5926
6013
  const runtimeCredentialConfig = resolveCredentialEnforcement(cfg.credentialEnforcement, credentialSources) === "off" ? null : loadRuntimeCredentialConfig(credentialSources);
5927
- const agentRootDir = resolve(process.cwd(), values["agent-root"] ?? process.cwd());
6014
+ const explicitAgentRootDir = values["agent-root"] ? resolve(process.cwd(), values["agent-root"]) : void 0;
5928
6015
  const { ctx, signingPrivateKey, startupWhoami, agentIdentity, hostCapabilitySigner } = await (async () => {
5929
6016
  let gate = "resolve_agent_context";
5930
6017
  try {
5931
6018
  const resolvedContext = await resolveAgentContext(baseCommon.agent, {
5932
- agentRootDir,
5933
- authMode: cfg.authMode
6019
+ agentRootDir: explicitAgentRootDir,
6020
+ credentialSource: cfg.credentialSource,
6021
+ envApiUrl: cfg.apiUrl
5934
6022
  });
5935
6023
  gate = "authenticate_and_bind";
5936
6024
  const whoami = await validateStartupBinding({
@@ -5940,7 +6028,7 @@ async function runPolling(opts) {
5940
6028
  });
5941
6029
  gate = "resolve_signing_material";
5942
6030
  const privateKey = await resolveExecutorSigningPrivateKey({
5943
- authMode: cfg.authMode,
6031
+ credentialSource: cfg.credentialSource,
5944
6032
  agentDir: resolvedContext.agentDir,
5945
6033
  configuredPrivateKey: cfg.signingPrivateKey,
5946
6034
  configuredPrivateKeyRef: cfg.signingPrivateKeyRef
@@ -5956,7 +6044,7 @@ async function runPolling(opts) {
5956
6044
  const agentIdentity = await resolveDaemonAgentIdentity({
5957
6045
  agentName: baseCommon.agent,
5958
6046
  whoami,
5959
- authMode: cfg.authMode,
6047
+ credentialSource: cfg.credentialSource,
5960
6048
  agentDir: resolvedContext.agentDir,
5961
6049
  gitAuthor: values["git-author"] ?? (cfg.gitAuthor || void 0)
5962
6050
  });
@@ -5977,7 +6065,7 @@ async function runPolling(opts) {
5977
6065
  level: cfg.logLevel || (baseCommon.debug ? "debug" : "info"),
5978
6066
  gate,
5979
6067
  agent: baseCommon.agent,
5980
- authMode: cfg.authMode,
6068
+ credentialSource: cfg.credentialSource,
5981
6069
  error
5982
6070
  });
5983
6071
  throw error;
@@ -6081,7 +6169,7 @@ async function runPolling(opts) {
6081
6169
  resourceAttributes: {
6082
6170
  "moltnet.team.id": teamId,
6083
6171
  "moltnet.agent.name": baseCommon.agent,
6084
- "moltnet.auth.mode": ctx.authMechanism,
6172
+ "moltnet.credential.source": ctx.credentialSource,
6085
6173
  "moltnet.runtime_profile.count": String(profiles.length),
6086
6174
  "moltnet.runtime_profile.ids": profiles.map((p) => p.id).join(",")
6087
6175
  }
@@ -6129,7 +6217,7 @@ async function runPolling(opts) {
6129
6217
  }
6130
6218
  });
6131
6219
  rootLogger.info({
6132
- authMode: cfg.authMode,
6220
+ credentialSource: cfg.credentialSource,
6133
6221
  subjectType: startupWhoami.subjectType,
6134
6222
  bindingScope: startupWhoami.credentialBinding?.bindingScope ?? null,
6135
6223
  credentialKeyId: startupWhoami.credentialBinding?.keyId ?? null,
@@ -6581,13 +6669,14 @@ async function runOnce(argv, runtimeAdapter = defaultPiDaemonAdapter) {
6581
6669
  };
6582
6670
  const runtimeCredentialConfig = resolveCredentialEnforcement(cfg.credentialEnforcement, credentialSources) === "off" ? null : loadRuntimeCredentialConfig(credentialSources);
6583
6671
  const initialOpts = opts;
6584
- const agentRootDir = resolve(process.cwd(), values["agent-root"] ?? process.cwd());
6672
+ const explicitAgentRootDir = values["agent-root"] ? resolve(process.cwd(), values["agent-root"]) : void 0;
6585
6673
  const { ctx, signingPrivateKey, agentIdentity, hostCapabilitySigner } = await (async () => {
6586
6674
  let gate = "resolve_agent_context";
6587
6675
  try {
6588
6676
  const resolvedContext = await resolveAgentContext(initialOpts.agent, {
6589
- agentRootDir,
6590
- authMode: cfg.authMode
6677
+ agentRootDir: explicitAgentRootDir,
6678
+ credentialSource: cfg.credentialSource,
6679
+ envApiUrl: cfg.apiUrl
6591
6680
  });
6592
6681
  gate = "authenticate_and_bind";
6593
6682
  const whoami = await validateStartupBinding({
@@ -6597,7 +6686,7 @@ async function runOnce(argv, runtimeAdapter = defaultPiDaemonAdapter) {
6597
6686
  });
6598
6687
  gate = "resolve_signing_material";
6599
6688
  const privateKey = await resolveExecutorSigningPrivateKey({
6600
- authMode: cfg.authMode,
6689
+ credentialSource: cfg.credentialSource,
6601
6690
  agentDir: resolvedContext.agentDir,
6602
6691
  configuredPrivateKey: cfg.signingPrivateKey,
6603
6692
  configuredPrivateKeyRef: cfg.signingPrivateKeyRef
@@ -6613,7 +6702,7 @@ async function runOnce(argv, runtimeAdapter = defaultPiDaemonAdapter) {
6613
6702
  const agentIdentity = await resolveDaemonAgentIdentity({
6614
6703
  agentName: initialOpts.agent,
6615
6704
  whoami,
6616
- authMode: cfg.authMode,
6705
+ credentialSource: cfg.credentialSource,
6617
6706
  agentDir: resolvedContext.agentDir,
6618
6707
  gitAuthor: values["git-author"] ?? (cfg.gitAuthor || void 0)
6619
6708
  });
@@ -6633,7 +6722,7 @@ async function runOnce(argv, runtimeAdapter = defaultPiDaemonAdapter) {
6633
6722
  level: cfg.logLevel || (initialOpts.debug ? "debug" : "info"),
6634
6723
  gate,
6635
6724
  agent: initialOpts.agent,
6636
- authMode: cfg.authMode,
6725
+ credentialSource: cfg.credentialSource,
6637
6726
  error
6638
6727
  });
6639
6728
  throw error;
@@ -6703,7 +6792,7 @@ async function runOnce(argv, runtimeAdapter = defaultPiDaemonAdapter) {
6703
6792
  resourceAttributes: {
6704
6793
  "moltnet.task.id": taskId,
6705
6794
  "moltnet.agent.name": opts.agent,
6706
- "moltnet.auth.mode": ctx.authMechanism,
6795
+ "moltnet.credential.source": ctx.credentialSource,
6707
6796
  "moltnet.llm.provider": profile.provider,
6708
6797
  "moltnet.llm.model": profile.model,
6709
6798
  ...profile.thinkingLevel ? { "moltnet.llm.thinking_level": profile.thinkingLevel } : {},
@@ -7752,11 +7841,14 @@ function snapshot(login) {
7752
7841
  ...waitingForAuthorization ? { waitingForAuthorization } : {}
7753
7842
  };
7754
7843
  }
7755
- var NAME_RE = /^[a-z0-9][a-z0-9_-]{0,63}$/i;
7844
+ var NAME_RE = IDENTITY_ALIAS_PATTERN;
7756
7845
  var PROVIDER_ID_RE = /^[a-z0-9][a-z0-9-]{0,63}$/;
7757
7846
  function assertStoreName(kind, value) {
7758
- if (!NAME_RE.test(value)) throw new AgentServerStoreError("invalid_name", `${kind} must match ${NAME_RE.source}`);
7759
- return value;
7847
+ try {
7848
+ return assertIdentityAlias(value);
7849
+ } catch {
7850
+ throw new AgentServerStoreError("invalid_name", `${kind} must match ${NAME_RE.source}`);
7851
+ }
7760
7852
  }
7761
7853
  function assertProviderId(value) {
7762
7854
  if (!PROVIDER_ID_RE.test(value)) throw new AgentServerStoreError("invalid_name", `provider id must match ${PROVIDER_ID_RE.source}`);
@@ -7769,12 +7861,46 @@ var AgentServerStoreError = class extends Error {
7769
7861
  this.code = code;
7770
7862
  }
7771
7863
  };
7772
- /** `MOLTNET_AGENT_SERVER_ROOT` override, else `$XDG_CONFIG_HOME/moltnet`, else `~/.config/moltnet`. */
7864
+ /**
7865
+ * `MOLTNET_AGENT_SERVER_ROOT` override, else `~/.config/moltnet`.
7866
+ *
7867
+ * Deliberately does NOT consult `XDG_CONFIG_HOME`. The Go CLI's GetConfigDir
7868
+ * and @moltnet/agent-config's getConfigDir both resolve `~/.config/moltnet`,
7869
+ * so honouring XDG here gave one application two config roots: on a machine
7870
+ * with the variable set, the daemon wrote identities the CLI and SDK could not
7871
+ * read. `MOLTNET_AGENT_SERVER_ROOT` remains the explicit escape hatch for a
7872
+ * genuinely custom location. The XDG parameter is gone rather than ignored: an
7873
+ * accepted-but-discarded argument reads like it still works. Callers adopting
7874
+ * state from the old root pass it to ensure() instead.
7875
+ */
7773
7876
  function resolveAgentServerRoot(input) {
7774
7877
  const override = input.root?.trim();
7775
7878
  if (override) return override;
7776
- const xdg = input.xdgConfigHome?.trim();
7777
- return join(xdg || join(homedir(), ".config"), "moltnet");
7879
+ return getConfigDir();
7880
+ }
7881
+ /** The pre-#1834 root that honoured XDG_CONFIG_HOME, if it differs. */
7882
+ function legacyXdgAgentServerRoot(xdgConfigHome) {
7883
+ const xdg = xdgConfigHome.trim();
7884
+ if (!xdg) return null;
7885
+ const legacy = join(xdg, "moltnet");
7886
+ return legacy === resolveAgentServerRoot({}) ? null : legacy;
7887
+ }
7888
+ /** True when a root holds agent-server state worth preserving. */
7889
+ function hasAgentServerState(root) {
7890
+ for (const child of [
7891
+ "agent-server.json",
7892
+ "identity-selector.json",
7893
+ "identities",
7894
+ "agents"
7895
+ ]) try {
7896
+ if (readdirSync(join(root, child)).length > 0) return true;
7897
+ } catch {
7898
+ try {
7899
+ readFileSync(join(root, child));
7900
+ return true;
7901
+ } catch {}
7902
+ }
7903
+ return false;
7778
7904
  }
7779
7905
  function providerEnvName(providerId) {
7780
7906
  return `MOLTNET_PROVIDER_${assertProviderId(providerId).replaceAll("-", "_").toUpperCase()}_API_KEY`;
@@ -7812,14 +7938,14 @@ function writeJsonAtomic(path, value) {
7812
7938
  }
7813
7939
  var AgentServerStore = class {
7814
7940
  root;
7815
- agentsDir;
7941
+ identitiesDir;
7816
7942
  runsDir;
7817
7943
  secretsDir;
7818
7944
  /** Shared Pi credential dir; `auth.json` inside is pi-managed (lockfiled). */
7819
7945
  piDir;
7820
7946
  constructor(root) {
7821
7947
  this.root = root;
7822
- this.agentsDir = join(root, "agents");
7948
+ this.identitiesDir = join(root, "identities");
7823
7949
  this.runsDir = join(root, "runs");
7824
7950
  this.secretsDir = join(root, "secrets");
7825
7951
  this.piDir = join(root, "pi");
@@ -7828,18 +7954,85 @@ var AgentServerStore = class {
7828
7954
  return join(this.piDir, "auth.json");
7829
7955
  }
7830
7956
  /** Create the directory layout (0700) if missing. Idempotent. */
7831
- ensure() {
7957
+ ensure(options = {}) {
7958
+ this.adoptLegacyXdgRoot(options.legacyXdgConfigHome ?? "");
7832
7959
  for (const dir of [
7833
7960
  this.root,
7834
- this.agentsDir,
7961
+ this.identitiesDir,
7835
7962
  this.runsDir,
7836
7963
  this.secretsDir
7837
7964
  ]) mkdirSync(dir, {
7838
7965
  recursive: true,
7839
7966
  mode: 448
7840
7967
  });
7968
+ this.migrateLegacyAgentDocuments();
7841
7969
  return this;
7842
7970
  }
7971
+ /**
7972
+ * Adopt state left at the pre-#1834 XDG root.
7973
+ *
7974
+ * The daemon used to resolve `$XDG_CONFIG_HOME/moltnet` while the CLI and SDK
7975
+ * resolved `~/.config/moltnet`. Aligning the root without this would silently
7976
+ * orphan an existing daemon's entire state — it would come up reporting zero
7977
+ * managed agents.
7978
+ */
7979
+ adoptLegacyXdgRoot(xdgConfigHome) {
7980
+ const legacyRoot = legacyXdgAgentServerRoot(xdgConfigHome);
7981
+ if (!legacyRoot || legacyRoot === this.root) return;
7982
+ if (!hasAgentServerState(legacyRoot)) return;
7983
+ if (hasAgentServerState(this.root)) throw new AgentServerStoreError("invalid_state", `agent server state exists at both ${legacyRoot} (the pre-1834 XDG_CONFIG_HOME location) and ${this.root}. The daemon now shares ${this.root} with the CLI and SDK. Merge or remove one of them, or set MOLTNET_AGENT_SERVER_ROOT to choose explicitly.`);
7984
+ mkdirSync(resolve(this.root, ".."), {
7985
+ recursive: true,
7986
+ mode: 448
7987
+ });
7988
+ try {
7989
+ if (readdirSync(this.root).length === 0) rmSync(this.root, { recursive: true });
7990
+ } catch {}
7991
+ try {
7992
+ renameSync(legacyRoot, this.root);
7993
+ } catch (error) {
7994
+ if (error.code !== "EXDEV") throw new AgentServerStoreError("io_error", `could not adopt the pre-1834 agent server state at ${legacyRoot}: ${String(error.message)}. Move it to ${this.root} manually, or set MOLTNET_AGENT_SERVER_ROOT to choose a root explicitly.`);
7995
+ cpSync(legacyRoot, this.root, { recursive: true });
7996
+ rmSync(legacyRoot, {
7997
+ recursive: true,
7998
+ force: true
7999
+ });
8000
+ }
8001
+ }
8002
+ /**
8003
+ * Migrate managed documents from the pre-#1834 `agents/<alias>.json` layout
8004
+ * to `identities/<alias>/moltnet.json`. Without this an upgraded daemon sees
8005
+ * zero managed agents and reports a plain "not found".
8006
+ */
8007
+ migrateLegacyAgentDocuments() {
8008
+ const legacyDir = join(this.root, "agents");
8009
+ let entries;
8010
+ try {
8011
+ entries = readdirSync(legacyDir);
8012
+ } catch {
8013
+ return;
8014
+ }
8015
+ for (const entry of entries) {
8016
+ if (!entry.endsWith(".json")) continue;
8017
+ const alias = entry.slice(0, -5);
8018
+ if (!NAME_RE.test(alias)) continue;
8019
+ const legacyPath = join(legacyDir, entry);
8020
+ const target = this.agentPath(alias);
8021
+ if (readJson(target)) continue;
8022
+ const config = readJson(legacyPath);
8023
+ if (!config) continue;
8024
+ mkdirSync(join(this.identitiesDir, alias), {
8025
+ recursive: true,
8026
+ mode: 448
8027
+ });
8028
+ writeJsonAtomic(target, config);
8029
+ rmSync(legacyPath, { force: true });
8030
+ }
8031
+ if (readdirSync(legacyDir).length === 0) rmSync(legacyDir, {
8032
+ recursive: true,
8033
+ force: true
8034
+ });
8035
+ }
7843
8036
  get statePath() {
7844
8037
  return join(this.root, "agent-server.json");
7845
8038
  }
@@ -7868,16 +8061,58 @@ var AgentServerStore = class {
7868
8061
  writeJsonAtomic(this.statePath, state);
7869
8062
  }
7870
8063
  agentPath(name) {
7871
- return storeChildPath(this.agentsDir, "agent name", name, ".json");
8064
+ return join(storeChildPath(this.identitiesDir, "identity alias", name), "moltnet.json");
8065
+ }
8066
+ /** Central identity directory, shared with the Go CLI layout. */
8067
+ identityDir(name) {
8068
+ return storeChildPath(this.identitiesDir, "identity alias", name);
8069
+ }
8070
+ get identitySelectorPath() {
8071
+ return join(this.root, "identity-selector.json");
8072
+ }
8073
+ readIdentitySelector() {
8074
+ const selector = readJson(this.identitySelectorPath);
8075
+ if (!selector) return null;
8076
+ if (!isRecord$1(selector) || selector.version !== 1 || selector.default_identity !== void 0 && typeof selector.default_identity !== "string") throw new AgentServerStoreError("invalid_state", "identity-selector.json is not a supported selector document");
8077
+ if (selector.default_identity) assertStoreName("identity alias", selector.default_identity);
8078
+ return selector;
8079
+ }
8080
+ writeIdentitySelector(alias) {
8081
+ writeJsonAtomic(this.identitySelectorPath, {
8082
+ version: 1,
8083
+ default_identity: assertStoreName("identity alias", alias)
8084
+ });
8085
+ }
8086
+ resolveIdentityAlias(explicit, active) {
8087
+ const alias = explicit?.trim() || active?.trim() || this.readIdentitySelector()?.default_identity;
8088
+ if (!alias) throw new AgentServerStoreError("not_found", "no active identity selected");
8089
+ return assertStoreName("identity alias", alias);
7872
8090
  }
7873
8091
  readAgentConfig(alias) {
7874
8092
  return readJson(this.agentPath(alias));
7875
8093
  }
7876
8094
  writeAgentConfig(alias, config) {
8095
+ mkdirSync(this.identityDir(alias), {
8096
+ recursive: true,
8097
+ mode: 448
8098
+ });
7877
8099
  writeJsonAtomic(this.agentPath(alias), config);
8100
+ if (!this.readIdentitySelector()?.default_identity) this.writeIdentitySelector(alias);
7878
8101
  }
7879
8102
  removeAgentConfig(alias) {
7880
8103
  rmSync(this.agentPath(alias), { force: true });
8104
+ this.clearIdentitySelectorIfDefault(alias);
8105
+ }
8106
+ /** Clears the persisted default when it names `alias`. */
8107
+ clearIdentitySelectorIfDefault(alias) {
8108
+ let selector = null;
8109
+ try {
8110
+ selector = this.readIdentitySelector();
8111
+ } catch {
8112
+ return;
8113
+ }
8114
+ if (!selector || selector.default_identity !== alias) return;
8115
+ writeJsonAtomic(this.identitySelectorPath, { version: 1 });
7881
8116
  }
7882
8117
  readActivation(alias) {
7883
8118
  return this.readAgentServerState().activations[assertStoreName("agent name", alias)] ?? null;
@@ -10387,12 +10622,9 @@ async function runAgentServer(argv) {
10387
10622
  return 1;
10388
10623
  }
10389
10624
  const allowedOrigins = parseAllowedOrigins(values["allowed-origins"] ?? (envConfig.allowedOrigins || DEFAULT_ALLOWED_ORIGINS));
10390
- const root = values.root ?? resolveAgentServerRoot({
10391
- root: envConfig.root,
10392
- xdgConfigHome: envConfig.xdgConfigHome
10393
- });
10625
+ const root = values.root ?? resolveAgentServerRoot({ root: envConfig.root });
10394
10626
  const defaultApiUrl = values["api-url"] ?? (envConfig.apiUrl || DEFAULT_API_URL);
10395
- const store = new AgentServerStore(root).ensure();
10627
+ const store = new AgentServerStore(root).ensure({ legacyXdgConfigHome: envConfig.xdgConfigHome });
10396
10628
  if (trustRequested) return runTrustCommand(commandArgs, root);
10397
10629
  const { logger, shutdown: shutdownLogger } = createRootLogger({
10398
10630
  name: "agent-daemon.server",
@@ -10686,7 +10918,13 @@ async function runSyncSessions(argv) {
10686
10918
  const state = parseState(values.state);
10687
10919
  const limit = parseLimit(values.limit);
10688
10920
  const agentRootDir = resolve(process.cwd(), values["agent-root"] ?? process.cwd());
10689
- const ctx = await resolveAgentContext(opts.agent, { agentRootDir });
10921
+ const explicitAgentRootDir = values["agent-root"] ? resolve(process.cwd(), values["agent-root"]) : void 0;
10922
+ const cfg = loadConfig();
10923
+ const ctx = await resolveAgentContext(opts.agent, {
10924
+ credentialSource: cfg.credentialSource,
10925
+ envApiUrl: cfg.apiUrl,
10926
+ agentRootDir: explicitAgentRootDir
10927
+ });
10690
10928
  await validateStartupBinding({
10691
10929
  agent: ctx.agent,
10692
10930
  teamId: values.team
@@ -10721,18 +10959,51 @@ function parseLimit(raw) {
10721
10959
  }
10722
10960
  //#endregion
10723
10961
  //#region src/lib/update.ts
10724
- var UPDATE_MANIFEST_URL = "https://themolt.net/download/manifest.json";
10962
+ /**
10963
+ * `npm install -g @themoltnet/agent-daemon@latest` resolves the registry's own
10964
+ * dist-tag, so for an npm install the registry is the precise answer — the
10965
+ * manifest serves the themolt.net pin, which gates the bundle installer's
10966
+ * default and says nothing about what npm already has. It is also the origin an
10967
+ * npm install already talks to, so this adds no new dependency in CI, where the
10968
+ * daemon runs as `npx @themoltnet/agent-daemon` and would otherwise reach for
10969
+ * api.github.com from shared runner IPs.
10970
+ *
10971
+ * Bundle and direct installs ask the release listing, and their upgrade command
10972
+ * passes `MOLTNET_AGENT_VERSION=latest` so the installer resolves the same thing
10973
+ * rather than falling back to its own pinned default. Reporting the pin here
10974
+ * would hide releases that are already downloadable; reporting the release
10975
+ * without the sentinel would advertise a version the command could not deliver.
10976
+ * The pin still governs a fresh `curl .../install/agent | sh`.
10977
+ */
10978
+ var UPDATE_RELEASES_URL = "https://api.github.com/repos/getlarge/themoltnet/releases?per_page=100";
10979
+ var RELEASE_TAG_PREFIX = "agent-daemon-v";
10980
+ var UPDATE_NPM_REGISTRY_URL = "https://registry.npmjs.org/@themoltnet/agent-daemon/latest";
10725
10981
  var UPDATE_CACHE_TTL_MS = 1440 * 60 * 1e3;
10726
10982
  var UPDATE_ERROR_CACHE_TTL_MS = 300 * 1e3;
10983
+ /**
10984
+ * Node does not resolve `process.argv[1]`: invoked through npm's bin shim it
10985
+ * reports `/usr/local/bin/moltnet-agent`, not the `node_modules` path this
10986
+ * matches on. Every global npm install therefore looked like `direct` and was
10987
+ * offered the bundle installer, which would drop a bundle on top of an npm
10988
+ * install. Resolve first; a missing path falls back to the input.
10989
+ */
10990
+ function resolveDaemonExecutable(executable) {
10991
+ if (!executable) return executable;
10992
+ try {
10993
+ return realpathSync(executable);
10994
+ } catch {
10995
+ return executable;
10996
+ }
10997
+ }
10727
10998
  function detectDaemonInstallMethod(executable = process.argv[1] ?? "") {
10728
- const path = executable.replaceAll("\\", "/");
10999
+ const path = resolveDaemonExecutable(executable).replaceAll("\\", "/");
10729
11000
  if (path.includes("/node_modules/@themoltnet/agent-daemon/")) return "npm";
10730
11001
  if (path.includes("/.local/share/moltnet/") || path.includes("/opt/moltnet/")) return "bundle";
10731
11002
  return "direct";
10732
11003
  }
10733
11004
  function daemonUpdateCommand(method) {
10734
11005
  if (method === "npm") return "npm install -g @themoltnet/agent-daemon@latest";
10735
- return "curl -fsSL https://themolt.net/install/agent | sh";
11006
+ return "curl -fsSL https://themolt.net/install/agent | MOLTNET_AGENT_VERSION=latest sh";
10736
11007
  }
10737
11008
  async function checkDaemonUpdate(input) {
10738
11009
  const installMethod = detectDaemonInstallMethod(input.executable ?? process.argv[1] ?? "moltnet-agent");
@@ -10753,11 +11024,15 @@ async function checkDaemonUpdate(input) {
10753
11024
  return result;
10754
11025
  }
10755
11026
  }
11027
+ const viaNpm = installMethod === "npm";
11028
+ const source = viaNpm ? UPDATE_NPM_REGISTRY_URL : UPDATE_RELEASES_URL;
11029
+ const label = viaNpm ? "npm registry" : "release listing";
10756
11030
  try {
10757
- const response = await (input.fetchFn ?? fetch)(UPDATE_MANIFEST_URL, { signal: AbortSignal.timeout(5e3) });
10758
- if (!response.ok) throw new Error(`manifest returned HTTP ${response.status}`);
10759
- const latest = manifestVersion(await response.json());
10760
- if (!latest) throw new Error("manifest has no valid agent version");
11031
+ const response = await (input.fetchFn ?? fetch)(source, { signal: AbortSignal.timeout(5e3) });
11032
+ if (!response.ok) throw new Error(`${label} returned HTTP ${response.status}`);
11033
+ const body = await response.json();
11034
+ const latest = viaNpm ? distTagVersion(body) : newestReleaseVersion(body);
11035
+ if (!latest) throw new Error(`${label} has no valid agent version`);
10761
11036
  await writeCache({
10762
11037
  checkedAt: now.toISOString(),
10763
11038
  latest
@@ -10773,13 +11048,31 @@ async function checkDaemonUpdate(input) {
10773
11048
  throw new Error(`could not check for MoltNet agent updates: ${error instanceof Error ? error.message : String(error)}`);
10774
11049
  }
10775
11050
  }
10776
- function manifestVersion(value) {
11051
+ function distTagVersion(value) {
10777
11052
  if (!value || typeof value !== "object") return void 0;
10778
- const agent = value.agent;
10779
- if (!agent || typeof agent !== "object") return void 0;
10780
- const version = agent.version;
11053
+ const version = value.version;
10781
11054
  return typeof version === "string" && validVersion(version) ? normalizeVersion(version) : void 0;
10782
11055
  }
11056
+ /**
11057
+ * The listing is ordered by creation date, not version, so compare every
11058
+ * candidate rather than trusting position. Drafts are filtered explicitly:
11059
+ * unauthenticated callers never see them, but a token would, and this
11060
+ * repository carries stuck drafts that would otherwise look newest.
11061
+ */
11062
+ function newestReleaseVersion(value) {
11063
+ if (!Array.isArray(value)) return void 0;
11064
+ let newest;
11065
+ for (const entry of value) {
11066
+ if (!entry || typeof entry !== "object") continue;
11067
+ const { tag_name: tag, draft, prerelease } = entry;
11068
+ if (draft === true || prerelease === true) continue;
11069
+ if (typeof tag !== "string" || !tag.startsWith(RELEASE_TAG_PREFIX)) continue;
11070
+ const candidate = tag.slice(14);
11071
+ if (!validVersion(candidate)) continue;
11072
+ if (!newest || compareVersions(candidate, newest) > 0) newest = normalizeVersion(candidate);
11073
+ }
11074
+ return newest;
11075
+ }
10783
11076
  function normalizeVersion(value) {
10784
11077
  return value.trim().replace(/^v/, "");
10785
11078
  }
@@ -10819,7 +11112,7 @@ async function writeCache(cache) {
10819
11112
  }
10820
11113
  //#endregion
10821
11114
  //#region src/version.ts
10822
- var DAEMON_VERSION = "0.53.0";
11115
+ var DAEMON_VERSION = "0.54.0";
10823
11116
  //#endregion
10824
11117
  //#region src/cli.ts
10825
11118
  async function runAgentDaemonCli(options) {
@@ -10877,11 +11170,7 @@ async function runUpdate(argv) {
10877
11170
  }
10878
11171
  }
10879
11172
  async function runRuntime(argv) {
10880
- const config = loadAgentServerEnvConfig();
10881
- const registry = new RuntimeRegistry(resolveAgentServerRoot({
10882
- root: config.root,
10883
- xdgConfigHome: config.xdgConfigHome
10884
- }));
11173
+ const registry = new RuntimeRegistry(resolveAgentServerRoot({ root: loadAgentServerEnvConfig().root }));
10885
11174
  const [command, ...rest] = argv;
10886
11175
  if (command === "list") {
10887
11176
  console.log(JSON.stringify(registry.list(), null, 2));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@themoltnet/agent-daemon",
3
- "version": "0.53.0",
3
+ "version": "0.54.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.",
@@ -85,10 +85,10 @@
85
85
  "proper-lockfile": "4.1.2",
86
86
  "reflect-metadata": "^0.2.2",
87
87
  "typebox": "^1.2.8",
88
- "@themoltnet/agent-runtime": "0.45.3",
89
- "@themoltnet/os-keyring": "0.3.0",
90
- "@themoltnet/pi-runtime": "0.14.2",
91
- "@themoltnet/sdk": "0.140.1"
88
+ "@themoltnet/pi-runtime": "0.14.3",
89
+ "@themoltnet/agent-runtime": "0.46.0",
90
+ "@themoltnet/sdk": "0.140.2",
91
+ "@themoltnet/os-keyring": "0.3.0"
92
92
  },
93
93
  "devDependencies": {
94
94
  "@fastify/swagger": "^9.6.1",
@@ -98,16 +98,16 @@
98
98
  "vite": "^8.0.0",
99
99
  "vite-plugin-dts": "^4.5.4",
100
100
  "vitest": "^3.0.0",
101
- "@moltnet/agent-eval": "0.1.0",
102
- "@moltnet/crypto-service": "0.1.0",
103
- "@moltnet/execution-integrations": "0.1.0",
104
101
  "@moltnet/bootstrap": "0.1.0",
102
+ "@moltnet/crypto-service": "0.1.0",
105
103
  "@moltnet/execution-plan": "0.1.0",
106
- "@moltnet/models": "0.1.0",
107
- "@moltnet/observability": "0.1.0",
108
- "@moltnet/loopback-companion": "0.1.0",
104
+ "@moltnet/execution-integrations": "0.1.0",
109
105
  "@moltnet/runtime-profiles": "0.1.0",
110
- "@moltnet/tasks": "0.1.0"
106
+ "@moltnet/tasks": "0.1.0",
107
+ "@moltnet/loopback-companion": "0.1.0",
108
+ "@moltnet/agent-eval": "0.1.0",
109
+ "@moltnet/observability": "0.1.0",
110
+ "@moltnet/models": "0.1.0"
111
111
  },
112
112
  "nx": {
113
113
  "projectType": "application",
@@ -132,6 +132,14 @@
132
132
  "cwd": "{workspaceRoot}",
133
133
  "command": "node tools/release/agent-bundle/build.mjs --skip-daemon-build --out dist/agent-bundle"
134
134
  }
135
+ },
136
+ "test": {
137
+ "inputs": [
138
+ "default",
139
+ "^production",
140
+ "{workspaceRoot}/docs/operate/*.md",
141
+ "{workspaceRoot}/docs/.vitepress/config.ts"
142
+ ]
135
143
  }
136
144
  }
137
145
  },