@themoltnet/agent-daemon 0.52.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 +53 -26
  2. package/dist/cli.js +423 -127
  3. package/package.json +18 -10
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
@@ -135,14 +154,22 @@ of `MOLTNET_PRIVATE_KEY`.
135
154
  An agent key used by the daemon needs this least-privilege scope set:
136
155
 
137
156
  ```text
138
- agent:profile runtime:read task:read task:claim task:execute
157
+ agent:profile crypto:sign runtime:read task:read task:claim task:execute
139
158
  ```
140
159
 
141
- The Console selects these five scopes by default. Knowledge-enabled workers
142
- must add `diary:read`, `diary:write`, `pack:read`, and `pack:write` when the key
143
- is issued. Runtime policy can narrow key authority but cannot add missing
144
- scopes, and existing keys are never widened automatically; issue a replacement
145
- credential when broader authority is required.
160
+ The Console selects these scopes by default. Knowledge-enabled workers must add
161
+ `diary:read`, `diary:write`, `pack:read`, and `pack:write` when the key is
162
+ issued.
163
+
164
+ `crypto:sign` is in the minimum because host-capability signing runs on the
165
+ daemon's own credential: the local seed signer calls the signing-request
166
+ endpoints, which require it. Without it the daemon refuses to start rather than
167
+ booting cleanly and failing the first time guest code signs a diary entry or a
168
+ commit. Keys issued with the older five-scope set must be reissued.
169
+
170
+ Runtime policy can narrow key authority but cannot add missing scopes, and
171
+ existing keys are never widened automatically; issue a replacement credential
172
+ when broader authority is required.
146
173
 
147
174
  ### Pi provider auth
148
175
 
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
@@ -779,16 +779,30 @@ var ALL_CREDENTIAL_SCOPES = Object.freeze(Object.values(CREDENTIAL_SCOPES));
779
779
  /**
780
780
  * Minimum grant for the agent daemon. Task credentials attenuate this further
781
781
  * to `task:execute` alone.
782
+ *
783
+ * `crypto:sign` is part of the minimum because host-capability signing runs on
784
+ * the daemon's own credential: the local seed signer calls the signing-request
785
+ * endpoints, which require it. A grant without it produces a daemon that boots
786
+ * cleanly and then fails the first time guest code signs a diary entry or a
787
+ * commit.
782
788
  */
783
789
  var AGENT_CREDENTIAL_SCOPES = [
784
790
  CREDENTIAL_SCOPES.AgentProfile,
791
+ CREDENTIAL_SCOPES.CryptoSign,
785
792
  CREDENTIAL_SCOPES.RuntimeRead,
786
793
  CREDENTIAL_SCOPES.TaskRead,
787
794
  CREDENTIAL_SCOPES.TaskClaim,
788
795
  CREDENTIAL_SCOPES.TaskExecute
789
796
  ];
790
797
  Object.freeze(ALL_CREDENTIAL_SCOPES.filter((scope) => scope !== CREDENTIAL_SCOPES.HumanProfile));
791
- [
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 = [
792
806
  CREDENTIAL_SCOPES.AgentProfile,
793
807
  CREDENTIAL_SCOPES.CryptoSign,
794
808
  CREDENTIAL_SCOPES.DiaryManage,
@@ -802,7 +816,13 @@ Object.freeze(ALL_CREDENTIAL_SCOPES.filter((scope) => scope !== CREDENTIAL_SCOPE
802
816
  CREDENTIAL_SCOPES.TaskRead,
803
817
  CREDENTIAL_SCOPES.TeamManage,
804
818
  CREDENTIAL_SCOPES.TeamRead
805
- ].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]);
806
826
  //#endregion
807
827
  //#region ../../libs/models/src/preview-sign.ts
808
828
  function schemaRef$2(schema, id) {
@@ -3444,9 +3464,19 @@ Commands:
3444
3464
  Run \`agent-daemon <command> --help\` for command-specific flags.
3445
3465
 
3446
3466
  Prerequisites:
3447
- - agent-key mode: MOLTNET_AGENT_KEY (or MOLTNET_AGENT_KEY_REF) and
3467
+ - configless: MOLTNET_AGENT_KEY (or MOLTNET_AGENT_KEY_REF) and
3448
3468
  MOLTNET_PRIVATE_KEY (or MOLTNET_PRIVATE_KEY_REF); no agent files
3449
- - 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
3450
3480
  - --profile — remote runtime profile supplies provider/model/sandbox
3451
3481
  policy and CWD is used as the VM mountPath.
3452
3482
 
@@ -3610,18 +3640,30 @@ function assessIdentityPin(current, expected) {
3610
3640
  //#endregion
3611
3641
  //#region src/lib/agent-context.ts
3612
3642
  /**
3613
- * Report which auth mode `connect()` will use, without ever reading the secret
3614
- * value into anything logged. Agent-key mode is selected when
3615
- * `MOLTNET_AGENT_KEY` or `MOLTNET_AGENT_KEY_REF` holds a non-blank value
3616
- * mirroring the SDK precedence
3617
- * where an environment key opts into key mode ahead of the config-file OAuth2
3618
- * credentials. The daemon never passes explicit in-code credentials to
3619
- * `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`).
3620
3662
  *
3621
3663
  * Pure: `env` is passed in (the config module owns the `process.env` read).
3622
3664
  */
3623
- function detectAuthMode(env) {
3624
- 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";
3625
3667
  }
3626
3668
  /**
3627
3669
  * Pure check: may the identity described by `whoami` operate the daemon as
@@ -3677,40 +3719,111 @@ async function validateStartupBinding(options) {
3677
3719
  /**
3678
3720
  * Resolve the agent's MoltNet credentials directory and connect via SDK.
3679
3721
  *
3680
- * Looks under an explicit agent root first, then falls back to the git root
3681
- * when available. Fails fast if the dir is missing credentials are required,
3682
- * 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.
3683
3724
  */
3684
3725
  async function resolveAgentContext(agentName, options = {}) {
3685
- if (!/^[a-zA-Z0-9_-]+$/.test(agentName)) throw new Error(`Invalid agent name "${agentName}": must match /^[a-zA-Z0-9_-]+$/`);
3686
- const roots = resolveCredentialRoots(options.agentRootDir);
3687
- if (options.authMode === "agent-key") {
3688
- const rootDir = roots[0] ?? process.cwd();
3689
- return {
3690
- agentDir: join(rootDir, ".moltnet", agentName),
3691
- agentRootDir: rootDir,
3692
- agent: await connect({ secretProviders: createNodeSecretProviderRegistry() }),
3693
- credentialSource: "environment",
3694
- authMechanism: "agent-key"
3695
- };
3696
- }
3697
- const located = locateAgentConfig(roots, agentName);
3698
- if (located) {
3699
- const agent = await connect({
3700
- configDir: located.agentDir,
3701
- secretProviders: createNodeSecretProviderRegistry()
3702
- });
3703
- const config = await readConfig(located.agentDir);
3704
- return {
3705
- agentDir: located.agentDir,
3706
- agentRootDir: located.rootDir,
3707
- agent,
3708
- credentialSource: "config",
3709
- 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
3710
3806
  };
3711
3807
  }
3712
- const tried = roots.map((root) => join(root, ".moltnet", agentName));
3713
- 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;
3714
3827
  }
3715
3828
  function isTransientWhoamiError(error) {
3716
3829
  if (error instanceof TypeError) return true;
@@ -3718,26 +3831,6 @@ function isTransientWhoamiError(error) {
3718
3831
  const statusCode = error.statusCode;
3719
3832
  return typeof statusCode === "number" && (statusCode === 408 || statusCode === 429 || statusCode >= 500);
3720
3833
  }
3721
- function locateAgentConfig(roots, agentName) {
3722
- for (const rootDir of roots) {
3723
- const agentDir = join(rootDir, ".moltnet", agentName);
3724
- if (existsSync(join(agentDir, "moltnet.json"))) return {
3725
- rootDir,
3726
- agentDir
3727
- };
3728
- }
3729
- }
3730
- function resolveCredentialRoots(agentRootDir) {
3731
- const roots = agentRootDir ? [agentRootDir] : [];
3732
- try {
3733
- const gitRoot = execFileSync("git", ["rev-parse", "--show-toplevel"], {
3734
- encoding: "utf8",
3735
- stdio: "pipe"
3736
- }).trim();
3737
- if (!roots.includes(gitRoot)) roots.push(gitRoot);
3738
- } catch {}
3739
- return roots;
3740
- }
3741
3834
  //#endregion
3742
3835
  //#region src/config.ts
3743
3836
  /**
@@ -3757,7 +3850,8 @@ function loadConfig() {
3757
3850
  profilePrerequisiteEnv: process.env,
3758
3851
  profilePrerequisitePath: process.env.PATH ?? "",
3759
3852
  piCodingAgentDir: process.env["PI_CODING_AGENT_DIR"] ?? "",
3760
- authMode: detectAuthMode(process.env),
3853
+ credentialSource: detectCredentialSource(process.env),
3854
+ apiUrl: process.env["MOLTNET_API_URL"] ?? "",
3761
3855
  signingPrivateKey: process.env["MOLTNET_PRIVATE_KEY"] ?? "",
3762
3856
  signingPrivateKeyRef: process.env["MOLTNET_PRIVATE_KEY_REF"] ?? "",
3763
3857
  gitAuthor: process.env["MOLTNET_GIT_AUTHOR"] ?? "",
@@ -3832,13 +3926,13 @@ async function abortActiveAttemptOnSignal(opts) {
3832
3926
  //#region src/lib/agent-identity.ts
3833
3927
  /**
3834
3928
  * Build the non-secret identity projected into guests. Host git config is a
3835
- * non-secret input and is consulted only on OAuth2 hosts, which already read
3836
- * that configuration for the signing seed; configless agent-key hosts never
3837
- * 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.
3838
3932
  */
3839
3933
  async function resolveDaemonAgentIdentity(input) {
3840
3934
  let hostGit;
3841
- if (input.gitAuthor === void 0 && input.authMode === "oauth2") {
3935
+ if (input.gitAuthor === void 0 && input.credentialSource === "config") {
3842
3936
  const config = await readConfig(input.agentDir);
3843
3937
  hostGit = config?.git ? {
3844
3938
  name: config.git.name,
@@ -4383,26 +4477,26 @@ function recoverScratchWorkspacePath(producer, stateDirs) {
4383
4477
  //#region src/lib/executor-attestation.ts
4384
4478
  var DAEMON_REQUIRED_SCOPES = AGENT_CREDENTIAL_SCOPES;
4385
4479
  async function resolveExecutorSigningPrivateKey(input) {
4386
- if (input.authMode === "agent-key") {
4480
+ if (input.credentialSource === "environment") {
4387
4481
  const privateKey = input.configuredPrivateKey.trim();
4388
4482
  if (privateKey) return privateKey;
4389
4483
  const reference = input.configuredPrivateKeyRef?.trim();
4390
- 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.");
4391
4485
  let resolved;
4392
4486
  try {
4393
4487
  resolved = await resolveEnvSecretReference(reference, createNodeSecretProviderRegistry());
4394
4488
  } catch (cause) {
4395
- 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 });
4396
4490
  }
4397
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.");
4398
4492
  return resolved;
4399
4493
  }
4400
4494
  const config = await readConfig(input.agentDir);
4401
- 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.`);
4402
4496
  try {
4403
4497
  return await resolveIdentitySeed(config, createNodeSecretProviderRegistry());
4404
4498
  } catch (cause) {
4405
- 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 });
4406
4500
  }
4407
4501
  }
4408
4502
  function validateDaemonScopes(whoami) {
@@ -5098,7 +5192,7 @@ async function logDaemonStartupFailure(input) {
5098
5192
  event: "agent-daemon.startup_failed",
5099
5193
  gate: input.gate,
5100
5194
  agent: input.agent,
5101
- authMode: input.authMode
5195
+ credentialSource: input.credentialSource
5102
5196
  }, "Agent daemon startup validation failed");
5103
5197
  await shutdown();
5104
5198
  }
@@ -5917,13 +6011,14 @@ async function runPolling(opts) {
5917
6011
  bindings: cfg.credentialBindings
5918
6012
  };
5919
6013
  const runtimeCredentialConfig = resolveCredentialEnforcement(cfg.credentialEnforcement, credentialSources) === "off" ? null : loadRuntimeCredentialConfig(credentialSources);
5920
- const agentRootDir = resolve(process.cwd(), values["agent-root"] ?? process.cwd());
6014
+ const explicitAgentRootDir = values["agent-root"] ? resolve(process.cwd(), values["agent-root"]) : void 0;
5921
6015
  const { ctx, signingPrivateKey, startupWhoami, agentIdentity, hostCapabilitySigner } = await (async () => {
5922
6016
  let gate = "resolve_agent_context";
5923
6017
  try {
5924
6018
  const resolvedContext = await resolveAgentContext(baseCommon.agent, {
5925
- agentRootDir,
5926
- authMode: cfg.authMode
6019
+ agentRootDir: explicitAgentRootDir,
6020
+ credentialSource: cfg.credentialSource,
6021
+ envApiUrl: cfg.apiUrl
5927
6022
  });
5928
6023
  gate = "authenticate_and_bind";
5929
6024
  const whoami = await validateStartupBinding({
@@ -5933,7 +6028,7 @@ async function runPolling(opts) {
5933
6028
  });
5934
6029
  gate = "resolve_signing_material";
5935
6030
  const privateKey = await resolveExecutorSigningPrivateKey({
5936
- authMode: cfg.authMode,
6031
+ credentialSource: cfg.credentialSource,
5937
6032
  agentDir: resolvedContext.agentDir,
5938
6033
  configuredPrivateKey: cfg.signingPrivateKey,
5939
6034
  configuredPrivateKeyRef: cfg.signingPrivateKeyRef
@@ -5949,7 +6044,7 @@ async function runPolling(opts) {
5949
6044
  const agentIdentity = await resolveDaemonAgentIdentity({
5950
6045
  agentName: baseCommon.agent,
5951
6046
  whoami,
5952
- authMode: cfg.authMode,
6047
+ credentialSource: cfg.credentialSource,
5953
6048
  agentDir: resolvedContext.agentDir,
5954
6049
  gitAuthor: values["git-author"] ?? (cfg.gitAuthor || void 0)
5955
6050
  });
@@ -5970,7 +6065,7 @@ async function runPolling(opts) {
5970
6065
  level: cfg.logLevel || (baseCommon.debug ? "debug" : "info"),
5971
6066
  gate,
5972
6067
  agent: baseCommon.agent,
5973
- authMode: cfg.authMode,
6068
+ credentialSource: cfg.credentialSource,
5974
6069
  error
5975
6070
  });
5976
6071
  throw error;
@@ -6074,7 +6169,7 @@ async function runPolling(opts) {
6074
6169
  resourceAttributes: {
6075
6170
  "moltnet.team.id": teamId,
6076
6171
  "moltnet.agent.name": baseCommon.agent,
6077
- "moltnet.auth.mode": ctx.authMechanism,
6172
+ "moltnet.credential.source": ctx.credentialSource,
6078
6173
  "moltnet.runtime_profile.count": String(profiles.length),
6079
6174
  "moltnet.runtime_profile.ids": profiles.map((p) => p.id).join(",")
6080
6175
  }
@@ -6122,7 +6217,7 @@ async function runPolling(opts) {
6122
6217
  }
6123
6218
  });
6124
6219
  rootLogger.info({
6125
- authMode: cfg.authMode,
6220
+ credentialSource: cfg.credentialSource,
6126
6221
  subjectType: startupWhoami.subjectType,
6127
6222
  bindingScope: startupWhoami.credentialBinding?.bindingScope ?? null,
6128
6223
  credentialKeyId: startupWhoami.credentialBinding?.keyId ?? null,
@@ -6574,13 +6669,14 @@ async function runOnce(argv, runtimeAdapter = defaultPiDaemonAdapter) {
6574
6669
  };
6575
6670
  const runtimeCredentialConfig = resolveCredentialEnforcement(cfg.credentialEnforcement, credentialSources) === "off" ? null : loadRuntimeCredentialConfig(credentialSources);
6576
6671
  const initialOpts = opts;
6577
- const agentRootDir = resolve(process.cwd(), values["agent-root"] ?? process.cwd());
6672
+ const explicitAgentRootDir = values["agent-root"] ? resolve(process.cwd(), values["agent-root"]) : void 0;
6578
6673
  const { ctx, signingPrivateKey, agentIdentity, hostCapabilitySigner } = await (async () => {
6579
6674
  let gate = "resolve_agent_context";
6580
6675
  try {
6581
6676
  const resolvedContext = await resolveAgentContext(initialOpts.agent, {
6582
- agentRootDir,
6583
- authMode: cfg.authMode
6677
+ agentRootDir: explicitAgentRootDir,
6678
+ credentialSource: cfg.credentialSource,
6679
+ envApiUrl: cfg.apiUrl
6584
6680
  });
6585
6681
  gate = "authenticate_and_bind";
6586
6682
  const whoami = await validateStartupBinding({
@@ -6590,7 +6686,7 @@ async function runOnce(argv, runtimeAdapter = defaultPiDaemonAdapter) {
6590
6686
  });
6591
6687
  gate = "resolve_signing_material";
6592
6688
  const privateKey = await resolveExecutorSigningPrivateKey({
6593
- authMode: cfg.authMode,
6689
+ credentialSource: cfg.credentialSource,
6594
6690
  agentDir: resolvedContext.agentDir,
6595
6691
  configuredPrivateKey: cfg.signingPrivateKey,
6596
6692
  configuredPrivateKeyRef: cfg.signingPrivateKeyRef
@@ -6606,7 +6702,7 @@ async function runOnce(argv, runtimeAdapter = defaultPiDaemonAdapter) {
6606
6702
  const agentIdentity = await resolveDaemonAgentIdentity({
6607
6703
  agentName: initialOpts.agent,
6608
6704
  whoami,
6609
- authMode: cfg.authMode,
6705
+ credentialSource: cfg.credentialSource,
6610
6706
  agentDir: resolvedContext.agentDir,
6611
6707
  gitAuthor: values["git-author"] ?? (cfg.gitAuthor || void 0)
6612
6708
  });
@@ -6626,7 +6722,7 @@ async function runOnce(argv, runtimeAdapter = defaultPiDaemonAdapter) {
6626
6722
  level: cfg.logLevel || (initialOpts.debug ? "debug" : "info"),
6627
6723
  gate,
6628
6724
  agent: initialOpts.agent,
6629
- authMode: cfg.authMode,
6725
+ credentialSource: cfg.credentialSource,
6630
6726
  error
6631
6727
  });
6632
6728
  throw error;
@@ -6696,7 +6792,7 @@ async function runOnce(argv, runtimeAdapter = defaultPiDaemonAdapter) {
6696
6792
  resourceAttributes: {
6697
6793
  "moltnet.task.id": taskId,
6698
6794
  "moltnet.agent.name": opts.agent,
6699
- "moltnet.auth.mode": ctx.authMechanism,
6795
+ "moltnet.credential.source": ctx.credentialSource,
6700
6796
  "moltnet.llm.provider": profile.provider,
6701
6797
  "moltnet.llm.model": profile.model,
6702
6798
  ...profile.thinkingLevel ? { "moltnet.llm.thinking_level": profile.thinkingLevel } : {},
@@ -7745,11 +7841,14 @@ function snapshot(login) {
7745
7841
  ...waitingForAuthorization ? { waitingForAuthorization } : {}
7746
7842
  };
7747
7843
  }
7748
- var NAME_RE = /^[a-z0-9][a-z0-9_-]{0,63}$/i;
7844
+ var NAME_RE = IDENTITY_ALIAS_PATTERN;
7749
7845
  var PROVIDER_ID_RE = /^[a-z0-9][a-z0-9-]{0,63}$/;
7750
7846
  function assertStoreName(kind, value) {
7751
- if (!NAME_RE.test(value)) throw new AgentServerStoreError("invalid_name", `${kind} must match ${NAME_RE.source}`);
7752
- return value;
7847
+ try {
7848
+ return assertIdentityAlias(value);
7849
+ } catch {
7850
+ throw new AgentServerStoreError("invalid_name", `${kind} must match ${NAME_RE.source}`);
7851
+ }
7753
7852
  }
7754
7853
  function assertProviderId(value) {
7755
7854
  if (!PROVIDER_ID_RE.test(value)) throw new AgentServerStoreError("invalid_name", `provider id must match ${PROVIDER_ID_RE.source}`);
@@ -7762,12 +7861,46 @@ var AgentServerStoreError = class extends Error {
7762
7861
  this.code = code;
7763
7862
  }
7764
7863
  };
7765
- /** `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
+ */
7766
7876
  function resolveAgentServerRoot(input) {
7767
7877
  const override = input.root?.trim();
7768
7878
  if (override) return override;
7769
- const xdg = input.xdgConfigHome?.trim();
7770
- 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;
7771
7904
  }
7772
7905
  function providerEnvName(providerId) {
7773
7906
  return `MOLTNET_PROVIDER_${assertProviderId(providerId).replaceAll("-", "_").toUpperCase()}_API_KEY`;
@@ -7805,14 +7938,14 @@ function writeJsonAtomic(path, value) {
7805
7938
  }
7806
7939
  var AgentServerStore = class {
7807
7940
  root;
7808
- agentsDir;
7941
+ identitiesDir;
7809
7942
  runsDir;
7810
7943
  secretsDir;
7811
7944
  /** Shared Pi credential dir; `auth.json` inside is pi-managed (lockfiled). */
7812
7945
  piDir;
7813
7946
  constructor(root) {
7814
7947
  this.root = root;
7815
- this.agentsDir = join(root, "agents");
7948
+ this.identitiesDir = join(root, "identities");
7816
7949
  this.runsDir = join(root, "runs");
7817
7950
  this.secretsDir = join(root, "secrets");
7818
7951
  this.piDir = join(root, "pi");
@@ -7821,18 +7954,85 @@ var AgentServerStore = class {
7821
7954
  return join(this.piDir, "auth.json");
7822
7955
  }
7823
7956
  /** Create the directory layout (0700) if missing. Idempotent. */
7824
- ensure() {
7957
+ ensure(options = {}) {
7958
+ this.adoptLegacyXdgRoot(options.legacyXdgConfigHome ?? "");
7825
7959
  for (const dir of [
7826
7960
  this.root,
7827
- this.agentsDir,
7961
+ this.identitiesDir,
7828
7962
  this.runsDir,
7829
7963
  this.secretsDir
7830
7964
  ]) mkdirSync(dir, {
7831
7965
  recursive: true,
7832
7966
  mode: 448
7833
7967
  });
7968
+ this.migrateLegacyAgentDocuments();
7834
7969
  return this;
7835
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
+ }
7836
8036
  get statePath() {
7837
8037
  return join(this.root, "agent-server.json");
7838
8038
  }
@@ -7861,16 +8061,58 @@ var AgentServerStore = class {
7861
8061
  writeJsonAtomic(this.statePath, state);
7862
8062
  }
7863
8063
  agentPath(name) {
7864
- 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);
7865
8090
  }
7866
8091
  readAgentConfig(alias) {
7867
8092
  return readJson(this.agentPath(alias));
7868
8093
  }
7869
8094
  writeAgentConfig(alias, config) {
8095
+ mkdirSync(this.identityDir(alias), {
8096
+ recursive: true,
8097
+ mode: 448
8098
+ });
7870
8099
  writeJsonAtomic(this.agentPath(alias), config);
8100
+ if (!this.readIdentitySelector()?.default_identity) this.writeIdentitySelector(alias);
7871
8101
  }
7872
8102
  removeAgentConfig(alias) {
7873
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 });
7874
8116
  }
7875
8117
  readActivation(alias) {
7876
8118
  return this.readAgentServerState().activations[assertStoreName("agent name", alias)] ?? null;
@@ -10380,12 +10622,9 @@ async function runAgentServer(argv) {
10380
10622
  return 1;
10381
10623
  }
10382
10624
  const allowedOrigins = parseAllowedOrigins(values["allowed-origins"] ?? (envConfig.allowedOrigins || DEFAULT_ALLOWED_ORIGINS));
10383
- const root = values.root ?? resolveAgentServerRoot({
10384
- root: envConfig.root,
10385
- xdgConfigHome: envConfig.xdgConfigHome
10386
- });
10625
+ const root = values.root ?? resolveAgentServerRoot({ root: envConfig.root });
10387
10626
  const defaultApiUrl = values["api-url"] ?? (envConfig.apiUrl || DEFAULT_API_URL);
10388
- const store = new AgentServerStore(root).ensure();
10627
+ const store = new AgentServerStore(root).ensure({ legacyXdgConfigHome: envConfig.xdgConfigHome });
10389
10628
  if (trustRequested) return runTrustCommand(commandArgs, root);
10390
10629
  const { logger, shutdown: shutdownLogger } = createRootLogger({
10391
10630
  name: "agent-daemon.server",
@@ -10679,7 +10918,13 @@ async function runSyncSessions(argv) {
10679
10918
  const state = parseState(values.state);
10680
10919
  const limit = parseLimit(values.limit);
10681
10920
  const agentRootDir = resolve(process.cwd(), values["agent-root"] ?? process.cwd());
10682
- 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
+ });
10683
10928
  await validateStartupBinding({
10684
10929
  agent: ctx.agent,
10685
10930
  teamId: values.team
@@ -10714,18 +10959,51 @@ function parseLimit(raw) {
10714
10959
  }
10715
10960
  //#endregion
10716
10961
  //#region src/lib/update.ts
10717
- 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";
10718
10981
  var UPDATE_CACHE_TTL_MS = 1440 * 60 * 1e3;
10719
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
+ }
10720
10998
  function detectDaemonInstallMethod(executable = process.argv[1] ?? "") {
10721
- const path = executable.replaceAll("\\", "/");
10999
+ const path = resolveDaemonExecutable(executable).replaceAll("\\", "/");
10722
11000
  if (path.includes("/node_modules/@themoltnet/agent-daemon/")) return "npm";
10723
11001
  if (path.includes("/.local/share/moltnet/") || path.includes("/opt/moltnet/")) return "bundle";
10724
11002
  return "direct";
10725
11003
  }
10726
11004
  function daemonUpdateCommand(method) {
10727
11005
  if (method === "npm") return "npm install -g @themoltnet/agent-daemon@latest";
10728
- return "curl -fsSL https://themolt.net/install/agent | sh";
11006
+ return "curl -fsSL https://themolt.net/install/agent | MOLTNET_AGENT_VERSION=latest sh";
10729
11007
  }
10730
11008
  async function checkDaemonUpdate(input) {
10731
11009
  const installMethod = detectDaemonInstallMethod(input.executable ?? process.argv[1] ?? "moltnet-agent");
@@ -10746,11 +11024,15 @@ async function checkDaemonUpdate(input) {
10746
11024
  return result;
10747
11025
  }
10748
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";
10749
11030
  try {
10750
- const response = await (input.fetchFn ?? fetch)(UPDATE_MANIFEST_URL, { signal: AbortSignal.timeout(5e3) });
10751
- if (!response.ok) throw new Error(`manifest returned HTTP ${response.status}`);
10752
- const latest = manifestVersion(await response.json());
10753
- 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`);
10754
11036
  await writeCache({
10755
11037
  checkedAt: now.toISOString(),
10756
11038
  latest
@@ -10766,13 +11048,31 @@ async function checkDaemonUpdate(input) {
10766
11048
  throw new Error(`could not check for MoltNet agent updates: ${error instanceof Error ? error.message : String(error)}`);
10767
11049
  }
10768
11050
  }
10769
- function manifestVersion(value) {
11051
+ function distTagVersion(value) {
10770
11052
  if (!value || typeof value !== "object") return void 0;
10771
- const agent = value.agent;
10772
- if (!agent || typeof agent !== "object") return void 0;
10773
- const version = agent.version;
11053
+ const version = value.version;
10774
11054
  return typeof version === "string" && validVersion(version) ? normalizeVersion(version) : void 0;
10775
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
+ }
10776
11076
  function normalizeVersion(value) {
10777
11077
  return value.trim().replace(/^v/, "");
10778
11078
  }
@@ -10812,7 +11112,7 @@ async function writeCache(cache) {
10812
11112
  }
10813
11113
  //#endregion
10814
11114
  //#region src/version.ts
10815
- var DAEMON_VERSION = "0.52.0";
11115
+ var DAEMON_VERSION = "0.54.0";
10816
11116
  //#endregion
10817
11117
  //#region src/cli.ts
10818
11118
  async function runAgentDaemonCli(options) {
@@ -10870,11 +11170,7 @@ async function runUpdate(argv) {
10870
11170
  }
10871
11171
  }
10872
11172
  async function runRuntime(argv) {
10873
- const config = loadAgentServerEnvConfig();
10874
- const registry = new RuntimeRegistry(resolveAgentServerRoot({
10875
- root: config.root,
10876
- xdgConfigHome: config.xdgConfigHome
10877
- }));
11173
+ const registry = new RuntimeRegistry(resolveAgentServerRoot({ root: loadAgentServerEnvConfig().root }));
10878
11174
  const [command, ...rest] = argv;
10879
11175
  if (command === "list") {
10880
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.52.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/pi-runtime": "0.14.1",
89
- "@themoltnet/agent-runtime": "0.45.2",
90
- "@themoltnet/os-keyring": "0.3.0",
91
- "@themoltnet/sdk": "0.140.0"
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",
@@ -99,15 +99,15 @@
99
99
  "vite-plugin-dts": "^4.5.4",
100
100
  "vitest": "^3.0.0",
101
101
  "@moltnet/bootstrap": "0.1.0",
102
- "@moltnet/agent-eval": "0.1.0",
103
102
  "@moltnet/crypto-service": "0.1.0",
104
- "@moltnet/execution-integrations": "0.1.0",
105
103
  "@moltnet/execution-plan": "0.1.0",
104
+ "@moltnet/execution-integrations": "0.1.0",
105
+ "@moltnet/runtime-profiles": "0.1.0",
106
+ "@moltnet/tasks": "0.1.0",
106
107
  "@moltnet/loopback-companion": "0.1.0",
108
+ "@moltnet/agent-eval": "0.1.0",
107
109
  "@moltnet/observability": "0.1.0",
108
- "@moltnet/runtime-profiles": "0.1.0",
109
- "@moltnet/models": "0.1.0",
110
- "@moltnet/tasks": "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
  },