@themoltnet/agent-daemon 0.53.0 → 0.55.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 +399 -169
  3. package/package.json +17 -9
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
16
  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";
17
+ import { AuthenticationError, IDENTITY_ALIAS_PATTERN, MoltNetError, agentKeyKey, assertIdentityAlias, assertTrustedConfigApiUrl, createExecutorAttestor, deriveMcpUrl, formatSecretReferenceString, getConfigDir, getIdentityDir, identitySeedKey, isCanonicalConfig, 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) {
@@ -1350,7 +1363,8 @@ Type.Object({ "x-moltnet-team-id": Type.Optional(Type.String({
1350
1363
  })) });
1351
1364
  Type.Object({
1352
1365
  kind: Type.Literal("agent"),
1353
- identityId: UuidSchema,
1366
+ agentId: UuidSchema,
1367
+ identityId: Type.Union([UuidSchema, Type.Null()]),
1354
1368
  fingerprint: FingerprintSchema,
1355
1369
  publicKey: PublicKeySchema
1356
1370
  }, {
@@ -1367,7 +1381,8 @@ Type.Object({
1367
1381
  });
1368
1382
  var principalUnionVariants = [Type.Object({
1369
1383
  kind: Type.Literal("agent"),
1370
- identityId: UuidSchema,
1384
+ agentId: UuidSchema,
1385
+ identityId: Type.Union([UuidSchema, Type.Null()]),
1371
1386
  fingerprint: FingerprintSchema,
1372
1387
  publicKey: PublicKeySchema
1373
1388
  }, { additionalProperties: false }), Type.Object({
@@ -3395,8 +3410,8 @@ var COMMON_OPTIONAL_FLAGS = `\
3395
3410
  --git-author <"Name <email>">
3396
3411
  Non-secret git identity projected into the
3397
3412
  guest for host-brokered commit signing. Default:
3398
- host git config (OAuth2) or
3399
- <identityId>+<agent>[bot]@users.noreply.github.com.
3413
+ host git config. Configless agent-key runs must
3414
+ provide this flag or MOLTNET_GIT_AUTHOR.
3400
3415
  Env: MOLTNET_GIT_AUTHOR.
3401
3416
  --lease-ttl-sec <n> Sliding liveness window. Silence longer than
3402
3417
  this ends the attempt with lease_expired.
@@ -3451,9 +3466,19 @@ Commands:
3451
3466
  Run \`agent-daemon <command> --help\` for command-specific flags.
3452
3467
 
3453
3468
  Prerequisites:
3454
- - agent-key mode: MOLTNET_AGENT_KEY (or MOLTNET_AGENT_KEY_REF) and
3469
+ - configless: MOLTNET_AGENT_KEY (or MOLTNET_AGENT_KEY_REF) and
3455
3470
  MOLTNET_PRIVATE_KEY (or MOLTNET_PRIVATE_KEY_REF); no agent files
3456
- - OAuth2 and sync-sessions: <agent-root>/.moltnet/<agent>/moltnet.json
3471
+ - config-based and sync-sessions: <agent-root>/.moltnet/<agent>/moltnet.json
3472
+ carrying agent_key_ref (OAuth2 client credentials are not accepted)
3473
+
3474
+ No key yet? Mint one with the CLI (--store writes agent_key_ref into
3475
+ moltnet.json and keeps the secret in a provider):
3476
+
3477
+ moltnet teams list # find the team id
3478
+ moltnet agents keys create --team-id <team-uuid> \\
3479
+ --name <agent>-daemon --store
3480
+
3481
+ https://docs.themolt.net/operate/agent-keys#run-the-daemon-with-an-agent-key
3457
3482
  - --profile — remote runtime profile supplies provider/model/sandbox
3458
3483
  policy and CWD is used as the VM mountPath.
3459
3484
 
@@ -3603,8 +3628,17 @@ remove that exact CA. Linux continues to use the Chromium PNA HTTP path.
3603
3628
  //#region src/lib/identity-pin.ts
3604
3629
  /** Compare every pinned field without choosing a caller-specific error type. */
3605
3630
  function assessIdentityPin(current, expected) {
3631
+ for (const [field, label] of [["publicKey", "public key"], ["fingerprint", "fingerprint"]]) if (!current[field] || current[field] !== expected[field]) return {
3632
+ ok: false,
3633
+ field,
3634
+ label
3635
+ };
3636
+ return { ok: true };
3637
+ }
3638
+ function assessAgentStartupPin(current, expected) {
3606
3639
  for (const [field, label] of [
3607
- ["identityId", "identity id"],
3640
+ ["subjectId", "subject id"],
3641
+ ["subjectType", "subject type"],
3608
3642
  ["publicKey", "public key"],
3609
3643
  ["fingerprint", "fingerprint"]
3610
3644
  ]) if (!current[field] || current[field] !== expected[field]) return {
@@ -3617,18 +3651,30 @@ function assessIdentityPin(current, expected) {
3617
3651
  //#endregion
3618
3652
  //#region src/lib/agent-context.ts
3619
3653
  /**
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.
3654
+ * Where an operator goes after the daemon refuses to start. The published site
3655
+ * rather than a GitHub blob: it renders, and it tracks the deployed docs
3656
+ * instead of whatever `main` happens to say. Sidebar entry lives in
3657
+ * `docs/.vitepress/config.ts`.
3658
+ */
3659
+ var AGENT_KEYS_DOC_URL = "https://docs.themolt.net/operate/agent-keys#run-the-daemon-with-an-agent-key";
3660
+ /** Scopes a daemon key must carry; mirrors `DAEMON_REQUIRED_SCOPES`. */
3661
+ var DAEMON_KEY_SCOPES = AGENT_CREDENTIAL_SCOPES;
3662
+ /**
3663
+ * Report where `connect()` will find the key, without ever reading the secret
3664
+ * value into anything logged. A non-blank `MOLTNET_AGENT_KEY` or
3665
+ * `MOLTNET_AGENT_KEY_REF` means configless; otherwise the key comes from
3666
+ * `moltnet.json`. This mirrors the SDK precedence, where an environment key
3667
+ * wins over the config file.
3668
+ *
3669
+ * It reports the *source*, not what `connect()` would pick on its own: the
3670
+ * config path resolves the key and passes it explicitly, because ambient
3671
+ * resolution ranks environment OAuth2 credentials above a configured
3672
+ * `agent_key_ref` (see `resolveAgentContext`).
3627
3673
  *
3628
3674
  * Pure: `env` is passed in (the config module owns the `process.env` read).
3629
3675
  */
3630
- function detectAuthMode(env) {
3631
- return env.MOLTNET_AGENT_KEY?.trim() || env.MOLTNET_AGENT_KEY_REF?.trim() ? "agent-key" : "oauth2";
3676
+ function detectCredentialSource(env) {
3677
+ return env.MOLTNET_AGENT_KEY?.trim() || env.MOLTNET_AGENT_KEY_REF?.trim() ? "environment" : "config";
3632
3678
  }
3633
3679
  /**
3634
3680
  * Pure check: may the identity described by `whoami` operate the daemon as
@@ -3677,47 +3723,118 @@ async function validateStartupBinding(options) {
3677
3723
  }
3678
3724
  const assessment = assessStartupBinding(whoami, options.teamId);
3679
3725
  if (!assessment.ok) throw new Error(`Daemon startup validation failed: ${assessment.reason}`);
3680
- const expected = options.expectedIdentity;
3681
- if (expected && !assessIdentityPin(whoami, expected).ok) throw new Error("Daemon startup validation failed: authenticated identity does not match the Agent Server activation.");
3726
+ const expected = options.expectedAgent;
3727
+ if (expected && !assessAgentStartupPin(whoami, expected).ok) throw new Error("Daemon startup validation failed: authenticated agent does not match the Agent Server activation.");
3682
3728
  return whoami;
3683
3729
  }
3684
3730
  /**
3685
3731
  * Resolve the agent's MoltNet credentials directory and connect via SDK.
3686
3732
  *
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.
3733
+ * The daemon selects the same central identity directory as the CLI. It never
3734
+ * inspects a repository, Git state, or legacy agent bundle for credentials.
3690
3735
  */
3691
3736
  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"
3737
+ assertIdentityAlias(agentName);
3738
+ const { agentDir, agentRootDir } = resolveIdentityLocation(agentName, options.agentRootDir, { requireConfig: options.credentialSource !== "environment" });
3739
+ if (options.credentialSource === "environment") return {
3740
+ agentDir,
3741
+ agentRootDir,
3742
+ agent: await connect({ secretProviders: createNodeSecretProviderRegistry() }),
3743
+ credentialSource: "environment"
3744
+ };
3745
+ const config = await readConfig(agentDir);
3746
+ if (!config?.agent_key_ref) throw new Error(agentKeyRequiredMessage(agentDir, agentName));
3747
+ const secretProviders = createNodeSecretProviderRegistry();
3748
+ const agentKey = await resolveAgentKey(config, secretProviders);
3749
+ if (!agentKey) throw new Error(agentKeyRequiredMessage(agentDir, agentName));
3750
+ return {
3751
+ agentDir,
3752
+ agentRootDir,
3753
+ agent: await connect({
3754
+ configDir: agentDir,
3755
+ secretProviders,
3756
+ agentKey,
3757
+ apiUrl: resolveConfigApiUrl(config, options.envApiUrl)
3758
+ }),
3759
+ credentialSource: "config"
3760
+ };
3761
+ }
3762
+ /**
3763
+ * The daemon runs on an agent key only. A `moltnet.json` carrying OAuth2
3764
+ * client credentials but no `agent_key_ref` is the pre-#2160 shape, and
3765
+ * `connect()` would happily authenticate it — so this has to be refused here
3766
+ * rather than left to surface as an over-scoped token later.
3767
+ */
3768
+ function agentKeyRequiredMessage(agentDir, agentName) {
3769
+ return [
3770
+ `${join(agentDir, "moltnet.json")} has no "agent_key_ref". The daemon`,
3771
+ `requires a team-bound agent key; OAuth2 client_credentials is no longer`,
3772
+ `accepted.`,
3773
+ ``,
3774
+ `Mint one (--agent-id defaults to the agent you authenticate as):`,
3775
+ ``,
3776
+ ` moltnet teams list # find the team id`,
3777
+ ` moltnet agents keys create \\`,
3778
+ ` --team-id <team-uuid> \\`,
3779
+ ` --name ${agentName}-daemon \\`,
3780
+ ` --scopes ${DAEMON_KEY_SCOPES.join(",")} \\`,
3781
+ ` --store`,
3782
+ ``,
3783
+ `--store writes "agent_key_ref" into moltnet.json and keeps the secret in`,
3784
+ `a provider, so the key itself never lands in the file. Omit --scopes to`,
3785
+ `get the same daemon minimum by default.`,
3786
+ ``,
3787
+ `To run configless instead, set MOLTNET_AGENT_KEY (or`,
3788
+ `MOLTNET_AGENT_KEY_REF) and skip the config entirely.`,
3789
+ ``,
3790
+ `Full guide: ${AGENT_KEYS_DOC_URL}`
3791
+ ].join("\n");
3792
+ }
3793
+ /**
3794
+ * The central identity directory, unless an explicit `--agent-root` names a
3795
+ * legacy bundle that actually exists.
3796
+ *
3797
+ * The flag is documented as "Directory that owns .moltnet/<agent>" and is still
3798
+ * accepted by `once`, `poll` and `sync-sessions`. Ignoring it silently sent
3799
+ * every caller that passes one — sandboxed runs, the e2e harness — to a
3800
+ * central store they never populated, and failed with a bare "No credentials
3801
+ * found".
3802
+ */
3803
+ /**
3804
+ * `agentDir` is where credentials live; `agentRootDir` is the directory that
3805
+ * OWNS it and is mounted into the sandbox. They differ for a legacy bundle
3806
+ * (`<root>/.moltnet/<agent>` inside `<root>`) and coincide for a central
3807
+ * identity, which owns nothing above itself. Collapsing them mounted the
3808
+ * credentials directory itself into the guest.
3809
+ */
3810
+ function resolveIdentityLocation(agentName, explicitRootDir, { requireConfig }) {
3811
+ const root = explicitRootDir?.trim();
3812
+ if (root) {
3813
+ const bundle = join(root, ".moltnet", agentName);
3814
+ if (!requireConfig || existsSync(join(bundle, "moltnet.json"))) return {
3815
+ agentDir: bundle,
3816
+ agentRootDir: root
3717
3817
  };
3718
3818
  }
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.`);
3819
+ const central = getIdentityDir(agentName);
3820
+ return {
3821
+ agentDir: central,
3822
+ agentRootDir: central
3823
+ };
3824
+ }
3825
+ /**
3826
+ * Pick the API URL for an explicitly-keyed connect, preserving the checks
3827
+ * ambient config resolution would have applied. An explicit `apiUrl` skips
3828
+ * ambient's own normalisation, so a URL taken from `moltnet.json` still has to
3829
+ * clear the config-trust and transport checks before it is used.
3830
+ */
3831
+ function resolveConfigApiUrl(config, envApiUrl) {
3832
+ if (envApiUrl?.trim()) return void 0;
3833
+ const fromConfig = config.endpoints?.api?.trim();
3834
+ if (!fromConfig) return void 0;
3835
+ assertTrustedConfigApiUrl(fromConfig);
3836
+ requireSecureCredentialApiUrl(fromConfig);
3837
+ return fromConfig;
3721
3838
  }
3722
3839
  function isTransientWhoamiError(error) {
3723
3840
  if (error instanceof TypeError) return true;
@@ -3725,26 +3842,6 @@ function isTransientWhoamiError(error) {
3725
3842
  const statusCode = error.statusCode;
3726
3843
  return typeof statusCode === "number" && (statusCode === 408 || statusCode === 429 || statusCode >= 500);
3727
3844
  }
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
3845
  //#endregion
3749
3846
  //#region src/config.ts
3750
3847
  /**
@@ -3757,14 +3854,15 @@ function resolveCredentialRoots(agentRootDir) {
3757
3854
  function loadConfig() {
3758
3855
  assertSingleCredentialForm("MOLTNET_AGENT_KEY", "MOLTNET_AGENT_KEY_REF");
3759
3856
  assertSingleCredentialForm("MOLTNET_PRIVATE_KEY", "MOLTNET_PRIVATE_KEY_REF");
3760
- const expectedIdentity = readExpectedIdentity();
3857
+ const expectedAgent = readExpectedAgent();
3761
3858
  return {
3762
3859
  otelEndpoint: process.env["MOLTNET_OTEL_ENDPOINT"] ?? "",
3763
3860
  logLevel: process.env["LOG_LEVEL"] ?? "",
3764
3861
  profilePrerequisiteEnv: process.env,
3765
3862
  profilePrerequisitePath: process.env.PATH ?? "",
3766
3863
  piCodingAgentDir: process.env["PI_CODING_AGENT_DIR"] ?? "",
3767
- authMode: detectAuthMode(process.env),
3864
+ credentialSource: detectCredentialSource(process.env),
3865
+ apiUrl: process.env["MOLTNET_API_URL"] ?? "",
3768
3866
  signingPrivateKey: process.env["MOLTNET_PRIVATE_KEY"] ?? "",
3769
3867
  signingPrivateKeyRef: process.env["MOLTNET_PRIVATE_KEY_REF"] ?? "",
3770
3868
  gitAuthor: process.env["MOLTNET_GIT_AUTHOR"] ?? "",
@@ -3772,22 +3870,27 @@ function loadConfig() {
3772
3870
  credentialBindings: process.env["MOLTNET_CREDENTIAL_BINDINGS"] ?? "",
3773
3871
  credentialEnforcement: process.env["MOLTNET_CREDENTIAL_ENFORCEMENT"] ?? "",
3774
3872
  traceIdlePolling: readBoolean("MOLTNET_TRACE_IDLE_POLLING", process.env["MOLTNET_TRACE_IDLE_POLLING"]),
3775
- ...expectedIdentity ? { expectedIdentity } : {}
3873
+ ...expectedAgent ? { expectedAgent } : {}
3776
3874
  };
3777
3875
  }
3778
- function readExpectedIdentity() {
3779
- const identityId = process.env["MOLTNET_EXPECTED_IDENTITY_ID"]?.trim() ?? "";
3876
+ function readExpectedAgent() {
3877
+ if (process.env["MOLTNET_EXPECTED_IDENTITY_ID"]?.trim()) throw new Error("MOLTNET_EXPECTED_IDENTITY_ID is no longer supported; set the complete MOLTNET_EXPECTED_SUBJECT_ID, MOLTNET_EXPECTED_SUBJECT_TYPE=agent, MOLTNET_EXPECTED_PUBLIC_KEY, and MOLTNET_EXPECTED_FINGERPRINT pin");
3878
+ const subjectId = process.env["MOLTNET_EXPECTED_SUBJECT_ID"]?.trim() ?? "";
3879
+ const subjectType = process.env["MOLTNET_EXPECTED_SUBJECT_TYPE"]?.trim() ?? "";
3780
3880
  const publicKey = process.env["MOLTNET_EXPECTED_PUBLIC_KEY"]?.trim() ?? "";
3781
3881
  const fingerprint = process.env["MOLTNET_EXPECTED_FINGERPRINT"]?.trim() ?? "";
3782
3882
  const present = [
3783
- identityId,
3883
+ subjectId,
3884
+ subjectType,
3784
3885
  publicKey,
3785
3886
  fingerprint
3786
3887
  ].filter(Boolean).length;
3787
3888
  if (present === 0) return void 0;
3788
- if (present !== 3) throw new Error("MOLTNET_EXPECTED_IDENTITY_ID, MOLTNET_EXPECTED_PUBLIC_KEY, and MOLTNET_EXPECTED_FINGERPRINT must be set together");
3889
+ if (present !== 4) throw new Error("MOLTNET_EXPECTED_SUBJECT_ID, MOLTNET_EXPECTED_SUBJECT_TYPE, MOLTNET_EXPECTED_PUBLIC_KEY, and MOLTNET_EXPECTED_FINGERPRINT must be set together");
3890
+ if (subjectType !== "agent") throw new Error("MOLTNET_EXPECTED_SUBJECT_TYPE must be agent");
3789
3891
  return {
3790
- identityId,
3892
+ subjectId,
3893
+ subjectType,
3791
3894
  publicKey,
3792
3895
  fingerprint
3793
3896
  };
@@ -3809,7 +3912,6 @@ function loadAgentServerEnvConfig() {
3809
3912
  port: process.env["MOLTNET_AGENT_SERVER_PORT"] ?? "",
3810
3913
  allowedOrigins: process.env["MOLTNET_AGENT_SERVER_ALLOWED_ORIGINS"] ?? "",
3811
3914
  root: process.env["MOLTNET_AGENT_SERVER_ROOT"] ?? "",
3812
- xdgConfigHome: process.env["XDG_CONFIG_HOME"] ?? "",
3813
3915
  apiUrl: process.env["MOLTNET_API_URL"] ?? "",
3814
3916
  logLevel: process.env["LOG_LEVEL"] ?? ""
3815
3917
  };
@@ -3839,13 +3941,13 @@ async function abortActiveAttemptOnSignal(opts) {
3839
3941
  //#region src/lib/agent-identity.ts
3840
3942
  /**
3841
3943
  * 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.
3944
+ * non-secret input and is consulted only on config-based hosts, which already
3945
+ * read that configuration for the signing seed; configless hosts never touch a
3946
+ * config directory.
3845
3947
  */
3846
3948
  async function resolveDaemonAgentIdentity(input) {
3847
3949
  let hostGit;
3848
- if (input.gitAuthor === void 0 && input.authMode === "oauth2") {
3950
+ if (input.gitAuthor === void 0 && input.credentialSource === "config") {
3849
3951
  const config = await readConfig(input.agentDir);
3850
3952
  hostGit = config?.git ? {
3851
3953
  name: config.git.name,
@@ -4390,26 +4492,26 @@ function recoverScratchWorkspacePath(producer, stateDirs) {
4390
4492
  //#region src/lib/executor-attestation.ts
4391
4493
  var DAEMON_REQUIRED_SCOPES = AGENT_CREDENTIAL_SCOPES;
4392
4494
  async function resolveExecutorSigningPrivateKey(input) {
4393
- if (input.authMode === "agent-key") {
4495
+ if (input.credentialSource === "environment") {
4394
4496
  const privateKey = input.configuredPrivateKey.trim();
4395
4497
  if (privateKey) return privateKey;
4396
4498
  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.");
4499
+ 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
4500
  let resolved;
4399
4501
  try {
4400
4502
  resolved = await resolveEnvSecretReference(reference, createNodeSecretProviderRegistry());
4401
4503
  } catch (cause) {
4402
- throw new Error(`Agent-key daemon startup could not resolve MOLTNET_PRIVATE_KEY_REF: ${cause.message}`, { cause });
4504
+ throw new Error(`Configless daemon startup could not resolve MOLTNET_PRIVATE_KEY_REF: ${cause.message}`, { cause });
4403
4505
  }
4404
4506
  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
4507
  return resolved;
4406
4508
  }
4407
4509
  const config = await readConfig(input.agentDir);
4408
- if (!config) throw new Error(`OAuth2 daemon startup requires ${input.agentDir}/moltnet.json.`);
4510
+ if (!config) throw new Error(`Config-based daemon startup requires ${input.agentDir}/moltnet.json.`);
4409
4511
  try {
4410
4512
  return await resolveIdentitySeed(config, createNodeSecretProviderRegistry());
4411
4513
  } 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 });
4514
+ 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
4515
  }
4414
4516
  }
4415
4517
  function validateDaemonScopes(whoami) {
@@ -5105,7 +5207,7 @@ async function logDaemonStartupFailure(input) {
5105
5207
  event: "agent-daemon.startup_failed",
5106
5208
  gate: input.gate,
5107
5209
  agent: input.agent,
5108
- authMode: input.authMode
5210
+ credentialSource: input.credentialSource
5109
5211
  }, "Agent daemon startup validation failed");
5110
5212
  await shutdown();
5111
5213
  }
@@ -5924,23 +6026,24 @@ async function runPolling(opts) {
5924
6026
  bindings: cfg.credentialBindings
5925
6027
  };
5926
6028
  const runtimeCredentialConfig = resolveCredentialEnforcement(cfg.credentialEnforcement, credentialSources) === "off" ? null : loadRuntimeCredentialConfig(credentialSources);
5927
- const agentRootDir = resolve(process.cwd(), values["agent-root"] ?? process.cwd());
6029
+ const explicitAgentRootDir = values["agent-root"] ? resolve(process.cwd(), values["agent-root"]) : void 0;
5928
6030
  const { ctx, signingPrivateKey, startupWhoami, agentIdentity, hostCapabilitySigner } = await (async () => {
5929
6031
  let gate = "resolve_agent_context";
5930
6032
  try {
5931
6033
  const resolvedContext = await resolveAgentContext(baseCommon.agent, {
5932
- agentRootDir,
5933
- authMode: cfg.authMode
6034
+ agentRootDir: explicitAgentRootDir,
6035
+ credentialSource: cfg.credentialSource,
6036
+ envApiUrl: cfg.apiUrl
5934
6037
  });
5935
6038
  gate = "authenticate_and_bind";
5936
6039
  const whoami = await validateStartupBinding({
5937
6040
  agent: resolvedContext.agent,
5938
6041
  teamId,
5939
- expectedIdentity: cfg.expectedIdentity
6042
+ expectedAgent: cfg.expectedAgent
5940
6043
  });
5941
6044
  gate = "resolve_signing_material";
5942
6045
  const privateKey = await resolveExecutorSigningPrivateKey({
5943
- authMode: cfg.authMode,
6046
+ credentialSource: cfg.credentialSource,
5944
6047
  agentDir: resolvedContext.agentDir,
5945
6048
  configuredPrivateKey: cfg.signingPrivateKey,
5946
6049
  configuredPrivateKeyRef: cfg.signingPrivateKeyRef
@@ -5956,7 +6059,7 @@ async function runPolling(opts) {
5956
6059
  const agentIdentity = await resolveDaemonAgentIdentity({
5957
6060
  agentName: baseCommon.agent,
5958
6061
  whoami,
5959
- authMode: cfg.authMode,
6062
+ credentialSource: cfg.credentialSource,
5960
6063
  agentDir: resolvedContext.agentDir,
5961
6064
  gitAuthor: values["git-author"] ?? (cfg.gitAuthor || void 0)
5962
6065
  });
@@ -5977,7 +6080,7 @@ async function runPolling(opts) {
5977
6080
  level: cfg.logLevel || (baseCommon.debug ? "debug" : "info"),
5978
6081
  gate,
5979
6082
  agent: baseCommon.agent,
5980
- authMode: cfg.authMode,
6083
+ credentialSource: cfg.credentialSource,
5981
6084
  error
5982
6085
  });
5983
6086
  throw error;
@@ -6081,7 +6184,7 @@ async function runPolling(opts) {
6081
6184
  resourceAttributes: {
6082
6185
  "moltnet.team.id": teamId,
6083
6186
  "moltnet.agent.name": baseCommon.agent,
6084
- "moltnet.auth.mode": ctx.authMechanism,
6187
+ "moltnet.credential.source": ctx.credentialSource,
6085
6188
  "moltnet.runtime_profile.count": String(profiles.length),
6086
6189
  "moltnet.runtime_profile.ids": profiles.map((p) => p.id).join(",")
6087
6190
  }
@@ -6129,7 +6232,7 @@ async function runPolling(opts) {
6129
6232
  }
6130
6233
  });
6131
6234
  rootLogger.info({
6132
- authMode: cfg.authMode,
6235
+ credentialSource: cfg.credentialSource,
6133
6236
  subjectType: startupWhoami.subjectType,
6134
6237
  bindingScope: startupWhoami.credentialBinding?.bindingScope ?? null,
6135
6238
  credentialKeyId: startupWhoami.credentialBinding?.keyId ?? null,
@@ -6581,23 +6684,24 @@ async function runOnce(argv, runtimeAdapter = defaultPiDaemonAdapter) {
6581
6684
  };
6582
6685
  const runtimeCredentialConfig = resolveCredentialEnforcement(cfg.credentialEnforcement, credentialSources) === "off" ? null : loadRuntimeCredentialConfig(credentialSources);
6583
6686
  const initialOpts = opts;
6584
- const agentRootDir = resolve(process.cwd(), values["agent-root"] ?? process.cwd());
6687
+ const explicitAgentRootDir = values["agent-root"] ? resolve(process.cwd(), values["agent-root"]) : void 0;
6585
6688
  const { ctx, signingPrivateKey, agentIdentity, hostCapabilitySigner } = await (async () => {
6586
6689
  let gate = "resolve_agent_context";
6587
6690
  try {
6588
6691
  const resolvedContext = await resolveAgentContext(initialOpts.agent, {
6589
- agentRootDir,
6590
- authMode: cfg.authMode
6692
+ agentRootDir: explicitAgentRootDir,
6693
+ credentialSource: cfg.credentialSource,
6694
+ envApiUrl: cfg.apiUrl
6591
6695
  });
6592
6696
  gate = "authenticate_and_bind";
6593
6697
  const whoami = await validateStartupBinding({
6594
6698
  agent: resolvedContext.agent,
6595
6699
  teamId: values.team,
6596
- expectedIdentity: cfg.expectedIdentity
6700
+ expectedAgent: cfg.expectedAgent
6597
6701
  });
6598
6702
  gate = "resolve_signing_material";
6599
6703
  const privateKey = await resolveExecutorSigningPrivateKey({
6600
- authMode: cfg.authMode,
6704
+ credentialSource: cfg.credentialSource,
6601
6705
  agentDir: resolvedContext.agentDir,
6602
6706
  configuredPrivateKey: cfg.signingPrivateKey,
6603
6707
  configuredPrivateKeyRef: cfg.signingPrivateKeyRef
@@ -6613,7 +6717,7 @@ async function runOnce(argv, runtimeAdapter = defaultPiDaemonAdapter) {
6613
6717
  const agentIdentity = await resolveDaemonAgentIdentity({
6614
6718
  agentName: initialOpts.agent,
6615
6719
  whoami,
6616
- authMode: cfg.authMode,
6720
+ credentialSource: cfg.credentialSource,
6617
6721
  agentDir: resolvedContext.agentDir,
6618
6722
  gitAuthor: values["git-author"] ?? (cfg.gitAuthor || void 0)
6619
6723
  });
@@ -6633,7 +6737,7 @@ async function runOnce(argv, runtimeAdapter = defaultPiDaemonAdapter) {
6633
6737
  level: cfg.logLevel || (initialOpts.debug ? "debug" : "info"),
6634
6738
  gate,
6635
6739
  agent: initialOpts.agent,
6636
- authMode: cfg.authMode,
6740
+ credentialSource: cfg.credentialSource,
6637
6741
  error
6638
6742
  });
6639
6743
  throw error;
@@ -6703,7 +6807,7 @@ async function runOnce(argv, runtimeAdapter = defaultPiDaemonAdapter) {
6703
6807
  resourceAttributes: {
6704
6808
  "moltnet.task.id": taskId,
6705
6809
  "moltnet.agent.name": opts.agent,
6706
- "moltnet.auth.mode": ctx.authMechanism,
6810
+ "moltnet.credential.source": ctx.credentialSource,
6707
6811
  "moltnet.llm.provider": profile.provider,
6708
6812
  "moltnet.llm.model": profile.model,
6709
6813
  ...profile.thinkingLevel ? { "moltnet.llm.thinking_level": profile.thinkingLevel } : {},
@@ -7752,11 +7856,14 @@ function snapshot(login) {
7752
7856
  ...waitingForAuthorization ? { waitingForAuthorization } : {}
7753
7857
  };
7754
7858
  }
7755
- var NAME_RE = /^[a-z0-9][a-z0-9_-]{0,63}$/i;
7859
+ var NAME_RE = IDENTITY_ALIAS_PATTERN;
7756
7860
  var PROVIDER_ID_RE = /^[a-z0-9][a-z0-9-]{0,63}$/;
7757
7861
  function assertStoreName(kind, value) {
7758
- if (!NAME_RE.test(value)) throw new AgentServerStoreError("invalid_name", `${kind} must match ${NAME_RE.source}`);
7759
- return value;
7862
+ try {
7863
+ return assertIdentityAlias(value);
7864
+ } catch {
7865
+ throw new AgentServerStoreError("invalid_name", `${kind} must match ${NAME_RE.source}`);
7866
+ }
7760
7867
  }
7761
7868
  function assertProviderId(value) {
7762
7869
  if (!PROVIDER_ID_RE.test(value)) throw new AgentServerStoreError("invalid_name", `provider id must match ${PROVIDER_ID_RE.source}`);
@@ -7769,12 +7876,20 @@ var AgentServerStoreError = class extends Error {
7769
7876
  this.code = code;
7770
7877
  }
7771
7878
  };
7772
- /** `MOLTNET_AGENT_SERVER_ROOT` override, else `$XDG_CONFIG_HOME/moltnet`, else `~/.config/moltnet`. */
7879
+ /**
7880
+ * `MOLTNET_AGENT_SERVER_ROOT` override, else `~/.config/moltnet`.
7881
+ *
7882
+ * Deliberately does NOT consult `XDG_CONFIG_HOME`. The Go CLI's GetConfigDir
7883
+ * and @moltnet/agent-config's getConfigDir both resolve `~/.config/moltnet`,
7884
+ * so honouring XDG here gave one application two config roots: on a machine
7885
+ * with the variable set, the daemon wrote identities the CLI and SDK could not
7886
+ * read. `MOLTNET_AGENT_SERVER_ROOT` remains the explicit escape hatch for a
7887
+ * genuinely custom location.
7888
+ */
7773
7889
  function resolveAgentServerRoot(input) {
7774
7890
  const override = input.root?.trim();
7775
7891
  if (override) return override;
7776
- const xdg = input.xdgConfigHome?.trim();
7777
- return join(xdg || join(homedir(), ".config"), "moltnet");
7892
+ return getConfigDir();
7778
7893
  }
7779
7894
  function providerEnvName(providerId) {
7780
7895
  return `MOLTNET_PROVIDER_${assertProviderId(providerId).replaceAll("-", "_").toUpperCase()}_API_KEY`;
@@ -7812,14 +7927,14 @@ function writeJsonAtomic(path, value) {
7812
7927
  }
7813
7928
  var AgentServerStore = class {
7814
7929
  root;
7815
- agentsDir;
7930
+ identitiesDir;
7816
7931
  runsDir;
7817
7932
  secretsDir;
7818
7933
  /** Shared Pi credential dir; `auth.json` inside is pi-managed (lockfiled). */
7819
7934
  piDir;
7820
7935
  constructor(root) {
7821
7936
  this.root = root;
7822
- this.agentsDir = join(root, "agents");
7937
+ this.identitiesDir = join(root, "identities");
7823
7938
  this.runsDir = join(root, "runs");
7824
7939
  this.secretsDir = join(root, "secrets");
7825
7940
  this.piDir = join(root, "pi");
@@ -7831,7 +7946,7 @@ var AgentServerStore = class {
7831
7946
  ensure() {
7832
7947
  for (const dir of [
7833
7948
  this.root,
7834
- this.agentsDir,
7949
+ this.identitiesDir,
7835
7950
  this.runsDir,
7836
7951
  this.secretsDir
7837
7952
  ]) mkdirSync(dir, {
@@ -7846,20 +7961,20 @@ var AgentServerStore = class {
7846
7961
  readAgentServerState() {
7847
7962
  const state = readJson(this.statePath);
7848
7963
  if (!state) return {
7849
- version: 1,
7964
+ version: 2,
7850
7965
  pendingRegistrations: {},
7851
7966
  activations: {}
7852
7967
  };
7853
- if (!isRecord$1(state) || state.version !== 1) throw new AgentServerStoreError("invalid_state", `agent-server.json version ${String(isRecord$1(state) ? state.version : void 0)} is not supported`);
7854
- if ("pairedOrigins" in state) throw new AgentServerStoreError("invalid_state", "agent-server.json uses the obsolete pairing format; clear the unreleased agent server store and reconfigure it");
7855
- if (!isRecord$1(state.pendingRegistrations) || !isRecord$1(state.activations)) throw new AgentServerStoreError("invalid_state", "agent-server.json is missing the version 1 activation map; clear the unreleased agent server store and reconfigure it");
7968
+ if (!isRecord$1(state) || state.version !== 2) throw new AgentServerStoreError("invalid_state", `agent-server.json version ${String(isRecord$1(state) ? state.version : void 0)} is not supported; move agent-server.json aside, run \`moltnet config migrate\`, then add or attach the agents again`);
7969
+ if ("pairedOrigins" in state) throw new AgentServerStoreError("invalid_state", "agent-server.json uses the obsolete pairing format; move agent-server.json aside and configure the agent server again");
7970
+ if (!isRecord$1(state.pendingRegistrations) || !isRecord$1(state.activations)) throw new AgentServerStoreError("invalid_state", "agent-server.json is missing the version 2 activation map; move agent-server.json aside, run `moltnet config migrate`, then add or attach the agents again");
7856
7971
  for (const [alias, activation] of Object.entries(state.activations)) validateActivation(alias, activation);
7857
7972
  for (const [alias, registration] of Object.entries(state.pendingRegistrations)) {
7858
7973
  assertStoreName("agent name", alias);
7859
7974
  if (!isRecord$1(registration) || typeof registration.apiUrl !== "string" || registration.apiUrl.length === 0 || typeof registration.createdAt !== "string" || registration.createdAt.length === 0) throw new AgentServerStoreError("invalid_state", `pending registration "${alias}" is not valid`);
7860
7975
  }
7861
7976
  return {
7862
- version: 1,
7977
+ version: 2,
7863
7978
  pendingRegistrations: state.pendingRegistrations,
7864
7979
  activations: state.activations
7865
7980
  };
@@ -7868,16 +7983,58 @@ var AgentServerStore = class {
7868
7983
  writeJsonAtomic(this.statePath, state);
7869
7984
  }
7870
7985
  agentPath(name) {
7871
- return storeChildPath(this.agentsDir, "agent name", name, ".json");
7986
+ return join(storeChildPath(this.identitiesDir, "identity alias", name), "moltnet.json");
7987
+ }
7988
+ /** Central identity directory, shared with the Go CLI layout. */
7989
+ identityDir(name) {
7990
+ return storeChildPath(this.identitiesDir, "identity alias", name);
7991
+ }
7992
+ get identitySelectorPath() {
7993
+ return join(this.root, "identity-selector.json");
7994
+ }
7995
+ readIdentitySelector() {
7996
+ const selector = readJson(this.identitySelectorPath);
7997
+ if (!selector) return null;
7998
+ 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");
7999
+ if (selector.default_identity) assertStoreName("identity alias", selector.default_identity);
8000
+ return selector;
8001
+ }
8002
+ writeIdentitySelector(alias) {
8003
+ writeJsonAtomic(this.identitySelectorPath, {
8004
+ version: 1,
8005
+ default_identity: assertStoreName("identity alias", alias)
8006
+ });
8007
+ }
8008
+ resolveIdentityAlias(explicit, active) {
8009
+ const alias = explicit?.trim() || active?.trim() || this.readIdentitySelector()?.default_identity;
8010
+ if (!alias) throw new AgentServerStoreError("not_found", "no active identity selected");
8011
+ return assertStoreName("identity alias", alias);
7872
8012
  }
7873
8013
  readAgentConfig(alias) {
7874
8014
  return readJson(this.agentPath(alias));
7875
8015
  }
7876
8016
  writeAgentConfig(alias, config) {
8017
+ mkdirSync(this.identityDir(alias), {
8018
+ recursive: true,
8019
+ mode: 448
8020
+ });
7877
8021
  writeJsonAtomic(this.agentPath(alias), config);
8022
+ if (!this.readIdentitySelector()?.default_identity) this.writeIdentitySelector(alias);
7878
8023
  }
7879
8024
  removeAgentConfig(alias) {
7880
8025
  rmSync(this.agentPath(alias), { force: true });
8026
+ this.clearIdentitySelectorIfDefault(alias);
8027
+ }
8028
+ /** Clears the persisted default when it names `alias`. */
8029
+ clearIdentitySelectorIfDefault(alias) {
8030
+ let selector = null;
8031
+ try {
8032
+ selector = this.readIdentitySelector();
8033
+ } catch {
8034
+ return;
8035
+ }
8036
+ if (!selector || selector.default_identity !== alias) return;
8037
+ writeJsonAtomic(this.identitySelectorPath, { version: 1 });
7881
8038
  }
7882
8039
  readActivation(alias) {
7883
8040
  return this.readAgentServerState().activations[assertStoreName("agent name", alias)] ?? null;
@@ -8046,13 +8203,13 @@ function isStrictDescendant(root, candidate) {
8046
8203
  }
8047
8204
  function validateActivation(alias, value) {
8048
8205
  const invalid = () => {
8049
- throw new AgentServerStoreError("invalid_state", `activation "${alias}" is not a valid version 1 activation`);
8206
+ throw new AgentServerStoreError("invalid_state", `activation "${alias}" is not a valid version 2 activation`);
8050
8207
  };
8051
8208
  if (!isRecord$1(value)) invalid();
8052
8209
  const activation = value;
8053
8210
  if (activation.alias !== alias) invalid();
8054
8211
  if (![
8055
- "identityId",
8212
+ "subjectId",
8056
8213
  "publicKey",
8057
8214
  "fingerprint",
8058
8215
  "createdAt"
@@ -8097,7 +8254,7 @@ async function createManagedAgent(store, secrets, input, connectAgent = connect)
8097
8254
  const alias = assertStoreName("agent name", input.name);
8098
8255
  if (!input.enrollmentToken.trim()) throw new AgentServerIdentityError("enrollment_required", "an enrollment token from the target team is required — a self-registered agent would be stranded in its own personal team");
8099
8256
  const releaseAlias = reserveAlias(store, alias);
8100
- let registeredIdentityId;
8257
+ let registered = false;
8101
8258
  try {
8102
8259
  let apiUrl;
8103
8260
  try {
@@ -8114,18 +8271,19 @@ async function createManagedAgent(store, secrets, input, connectAgent = connect)
8114
8271
  });
8115
8272
  if (result.credentials.type !== "agent_key") throw new AgentServerIdentityError("unsupported_credential", `registration returned credential type "${result.credentials.type}"; agent server manages agent-key credentials only`);
8116
8273
  const now = (/* @__PURE__ */ new Date()).toISOString();
8117
- const { identityId, fingerprint, publicKey, privateKey } = result.identity;
8118
- registeredIdentityId = identityId;
8274
+ const { subjectId, fingerprint, publicKey, privateKey } = result.identity;
8275
+ registered = true;
8119
8276
  const agentKeyReference = {
8120
8277
  provider: FILE_SECRET_PROVIDER,
8121
- key: agentKeyKey(identityId)
8278
+ key: agentKeyKey(subjectId)
8122
8279
  };
8123
8280
  const seedReference = {
8124
8281
  provider: FILE_SECRET_PROVIDER,
8125
8282
  key: identitySeedKey(fingerprint)
8126
8283
  };
8127
8284
  const config = {
8128
- identity_id: identityId,
8285
+ subject_id: subjectId,
8286
+ subject_type: "agent",
8129
8287
  registered_at: now,
8130
8288
  agent_key_ref: agentKeyReference,
8131
8289
  keys: {
@@ -8146,15 +8304,15 @@ async function createManagedAgent(store, secrets, input, connectAgent = connect)
8146
8304
  apiUrl: result.apiUrl
8147
8305
  }, store.agentPath(alias), input.signal);
8148
8306
  assertIdentityMatches(whoami, {
8149
- identityId,
8150
8307
  publicKey,
8151
8308
  fingerprint
8152
8309
  }, "authenticated whoami", `new managed agent "${alias}"`);
8310
+ assertSubjectMatches(whoami, config, "authenticated whoami", `managed config ${store.agentPath(alias)}`);
8153
8311
  const boundTeamId = boundTeamIdFromWhoami(whoami);
8154
8312
  const activation = {
8155
8313
  alias,
8156
8314
  source: "managed",
8157
- identityId,
8315
+ subjectId: whoami.subjectId,
8158
8316
  publicKey,
8159
8317
  fingerprint,
8160
8318
  ...boundTeamId ? { boundTeamId } : {},
@@ -8168,11 +8326,11 @@ async function createManagedAgent(store, secrets, input, connectAgent = connect)
8168
8326
  ...boundTeamId ? { boundTeamId } : {}
8169
8327
  };
8170
8328
  } catch (cause) {
8171
- if (!registeredIdentityId && cause instanceof MoltNetError && cause.statusCode !== void 0 && cause.statusCode >= 400 && cause.statusCode < 500) {
8329
+ if (!registered && cause instanceof MoltNetError && cause.statusCode !== void 0 && cause.statusCode >= 400 && cause.statusCode < 500) {
8172
8330
  store.clearPendingRegistration(alias);
8173
8331
  throw new AgentServerIdentityError("registration_failed", registrationRejectionMessage(alias, cause), { cause });
8174
8332
  }
8175
- if (store.hasPendingRegistration(alias)) throw new AgentServerIdentityError("registration_incomplete", registeredIdentityId ? `identity "${registeredIdentityId}" was registered but local activation is incomplete; reconcile or clear its pending Agent Server record before retrying` : `registration for "${alias}" may be incomplete; inspect the remote API before changing its pending Agent Server record`, { cause });
8333
+ if (store.hasPendingRegistration(alias)) throw new AgentServerIdentityError("registration_incomplete", registered ? `the remote agent was registered but local activation is incomplete; reconcile or clear its pending Agent Server record before retrying` : `registration for "${alias}" may be incomplete; inspect the remote API before changing its pending Agent Server record`, { cause });
8176
8334
  throw cause;
8177
8335
  } finally {
8178
8336
  releaseAlias();
@@ -8203,17 +8361,19 @@ async function reconcileManagedRegistration(store, secrets, aliasInput, action,
8203
8361
  if (!config?.agent_key_ref || config.agent_key_ref.provider !== FILE_SECRET_PROVIDER || config.keys.private_key_ref?.provider !== FILE_SECRET_PROVIDER) throw new AgentServerIdentityError("registration_incomplete", `pending registration for "${alias}" does not have complete managed references`);
8204
8362
  const [agentKey, privateKeyState] = await Promise.all([secrets.read(config.agent_key_ref.key), secrets.probe(config.keys.private_key_ref.key)]);
8205
8363
  if (!agentKey || privateKeyState !== "present") throw new AgentServerIdentityError("registration_incomplete", `pending registration for "${alias}" is missing persisted secret material`);
8206
- const identity = identityFromConfig(config);
8207
8364
  const apiUrl = requireConfigApiUrl(config, store.agentPath(alias));
8208
8365
  const whoami = await callWhoami(connectAgent, {
8209
8366
  agentKey,
8210
8367
  apiUrl
8211
8368
  }, store.agentPath(alias), signal);
8369
+ assertSubjectMatches(whoami, config, "authenticated whoami", `pending registration "${alias}" config`);
8370
+ const identity = identityFromConfig(config);
8212
8371
  assertIdentityMatches(whoami, identity, "authenticated whoami", `pending registration "${alias}" config`);
8213
8372
  const boundTeamId = boundTeamIdFromWhoami(whoami);
8214
8373
  const recovered = {
8215
8374
  alias,
8216
8375
  source: "managed",
8376
+ subjectId: whoami.subjectId,
8217
8377
  ...identity,
8218
8378
  ...boundTeamId ? { boundTeamId } : {},
8219
8379
  createdAt: config.registered_at,
@@ -8239,10 +8399,12 @@ async function attachExternalAgent(store, secretProviders, input, connectAgent =
8239
8399
  const whoami = await authenticateConfig(input.configDir, effectiveApiUrl, secretProviders, connectAgent, input.signal);
8240
8400
  const identity = identityFromConfig(config);
8241
8401
  assertIdentityMatches(identity, whoami, `external config ${configPath}`, "authenticated whoami");
8402
+ assertSubjectMatches(whoami, config, "authenticated whoami", `external config ${configPath}`);
8242
8403
  const boundTeamId = boundTeamIdFromWhoami(whoami);
8243
8404
  const activation = {
8244
8405
  alias,
8245
8406
  source: "external",
8407
+ subjectId: whoami.subjectId,
8246
8408
  ...identity,
8247
8409
  ...boundTeamId ? { boundTeamId } : {},
8248
8410
  createdAt: (/* @__PURE__ */ new Date()).toISOString(),
@@ -8260,15 +8422,26 @@ async function attachExternalAgent(store, secretProviders, input, connectAgent =
8260
8422
  releaseAlias();
8261
8423
  }
8262
8424
  }
8263
- /** Load and authenticate the current config, then compare all pinned fields. */
8425
+ /** Load and authenticate the current config, then refresh its derived pin. */
8264
8426
  async function verifyAgentActivation(store, alias, managedSecretProviders, externalSecretProviders, connectAgent = connect, signal) {
8265
8427
  const activation = requireActivation(store, alias);
8266
8428
  const verified = activation.source === "managed" ? await verifyManagedActivation(store, activation, managedSecretProviders, connectAgent, signal) : await verifyExternalActivation(activation, externalSecretProviders, connectAgent, signal);
8267
- assertIdentityMatches(verified.whoami, activation, "authenticated whoami", `agent "${activation.alias}" pinned activation`);
8429
+ assertSubjectMatches(verified.whoami, verified.config, "authenticated whoami", `agent "${activation.alias}" config`);
8430
+ if (verified.whoami.subjectId !== activation.subjectId) throw new AgentServerIdentityError("verification_failed", `authenticated whoami subject does not match agent "${activation.alias}" pinned activation`);
8431
+ const identity = identityFromConfig(verified.config);
8432
+ assertIdentityMatches(verified.whoami, identity, "authenticated whoami", `agent "${activation.alias}" config`);
8268
8433
  const boundTeamId = boundTeamIdFromWhoami(verified.whoami);
8269
8434
  if (activation.boundTeamId !== boundTeamId) throw new AgentServerIdentityError("verification_failed", `authenticated whoami team binding does not match agent "${activation.alias}" pinned activation`);
8435
+ const refreshed = {
8436
+ ...activation,
8437
+ ...identity
8438
+ };
8439
+ if (activation.publicKey !== refreshed.publicKey || activation.fingerprint !== refreshed.fingerprint) {
8440
+ store.writeActivation(refreshed);
8441
+ process.stderr.write(`agent-server: refreshed the authenticated signing identity for ${JSON.stringify(activation.alias)}\n`);
8442
+ }
8270
8443
  return {
8271
- activation,
8444
+ activation: refreshed,
8272
8445
  config: verified.config,
8273
8446
  ...boundTeamId ? { boundTeamId } : {}
8274
8447
  };
@@ -8311,7 +8484,7 @@ function requireTrustedApiOverride(override, configApiUrl, configPath) {
8311
8484
  }
8312
8485
  function assertActivatedConfig(config, activation, configPath, currentApiUrl, pinnedApiUrl) {
8313
8486
  if (currentApiUrl !== pinnedApiUrl) throw new AgentServerIdentityError("verification_failed", `agent config at ${configPath} API endpoint does not match its pinned activation`);
8314
- assertIdentityMatches(identityFromConfig(config), activation, configPath, `agent "${activation.alias}" pinned activation`);
8487
+ if (config.subject_id !== activation.subjectId) throw new AgentServerIdentityError("verification_failed", `agent config at ${configPath} subject does not match its pinned activation`);
8315
8488
  }
8316
8489
  function requireConfigApiUrl(config, configPath) {
8317
8490
  const apiUrl = config?.endpoints?.api?.trim();
@@ -8383,16 +8556,18 @@ function boundedIdentitySignal(signal) {
8383
8556
  return signal ? AbortSignal.any([signal, timeout]) : timeout;
8384
8557
  }
8385
8558
  function identityFromConfig(config) {
8386
- const identityId = config?.identity_id?.trim();
8387
8559
  const publicKey = config?.keys?.public_key?.trim();
8388
8560
  const fingerprint = config?.keys?.fingerprint?.trim();
8389
- if (!identityId || !publicKey || !fingerprint) throw new AgentServerIdentityError("verification_failed", "agent config is missing canonical identity_id, keys.public_key, or keys.fingerprint");
8561
+ if (!publicKey || !fingerprint) throw new AgentServerIdentityError("verification_failed", "agent config is missing keys.public_key or keys.fingerprint");
8390
8562
  return {
8391
- identityId,
8392
8563
  publicKey,
8393
8564
  fingerprint
8394
8565
  };
8395
8566
  }
8567
+ function assertSubjectMatches(current, expected, currentLabel, expectedLabel) {
8568
+ if (!isCanonicalConfig(expected)) throw new AgentServerIdentityError("verification_failed", `${expectedLabel} is missing canonical subject_type=agent and subject_id; run \`moltnet config migrate\` first`);
8569
+ if (current.subjectType !== "agent" || current.subjectId !== expected.subject_id) throw new AgentServerIdentityError("verification_failed", `${currentLabel} subject does not match ${expectedLabel}`);
8570
+ }
8396
8571
  function assertIdentityMatches(current, expected, currentLabel, expectedLabel) {
8397
8572
  const assessment = assessIdentityPin(current, expected);
8398
8573
  if (!assessment.ok) throw new AgentServerIdentityError("verification_failed", `${currentLabel} ${assessment.label} does not match ${expectedLabel}`);
@@ -8407,7 +8582,7 @@ function publicAgentView(store, activation) {
8407
8582
  return {
8408
8583
  kind: "managed",
8409
8584
  agentName: activation.alias,
8410
- identityId: activation.identityId,
8585
+ subjectId: activation.subjectId,
8411
8586
  fingerprint: activation.fingerprint,
8412
8587
  ...activation.boundTeamId ? { teamId: activation.boundTeamId } : {},
8413
8588
  apiUrl: activation.apiUrl,
@@ -8421,7 +8596,7 @@ function publicAgentView(store, activation) {
8421
8596
  agentName: activation.alias,
8422
8597
  configDir: dirname(activation.configPath),
8423
8598
  ...activation.apiUrl ? { apiUrl: activation.apiUrl } : {},
8424
- identityId: activation.identityId,
8599
+ subjectId: activation.subjectId,
8425
8600
  fingerprint: activation.fingerprint,
8426
8601
  ...activation.boundTeamId ? { teamId: activation.boundTeamId } : {},
8427
8602
  createdAt: activation.createdAt
@@ -8567,7 +8742,8 @@ var RunManager = class {
8567
8742
  throw new AgentServerRunError("invalid_spec", `external credentials for "${activation.alias}" could not be projected`);
8568
8743
  }
8569
8744
  }
8570
- env["MOLTNET_EXPECTED_IDENTITY_ID"] = activation.identityId;
8745
+ env["MOLTNET_EXPECTED_SUBJECT_ID"] = activation.subjectId;
8746
+ env["MOLTNET_EXPECTED_SUBJECT_TYPE"] = "agent";
8571
8747
  env["MOLTNET_EXPECTED_PUBLIC_KEY"] = activation.publicKey;
8572
8748
  env["MOLTNET_EXPECTED_FINGERPRINT"] = activation.fingerprint;
8573
8749
  env["MOLTNET_SUPERVISED_RUN"] = "1";
@@ -9224,7 +9400,7 @@ var AgentServerProblemSchema = Type.Object({
9224
9400
  var AgentServerAgentSchema = Type.Object({
9225
9401
  kind: Type.Union([Type.Literal("managed"), Type.Literal("external")]),
9226
9402
  agentName: Type.String(),
9227
- identityId: Type.Optional(Type.String()),
9403
+ subjectId: Type.String(),
9228
9404
  fingerprint: Type.Optional(Type.String()),
9229
9405
  apiUrl: Type.Optional(Type.String()),
9230
9406
  teamId: Type.Optional(Type.String()),
@@ -10387,10 +10563,7 @@ async function runAgentServer(argv) {
10387
10563
  return 1;
10388
10564
  }
10389
10565
  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
- });
10566
+ const root = values.root ?? resolveAgentServerRoot({ root: envConfig.root });
10394
10567
  const defaultApiUrl = values["api-url"] ?? (envConfig.apiUrl || DEFAULT_API_URL);
10395
10568
  const store = new AgentServerStore(root).ensure();
10396
10569
  if (trustRequested) return runTrustCommand(commandArgs, root);
@@ -10686,7 +10859,13 @@ async function runSyncSessions(argv) {
10686
10859
  const state = parseState(values.state);
10687
10860
  const limit = parseLimit(values.limit);
10688
10861
  const agentRootDir = resolve(process.cwd(), values["agent-root"] ?? process.cwd());
10689
- const ctx = await resolveAgentContext(opts.agent, { agentRootDir });
10862
+ const explicitAgentRootDir = values["agent-root"] ? resolve(process.cwd(), values["agent-root"]) : void 0;
10863
+ const cfg = loadConfig();
10864
+ const ctx = await resolveAgentContext(opts.agent, {
10865
+ credentialSource: cfg.credentialSource,
10866
+ envApiUrl: cfg.apiUrl,
10867
+ agentRootDir: explicitAgentRootDir
10868
+ });
10690
10869
  await validateStartupBinding({
10691
10870
  agent: ctx.agent,
10692
10871
  teamId: values.team
@@ -10721,18 +10900,51 @@ function parseLimit(raw) {
10721
10900
  }
10722
10901
  //#endregion
10723
10902
  //#region src/lib/update.ts
10724
- var UPDATE_MANIFEST_URL = "https://themolt.net/download/manifest.json";
10903
+ /**
10904
+ * `npm install -g @themoltnet/agent-daemon@latest` resolves the registry's own
10905
+ * dist-tag, so for an npm install the registry is the precise answer — the
10906
+ * manifest serves the themolt.net pin, which gates the bundle installer's
10907
+ * default and says nothing about what npm already has. It is also the origin an
10908
+ * npm install already talks to, so this adds no new dependency in CI, where the
10909
+ * daemon runs as `npx @themoltnet/agent-daemon` and would otherwise reach for
10910
+ * api.github.com from shared runner IPs.
10911
+ *
10912
+ * Bundle and direct installs ask the release listing, and their upgrade command
10913
+ * passes `MOLTNET_AGENT_VERSION=latest` so the installer resolves the same thing
10914
+ * rather than falling back to its own pinned default. Reporting the pin here
10915
+ * would hide releases that are already downloadable; reporting the release
10916
+ * without the sentinel would advertise a version the command could not deliver.
10917
+ * The pin still governs a fresh `curl .../install/agent | sh`.
10918
+ */
10919
+ var UPDATE_RELEASES_URL = "https://api.github.com/repos/getlarge/themoltnet/releases?per_page=100";
10920
+ var RELEASE_TAG_PREFIX = "agent-daemon-v";
10921
+ var UPDATE_NPM_REGISTRY_URL = "https://registry.npmjs.org/@themoltnet/agent-daemon/latest";
10725
10922
  var UPDATE_CACHE_TTL_MS = 1440 * 60 * 1e3;
10726
10923
  var UPDATE_ERROR_CACHE_TTL_MS = 300 * 1e3;
10924
+ /**
10925
+ * Node does not resolve `process.argv[1]`: invoked through npm's bin shim it
10926
+ * reports `/usr/local/bin/moltnet-agent`, not the `node_modules` path this
10927
+ * matches on. Every global npm install therefore looked like `direct` and was
10928
+ * offered the bundle installer, which would drop a bundle on top of an npm
10929
+ * install. Resolve first; a missing path falls back to the input.
10930
+ */
10931
+ function resolveDaemonExecutable(executable) {
10932
+ if (!executable) return executable;
10933
+ try {
10934
+ return realpathSync(executable);
10935
+ } catch {
10936
+ return executable;
10937
+ }
10938
+ }
10727
10939
  function detectDaemonInstallMethod(executable = process.argv[1] ?? "") {
10728
- const path = executable.replaceAll("\\", "/");
10940
+ const path = resolveDaemonExecutable(executable).replaceAll("\\", "/");
10729
10941
  if (path.includes("/node_modules/@themoltnet/agent-daemon/")) return "npm";
10730
10942
  if (path.includes("/.local/share/moltnet/") || path.includes("/opt/moltnet/")) return "bundle";
10731
10943
  return "direct";
10732
10944
  }
10733
10945
  function daemonUpdateCommand(method) {
10734
10946
  if (method === "npm") return "npm install -g @themoltnet/agent-daemon@latest";
10735
- return "curl -fsSL https://themolt.net/install/agent | sh";
10947
+ return "curl -fsSL https://themolt.net/install/agent | MOLTNET_AGENT_VERSION=latest sh";
10736
10948
  }
10737
10949
  async function checkDaemonUpdate(input) {
10738
10950
  const installMethod = detectDaemonInstallMethod(input.executable ?? process.argv[1] ?? "moltnet-agent");
@@ -10753,11 +10965,15 @@ async function checkDaemonUpdate(input) {
10753
10965
  return result;
10754
10966
  }
10755
10967
  }
10968
+ const viaNpm = installMethod === "npm";
10969
+ const source = viaNpm ? UPDATE_NPM_REGISTRY_URL : UPDATE_RELEASES_URL;
10970
+ const label = viaNpm ? "npm registry" : "release listing";
10756
10971
  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");
10972
+ const response = await (input.fetchFn ?? fetch)(source, { signal: AbortSignal.timeout(5e3) });
10973
+ if (!response.ok) throw new Error(`${label} returned HTTP ${response.status}`);
10974
+ const body = await response.json();
10975
+ const latest = viaNpm ? distTagVersion(body) : newestReleaseVersion(body);
10976
+ if (!latest) throw new Error(`${label} has no valid agent version`);
10761
10977
  await writeCache({
10762
10978
  checkedAt: now.toISOString(),
10763
10979
  latest
@@ -10773,13 +10989,31 @@ async function checkDaemonUpdate(input) {
10773
10989
  throw new Error(`could not check for MoltNet agent updates: ${error instanceof Error ? error.message : String(error)}`);
10774
10990
  }
10775
10991
  }
10776
- function manifestVersion(value) {
10992
+ function distTagVersion(value) {
10777
10993
  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;
10994
+ const version = value.version;
10781
10995
  return typeof version === "string" && validVersion(version) ? normalizeVersion(version) : void 0;
10782
10996
  }
10997
+ /**
10998
+ * The listing is ordered by creation date, not version, so compare every
10999
+ * candidate rather than trusting position. Drafts are filtered explicitly:
11000
+ * unauthenticated callers never see them, but a token would, and this
11001
+ * repository carries stuck drafts that would otherwise look newest.
11002
+ */
11003
+ function newestReleaseVersion(value) {
11004
+ if (!Array.isArray(value)) return void 0;
11005
+ let newest;
11006
+ for (const entry of value) {
11007
+ if (!entry || typeof entry !== "object") continue;
11008
+ const { tag_name: tag, draft, prerelease } = entry;
11009
+ if (draft === true || prerelease === true) continue;
11010
+ if (typeof tag !== "string" || !tag.startsWith(RELEASE_TAG_PREFIX)) continue;
11011
+ const candidate = tag.slice(14);
11012
+ if (!validVersion(candidate)) continue;
11013
+ if (!newest || compareVersions(candidate, newest) > 0) newest = normalizeVersion(candidate);
11014
+ }
11015
+ return newest;
11016
+ }
10783
11017
  function normalizeVersion(value) {
10784
11018
  return value.trim().replace(/^v/, "");
10785
11019
  }
@@ -10819,7 +11053,7 @@ async function writeCache(cache) {
10819
11053
  }
10820
11054
  //#endregion
10821
11055
  //#region src/version.ts
10822
- var DAEMON_VERSION = "0.53.0";
11056
+ var DAEMON_VERSION = "0.55.0";
10823
11057
  //#endregion
10824
11058
  //#region src/cli.ts
10825
11059
  async function runAgentDaemonCli(options) {
@@ -10877,11 +11111,7 @@ async function runUpdate(argv) {
10877
11111
  }
10878
11112
  }
10879
11113
  async function runRuntime(argv) {
10880
- const config = loadAgentServerEnvConfig();
10881
- const registry = new RuntimeRegistry(resolveAgentServerRoot({
10882
- root: config.root,
10883
- xdgConfigHome: config.xdgConfigHome
10884
- }));
11114
+ const registry = new RuntimeRegistry(resolveAgentServerRoot({ root: loadAgentServerEnvConfig().root }));
10885
11115
  const [command, ...rest] = argv;
10886
11116
  if (command === "list") {
10887
11117
  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.55.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",
88
+ "@themoltnet/agent-runtime": "1.0.0",
89
+ "@themoltnet/pi-runtime": "0.15.0",
89
90
  "@themoltnet/os-keyring": "0.3.0",
90
- "@themoltnet/pi-runtime": "0.14.2",
91
- "@themoltnet/sdk": "0.140.1"
91
+ "@themoltnet/sdk": "0.141.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",
101
+ "@moltnet/bootstrap": "0.1.0",
102
102
  "@moltnet/crypto-service": "0.1.0",
103
103
  "@moltnet/execution-integrations": "0.1.0",
104
- "@moltnet/bootstrap": "0.1.0",
104
+ "@moltnet/agent-eval": "0.1.0",
105
105
  "@moltnet/execution-plan": "0.1.0",
106
+ "@moltnet/loopback-companion": "0.1.0",
106
107
  "@moltnet/models": "0.1.0",
108
+ "@moltnet/tasks": "0.1.0",
107
109
  "@moltnet/observability": "0.1.0",
108
- "@moltnet/loopback-companion": "0.1.0",
109
- "@moltnet/runtime-profiles": "0.1.0",
110
- "@moltnet/tasks": "0.1.0"
110
+ "@moltnet/runtime-profiles": "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
  },