@themoltnet/agent-daemon 0.59.0 → 0.60.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 +23 -11
  2. package/dist/cli.js +115 -81
  3. package/package.json +8 -8
package/README.md CHANGED
@@ -114,7 +114,7 @@ All config flows from environment variables. The daemon reads them in
114
114
  | Var | Required | Purpose |
115
115
  | --------------------- | ---------------------------------- | ------------------------------------------------------------------------- |
116
116
  | `GIT_CONFIG_GLOBAL` | config-based | Optional git identity path; not needed for configless startup. |
117
- | `MOLTNET_AGENT_NAME` | yes | Agent name (matches `.moltnet/<name>/`). |
117
+ | `MOLTNET_AGENT_NAME` | yes | Central identity alias (`identities/<name>/`). |
118
118
  | `MOLTNET_API_URL` | configless only | Explicit API endpoint; configless runs never read it from `moltnet.json`. |
119
119
  | `MOLTNET_AGENT_KEY` | no | Team- or identity-scoped agent key. Overrides `moltnet.json`. |
120
120
  | `MOLTNET_PRIVATE_KEY` | configless `once`, `poll`, `drain` | Base64 Ed25519 seed used by daemon-owned executor attestation. |
@@ -133,18 +133,19 @@ its gitconfig in `.moltnet/<agent>/`. Three paths create it:
133
133
  does the same for coding agents that need git and GitHub.
134
134
 
135
135
  **The daemon runs on an agent key only.** OAuth2 client_credentials is not
136
- accepted: it hands the daemon the full 17-scope agent grant against a six-scope
136
+ accepted: it hands the daemon the full agent OAuth2 grant against a least-privilege
137
137
  need, and a Hydra token cannot be a Talos derivation parent. A `moltnet.json`
138
- without `agent_key_ref` is refused at startup with the command that fixes it.
138
+ without `agent_key_ref` or `agent_key_refs` is refused at startup with the command that fixes it.
139
139
 
140
140
  The key reaches the daemon two ways, and `MOLTNET_AGENT_KEY` wins when both are
141
141
  present:
142
142
 
143
143
  - **Configless** — `MOLTNET_AGENT_KEY` (or `MOLTNET_AGENT_KEY_REF`) in the
144
144
  environment. No agent files are read at all.
145
- - **From `moltnet.json`** — an `agent_key_ref` pointing at a secret provider,
146
- which is what `moltnet agents keys create --store` writes. The plaintext
147
- secret never lands in the file.
145
+ - **From `moltnet.json`** — `agent_key_refs[teamId]` selects the run team's
146
+ provider reference. `agent_key_ref` is used only if that team has no entry.
147
+ A configured entry that fails never falls back. The plaintext secret never
148
+ lands in the file.
148
149
 
149
150
  Mint or rotate the key with the CLI, which is separate operator tooling and
150
151
  keeps using OAuth2 for its own authentication:
@@ -155,14 +156,21 @@ moltnet agents keys create --agent-id <uuid> --team-id <uuid> \
155
156
  moltnet agents keys rotate <key-id> --team-id <uuid> --store
156
157
  ```
157
158
 
159
+ To enroll the same identity into another team, use
160
+ `moltnet teams join --code <code> --issue-agent-key --store --idempotency-key <uuid>`.
161
+ Each Agent Server run selects its `teamId` before activation and profile lookup;
162
+ concurrent runs do not change a shared team selector. New runs reload credentials,
163
+ so restart active runs after rotating a key. See the
164
+ [enrollment and recovery guide](../../docs/operate/agent-keys.md#enroll-an-existing-agent-into-another-team).
165
+
158
166
  The daemon reconciles a team-bound key against `--team` at startup; an
159
167
  identity-scoped key may select any team where the agent is authorized. It fails
160
168
  fast if the key is rejected, is not an agent, or a team binding mismatches. See
161
169
  [Run the daemon with an agent key](../../docs/operate/agent-keys.md#run-the-daemon-with-an-agent-key).
162
170
 
163
171
  Daemon authentication and the guest boundary are two separate concerns. Where
164
- the agent key comes from (the environment, or an `agent_key_ref` in
165
- `.moltnet/<agent>/moltnet.json` resolved through the host secret provider)
172
+ the agent key comes from (the environment, or a selected key reference in the
173
+ central identity's `moltnet.json` resolved through the host secret provider)
166
174
  decides how the host-side SDK `Agent` is built. The guest boundary is fixed: **the guest never
167
175
  receives MoltNet credential material.** No `.moltnet` file, gitconfig, SSH
168
176
  signing key, GitHub App PEM, or MoltNet environment credential is injected into
@@ -196,9 +204,13 @@ An agent key used by the daemon needs this least-privilege scope set:
196
204
  agent:profile crypto:sign runtime:read task:read task:claim task:execute
197
205
  ```
198
206
 
199
- The Console selects these scopes by default. Knowledge-enabled workers must add
200
- `diary:read`, `diary:write`, `pack:read`, and `pack:write` when the key is
201
- issued.
207
+ The daemon also uses `diary:read`, `team:read` and `team:join` when the key
208
+ carries them -- read access to the agent's teams and their diaries, and
209
+ enrollment into a team. Startup does not check for them: a key without them
210
+ claims and runs work, and the daemon reports which are absent.
211
+
212
+ The Console selects the full set by default. Knowledge-enabled workers must add
213
+ `diary:write`, `pack:read`, and `pack:write` when the key is issued.
202
214
 
203
215
  `crypto:sign` is in the minimum because host-capability signing runs on the
204
216
  daemon's own credential: the local seed signer calls the signing-request
package/dist/cli.js CHANGED
@@ -14,7 +14,7 @@ import { AgentRuntime, ApiTaskReporter, ApiTaskSource, PollingApiTaskSource, cre
14
14
  import { GuestEnvironmentBoundaryError, assertGuestEnvironmentBoundary, createPiRetryTriage, findMainWorktree, isResolvedPathInsideRoot, normalizeRetryTriageResult, redactRetryTriageSecrets, resolveRuntimeProfileModel } from "@themoltnet/pi-runtime";
15
15
  import { FILE_SECRET_PROVIDER, FileSecretProvider, RegisterIdentityError, boundedIdentitySignal, connect, createNodeSecretProviderRegistry, register } from "@themoltnet/sdk/node";
16
16
  import { constants, copyFileSync, createReadStream, createWriteStream, existsSync, lstatSync, mkdirSync, mkdtempSync, openSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
17
- import { AuthenticationError, IDENTITY_ALIAS_PATTERN, MoltNetError, assertIdentityAlias, assertTrustedConfigApiUrl, createExecutorAttestor, formatSecretReferenceString, getConfigDir, getIdentityDir, isCanonicalConfig, parseSecretReferenceString, readConfig, requireSecureCredentialApiUrl, resolveAgentKey, resolveEnvSecretReference, resolveIdentitySeed } from "@themoltnet/sdk";
17
+ import { AuthenticationError, IDENTITY_ALIAS_PATTERN, MoltNetError, assertIdentityAlias, assertTrustedConfigApiUrl, createExecutorAttestor, formatSecretReferenceString, getConfigDir, getIdentityDir, hasAgentKeyConfiguration, isCanonicalConfig, parseSecretReferenceString, readConfig, requireSecureCredentialApiUrl, resolveAgentKey, resolveEnvSecretReference, resolveIdentitySeed, selectAgentKeyReference } from "@themoltnet/sdk";
18
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";
@@ -774,13 +774,24 @@ var CREDENTIAL_SCOPES = {
774
774
  TaskManage: "task:manage",
775
775
  TaskRead: "task:read",
776
776
  TaskWrite: "task:write",
777
+ TeamJoin: "team:join",
777
778
  TeamManage: "team:manage",
778
779
  TeamRead: "team:read"
779
780
  };
780
781
  var ALL_CREDENTIAL_SCOPES = Object.freeze(Object.values(CREDENTIAL_SCOPES));
781
782
  /**
782
- * Minimum grant for the agent daemon. Task credentials attenuate this further
783
- * to `task:execute` alone.
783
+ * What the agent daemon cannot run without, checked against
784
+ * `GET /agents/whoami` at startup. Task credentials attenuate it further to
785
+ * `task:execute` alone.
786
+ *
787
+ * This is the **boot floor**, and deliberately not the same list as
788
+ * `AGENT_CREDENTIAL_SCOPES`. A credential's scopes are fixed when it is minted
789
+ * and `POST /agent-keys` caps a new key at the scopes of the credential
790
+ * requesting it, so no key can ever widen itself. A scope added here therefore
791
+ * stops every daemon already in the field, and only a human with a Console
792
+ * session can mint the replacement. Add one only when the daemon genuinely
793
+ * cannot work without it; anything a caller merely benefits from belongs in
794
+ * `DAEMON_OPTIONAL_SCOPES`, where absence costs a capability instead.
784
795
  *
785
796
  * `crypto:sign` is part of the minimum because host-capability signing runs on
786
797
  * the daemon's own credential: the local seed signer calls the signing-request
@@ -788,7 +799,7 @@ var ALL_CREDENTIAL_SCOPES = Object.freeze(Object.values(CREDENTIAL_SCOPES));
788
799
  * cleanly and then fails the first time guest code signs a diary entry or a
789
800
  * commit.
790
801
  */
791
- var AGENT_CREDENTIAL_SCOPES = [
802
+ var DAEMON_MINIMUM_SCOPES = [
792
803
  CREDENTIAL_SCOPES.AgentProfile,
793
804
  CREDENTIAL_SCOPES.CryptoSign,
794
805
  CREDENTIAL_SCOPES.RuntimeRead,
@@ -796,6 +807,22 @@ var AGENT_CREDENTIAL_SCOPES = [
796
807
  CREDENTIAL_SCOPES.TaskClaim,
797
808
  CREDENTIAL_SCOPES.TaskExecute
798
809
  ];
810
+ /**
811
+ * Read and enrollment authority a daemon uses when it has it, and runs without
812
+ * when it does not: reading the teams it belongs to and their diaries, and
813
+ * joining a team it is not yet a member of.
814
+ *
815
+ * Which product surface each one enables is deliberately not recorded here.
816
+ * That mapping belongs to whatever consumes the scope and changes with it,
817
+ * while the scope names are the contract and do not.
818
+ */
819
+ var DAEMON_OPTIONAL_SCOPES = [
820
+ CREDENTIAL_SCOPES.DiaryRead,
821
+ CREDENTIAL_SCOPES.TeamRead,
822
+ CREDENTIAL_SCOPES.TeamJoin
823
+ ];
824
+ /** What a newly issued agent key should carry: the floor plus the rest. */
825
+ var AGENT_CREDENTIAL_SCOPES = [...DAEMON_MINIMUM_SCOPES, ...DAEMON_OPTIONAL_SCOPES];
799
826
  CREDENTIAL_SCOPES.AgentProfile, CREDENTIAL_SCOPES.TaskRead, CREDENTIAL_SCOPES.TaskWrite;
800
827
  CREDENTIAL_SCOPES.AgentProfile, CREDENTIAL_SCOPES.DiaryRead, CREDENTIAL_SCOPES.PackRead, CREDENTIAL_SCOPES.RuntimeRead, CREDENTIAL_SCOPES.TaskRead, CREDENTIAL_SCOPES.TeamRead;
801
828
  Object.freeze(ALL_CREDENTIAL_SCOPES.filter((scope) => scope !== CREDENTIAL_SCOPES.HumanProfile));
@@ -819,6 +846,7 @@ var MCP_CLIENT_SCOPES = [
819
846
  CREDENTIAL_SCOPES.TaskManage,
820
847
  CREDENTIAL_SCOPES.TaskRead,
821
848
  CREDENTIAL_SCOPES.TaskWrite,
849
+ CREDENTIAL_SCOPES.TeamJoin,
822
850
  CREDENTIAL_SCOPES.TeamManage,
823
851
  CREDENTIAL_SCOPES.TeamRead
824
852
  ];
@@ -1172,17 +1200,16 @@ Type.Object({
1172
1200
  Type.Literal("executor"),
1173
1201
  Type.Literal("member")
1174
1202
  ])),
1175
- maxUses: Type.Optional(Type.Integer({
1176
- minimum: 1,
1177
- default: 1
1178
- })),
1179
1203
  expiresInHours: Type.Optional(Type.Integer({
1180
1204
  minimum: 1,
1181
1205
  maximum: 720,
1182
1206
  default: 168
1183
1207
  }))
1184
1208
  });
1185
- Type.Object({ code: Type.String({ minLength: 1 }) });
1209
+ Type.Object({
1210
+ code: Type.String({ minLength: 1 }),
1211
+ issueAgentKey: Type.Optional(Type.Literal(true))
1212
+ });
1186
1213
  Type.Object({ role: Type.Union([
1187
1214
  Type.Literal("manager"),
1188
1215
  Type.Literal("executor"),
@@ -1207,8 +1234,7 @@ Type.Object({
1207
1234
  Type.Literal("executor"),
1208
1235
  Type.Literal("member")
1209
1236
  ]),
1210
- maxUses: Type.Integer(),
1211
- useCount: Type.Integer(),
1237
+ usedAt: Type.Union([Type.String({ format: "date-time" }), Type.Null()]),
1212
1238
  expiresAt: DateTimeUnsafe,
1213
1239
  createdAt: DateTimeUnsafe
1214
1240
  });
@@ -1239,11 +1265,7 @@ Type.Object({
1239
1265
  });
1240
1266
  Type.Object({
1241
1267
  teamId: UuidSchema,
1242
- role: Type.Union([
1243
- Type.Literal("manager"),
1244
- Type.Literal("executor"),
1245
- Type.Literal("member")
1246
- ])
1268
+ role: TeamRoleSchema
1247
1269
  });
1248
1270
  Type.Object({
1249
1271
  updated: Type.Boolean(),
@@ -3501,10 +3523,11 @@ Run \`agent-daemon <command> --help\` for command-specific flags.
3501
3523
  Prerequisites:
3502
3524
  - configless: MOLTNET_AGENT_KEY (or MOLTNET_AGENT_KEY_REF) and
3503
3525
  MOLTNET_PRIVATE_KEY (or MOLTNET_PRIVATE_KEY_REF); no agent files
3504
- - config-based and sync-sessions: <agent-root>/.moltnet/<agent>/moltnet.json
3505
- carrying agent_key_ref (OAuth2 client credentials are not accepted)
3526
+ - config-based: ~/.config/moltnet/identities/<agent>/moltnet.json
3527
+ carrying agent_key_refs or agent_key_ref (OAuth2 is not accepted)
3528
+ --agent-root explicitly selects a legacy .moltnet/<agent> bundle
3506
3529
 
3507
- No key yet? Mint one with the CLI (--store writes agent_key_ref into
3530
+ No key yet? Mint one with the CLI (--store writes the team slot into
3508
3531
  moltnet.json and keeps the secret in a provider):
3509
3532
 
3510
3533
  moltnet teams list # find the team id
@@ -3712,6 +3735,10 @@ function assessAgentStartupPin(current, expected) {
3712
3735
  };
3713
3736
  return { ok: true };
3714
3737
  }
3738
+ /** A configured team slot must authenticate with that exact team ceiling. */
3739
+ function matchesCredentialTeam(current, credentialTeamId) {
3740
+ return !credentialTeamId || current.credentialBinding?.bindingScope === "team" && current.credentialBinding.boundTeamId === credentialTeamId;
3741
+ }
3715
3742
  //#endregion
3716
3743
  //#region src/lib/agent-context.ts
3717
3744
  /**
@@ -3755,7 +3782,7 @@ function detectCredentialSource(env) {
3755
3782
  function assessStartupBinding(whoami, teamId) {
3756
3783
  if (whoami.subjectType !== "agent") return {
3757
3784
  ok: false,
3758
- reason: `the daemon must authenticate as an agent, but whoami reported subjectType "${whoami.subjectType}". Provide agent credentials (an agent key or the agent's client id/secret).`
3785
+ reason: `the daemon must authenticate as an agent, but whoami reported subjectType "${whoami.subjectType}". Provide agent credentials (an agent key).`
3759
3786
  };
3760
3787
  const boundTeamId = whoami.credentialBinding?.bindingScope === "team" ? whoami.credentialBinding.boundTeamId : void 0;
3761
3788
  if (teamId && boundTeamId && boundTeamId !== teamId) return {
@@ -3768,8 +3795,7 @@ function assessStartupBinding(whoami, teamId) {
3768
3795
  * Validate at startup — after `connect()`, before polling — that the connected
3769
3796
  * credential can operate as `teamId`, failing fast with an actionable message
3770
3797
  * instead of letting an obscure 401/403 surface mid-poll. Runs in both auth
3771
- * modes; in OAuth2 mode it also doubles as an API-reachability and
3772
- * subject-type check. Returns the `whoami` so the caller can log the resolved
3798
+ * credential sources and also checks API reachability and subject type. Returns the `whoami` so the caller can log the resolved
3773
3799
  * identity (never the secret).
3774
3800
  */
3775
3801
  async function validateStartupBinding(options) {
@@ -3785,6 +3811,7 @@ async function validateStartupBinding(options) {
3785
3811
  setTimeout(resolve, 100 * attempt);
3786
3812
  });
3787
3813
  }
3814
+ if (!matchesCredentialTeam(whoami, options.credentialTeamId)) throw new Error("Daemon startup validation failed: selected team credential has a different binding.");
3788
3815
  const assessment = assessStartupBinding(whoami, options.teamId);
3789
3816
  if (!assessment.ok) throw new Error(`Daemon startup validation failed: ${assessment.reason}`);
3790
3817
  const expected = options.expectedAgent;
@@ -3807,9 +3834,9 @@ async function resolveAgentContext(agentName, options = {}) {
3807
3834
  credentialSource: "environment"
3808
3835
  };
3809
3836
  const config = await readConfig(agentDir);
3810
- if (!config?.agent_key_ref) throw new Error(agentKeyRequiredMessage(agentDir, agentName));
3837
+ if (!config || !hasAgentKeyConfiguration(config)) throw new Error(agentKeyRequiredMessage(agentDir, agentName));
3811
3838
  const secretProviders = createNodeSecretProviderRegistry();
3812
- const agentKey = await resolveAgentKey(config, secretProviders);
3839
+ const agentKey = await resolveAgentKey(config, secretProviders, options.teamId);
3813
3840
  if (!agentKey) throw new Error(agentKeyRequiredMessage(agentDir, agentName));
3814
3841
  return {
3815
3842
  agentDir,
@@ -3820,7 +3847,8 @@ async function resolveAgentContext(agentName, options = {}) {
3820
3847
  agentKey,
3821
3848
  apiUrl: resolveConfigApiUrl(config, options.envApiUrl)
3822
3849
  }),
3823
- credentialSource: "config"
3850
+ credentialSource: "config",
3851
+ credentialTeamId: selectAgentKeyReference(config, options.teamId)?.teamId
3824
3852
  };
3825
3853
  }
3826
3854
  /**
@@ -3831,7 +3859,7 @@ async function resolveAgentContext(agentName, options = {}) {
3831
3859
  */
3832
3860
  function agentKeyRequiredMessage(agentDir, agentName) {
3833
3861
  return [
3834
- `${join(agentDir, "moltnet.json")} has no "agent_key_ref". The daemon`,
3862
+ `${join(agentDir, "moltnet.json")} has no "agent_key_ref" or "agent_key_refs". The daemon`,
3835
3863
  `requires a team-bound agent key; OAuth2 client_credentials is no longer`,
3836
3864
  `accepted.`,
3837
3865
  ``,
@@ -3844,7 +3872,7 @@ function agentKeyRequiredMessage(agentDir, agentName) {
3844
3872
  ` --scopes ${DAEMON_KEY_SCOPES.join(",")} \\`,
3845
3873
  ` --store`,
3846
3874
  ``,
3847
- `--store writes "agent_key_ref" into moltnet.json and keeps the secret in`,
3875
+ `--store writes the team slot in "agent_key_refs" and keeps the secret in`,
3848
3876
  `a provider, so the key itself never lands in the file. Omit --scopes to`,
3849
3877
  `get the same daemon minimum by default.`,
3850
3878
  ``,
@@ -4558,7 +4586,14 @@ function recoverScratchWorkspacePath(producer, stateDirs) {
4558
4586
  }
4559
4587
  //#endregion
4560
4588
  //#region src/lib/executor-attestation.ts
4561
- var DAEMON_REQUIRED_SCOPES = AGENT_CREDENTIAL_SCOPES;
4589
+ /**
4590
+ * The startup gate, which is the boot floor rather than the issuance default.
4591
+ * Gating on the wider `AGENT_CREDENTIAL_SCOPES` would refuse every key minted
4592
+ * before a scope was added to it, and a key cannot widen itself.
4593
+ */
4594
+ var DAEMON_REQUIRED_SCOPES = DAEMON_MINIMUM_SCOPES;
4595
+ /** What `moltnet agents keys create` should mint for a new daemon. */
4596
+ var DAEMON_RECOMMENDED_SCOPES = AGENT_CREDENTIAL_SCOPES;
4562
4597
  async function resolveExecutorSigningPrivateKey(input) {
4563
4598
  if (input.credentialSource === "environment") {
4564
4599
  const privateKey = input.configuredPrivateKey.trim();
@@ -4585,7 +4620,7 @@ async function resolveExecutorSigningPrivateKey(input) {
4585
4620
  function validateDaemonScopes(whoami) {
4586
4621
  const available = new Set(whoami.scopes ?? []);
4587
4622
  const missing = DAEMON_REQUIRED_SCOPES.filter((scope) => !available.has(scope));
4588
- if (missing.length > 0) throw new Error(`Daemon startup credential is missing required scopes: ${missing.join(" ")}. Issue a replacement credential with ${DAEMON_REQUIRED_SCOPES.join(" ")}.`);
4623
+ if (missing.length > 0) throw new Error(`Daemon startup credential is missing required scopes: ${missing.join(" ")}. Issue a replacement credential with ${DAEMON_RECOMMENDED_SCOPES.join(" ")}.`);
4589
4624
  }
4590
4625
  async function validateExecutorSigningIdentity(input) {
4591
4626
  let publicKey;
@@ -6632,11 +6667,13 @@ async function runPolling(opts) {
6632
6667
  const resolvedContext = await resolveAgentContext(identity.agent, {
6633
6668
  agentRootDir: explicitAgentRootDir,
6634
6669
  credentialSource: cfg.credentialSource,
6635
- envApiUrl: cfg.apiUrl
6670
+ envApiUrl: cfg.apiUrl,
6671
+ teamId
6636
6672
  });
6637
6673
  gate = "authenticate_and_bind";
6638
6674
  const whoami = await validateStartupBinding({
6639
6675
  agent: resolvedContext.agent,
6676
+ credentialTeamId: resolvedContext.credentialTeamId,
6640
6677
  teamId,
6641
6678
  expectedAgent: cfg.expectedAgent
6642
6679
  });
@@ -7239,11 +7276,13 @@ async function runOnce(argv, runtimeAdapter = defaultPiDaemonAdapter) {
7239
7276
  const resolvedContext = await resolveAgentContext(identity.agent, {
7240
7277
  agentRootDir: explicitAgentRootDir,
7241
7278
  credentialSource: cfg.credentialSource,
7242
- envApiUrl: cfg.apiUrl
7279
+ envApiUrl: cfg.apiUrl,
7280
+ teamId: values.team
7243
7281
  });
7244
7282
  gate = "authenticate_and_bind";
7245
7283
  const whoami = await validateStartupBinding({
7246
7284
  agent: resolvedContext.agent,
7285
+ credentialTeamId: resolvedContext.credentialTeamId,
7247
7286
  teamId: values.team,
7248
7287
  expectedAgent: cfg.expectedAgent
7249
7288
  });
@@ -9540,9 +9579,10 @@ async function createManagedAgent(store, secrets, input, connectAgent = connect)
9540
9579
  */
9541
9580
  function incompleteRegistrationMessage(alias, known) {
9542
9581
  const endpoint = `POST /v1/agents/${alias}/reconcile`;
9582
+ const recovery = known.recoveryPath ? ` Inspect the protected recovery file at ${known.recoveryPath}; it may be incomplete.` : "";
9543
9583
  const reconcile = known.configPath ? `Finish it with ${endpoint} and {"action":"resume"}, or discard the local record with {"action":"abandon"}.` : `No local config was written, so it cannot be resumed; discard the local record with ${endpoint} and {"action":"abandon"}.`;
9544
- if (known.subjectId) return `the remote agent ${known.subjectId} was registered but local activation is incomplete. ${reconcile}`;
9545
- return `registration for "${alias}" may have completed on the server${known.fingerprint ? ` (fingerprint ${known.fingerprint})` : ""}; look the agent up first. ${reconcile}`;
9584
+ if (known.subjectId) return `the remote agent ${known.subjectId} was registered but local activation is incomplete. ${reconcile}${recovery}`;
9585
+ return `registration for "${alias}" may have completed on the server${known.fingerprint ? ` (fingerprint ${known.fingerprint})` : ""}; look the agent up first. ${reconcile}${recovery}`;
9546
9586
  }
9547
9587
  /** Resume a fully persisted registration or explicitly abandon local recovery. */
9548
9588
  async function reconcileManagedRegistration(store, secrets, aliasInput, action, connectAgent = connect, signal) {
@@ -9563,8 +9603,9 @@ async function reconcileManagedRegistration(store, secrets, aliasInput, action,
9563
9603
  store.clearPendingRegistration(alias);
9564
9604
  return null;
9565
9605
  }
9566
- 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`);
9567
- const [agentKey, privateKeyState] = await Promise.all([secrets.read(config.agent_key_ref.key), secrets.probe(config.keys.private_key_ref.key)]);
9606
+ const reference = config ? selectAgentKeyReference(config)?.reference : void 0;
9607
+ if (!config || !reference || reference.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`);
9608
+ const [agentKey, privateKeyState] = await Promise.all([secrets.read(reference.key), secrets.probe(config.keys.private_key_ref.key)]);
9568
9609
  if (!agentKey || privateKeyState !== "present") throw new AgentServerIdentityError("registration_incomplete", `pending registration for "${alias}" is missing persisted secret material`);
9569
9610
  const apiUrl = requireConfigApiUrl(config, store.agentPath(alias));
9570
9611
  const whoami = await callWhoami(connectAgent, {
@@ -9600,9 +9641,9 @@ async function attachExternalAgent(store, secretProviders, input, connectAgent =
9600
9641
  try {
9601
9642
  if (!central) externalAgentLocation(configPath);
9602
9643
  const config = await readCurrentConfig(configPath);
9603
- if (central && !config.agent_key_ref) throw new AgentServerIdentityError("unsupported_credential", `central identity "${alias}" needs a stored agent key before the Agent Server can run it`);
9644
+ if (central && !hasAgentKeyConfiguration(config)) throw new AgentServerIdentityError("unsupported_credential", `central identity "${alias}" needs a stored agent key before the Agent Server can run it`);
9604
9645
  const configApiUrl = requireTrustedConfigApiUrl(config, configPath);
9605
- const whoami = await authenticateConfig(config, configPath, requireTrustedApiOverride(input.apiUrl, configApiUrl, configPath), secretProviders, connectAgent, input.signal);
9646
+ const whoami = await authenticateConfig(config, configPath, requireTrustedApiOverride(input.apiUrl, configApiUrl, configPath), secretProviders, connectAgent, input.signal, input.teamId);
9606
9647
  const identity = identityFromConfig(config);
9607
9648
  assertIdentityMatches(identity, whoami, `external config ${configPath}`, "authenticated whoami");
9608
9649
  assertSubjectMatches(whoami, config, "authenticated whoami", `external config ${configPath}`);
@@ -9629,15 +9670,16 @@ async function attachExternalAgent(store, secretProviders, input, connectAgent =
9629
9670
  }
9630
9671
  }
9631
9672
  /** Load and authenticate the current config, then refresh its derived pin. */
9632
- async function verifyAgentActivation(store, alias, managedSecretProviders, externalSecretProviders, connectAgent = connect, signal) {
9673
+ async function verifyAgentActivation(store, alias, managedSecretProviders, externalSecretProviders, connectAgent = connect, signal, teamId) {
9633
9674
  const activation = requireActivation(store, alias);
9634
- const verified = activation.source === "managed" ? await verifyManagedActivation(store, activation, managedSecretProviders, connectAgent, signal) : await verifyExternalActivation(store, activation, externalSecretProviders, connectAgent, signal);
9675
+ teamId ??= activation.boundTeamId;
9676
+ const verified = activation.source === "managed" ? await verifyManagedActivation(store, activation, managedSecretProviders, connectAgent, signal, teamId) : await verifyExternalActivation(store, activation, externalSecretProviders, connectAgent, signal, teamId);
9635
9677
  assertSubjectMatches(verified.whoami, verified.config, "authenticated whoami", `agent "${activation.alias}" config`);
9636
9678
  if (verified.whoami.subjectId !== activation.subjectId) throw new AgentServerIdentityError("verification_failed", `authenticated whoami subject does not match agent "${activation.alias}" pinned activation`);
9637
9679
  const identity = identityFromConfig(verified.config);
9638
9680
  assertIdentityMatches(verified.whoami, identity, "authenticated whoami", `agent "${activation.alias}" config`);
9639
9681
  const boundTeamId = boundTeamIdFromWhoami(verified.whoami);
9640
- if (activation.boundTeamId !== boundTeamId) throw new AgentServerIdentityError("verification_failed", `authenticated whoami team binding does not match agent "${activation.alias}" pinned activation`);
9682
+ if (!selectAgentKeyReference(verified.config, teamId)?.teamId && activation.boundTeamId !== boundTeamId) throw new AgentServerIdentityError("verification_failed", `authenticated whoami team binding does not match agent "${activation.alias}" pinned activation`);
9641
9683
  const refreshed = {
9642
9684
  ...activation,
9643
9685
  ...identity
@@ -9652,36 +9694,26 @@ async function verifyAgentActivation(store, alias, managedSecretProviders, exter
9652
9694
  ...boundTeamId ? { boundTeamId } : {}
9653
9695
  };
9654
9696
  }
9655
- async function verifyManagedActivation(store, activation, secretProviders, connectAgent, signal) {
9697
+ async function verifyManagedActivation(store, activation, secretProviders, connectAgent, signal, teamId) {
9656
9698
  const configPath = store.agentPath(activation.alias);
9657
9699
  const config = await readCurrentConfig(configPath);
9658
9700
  assertActivatedConfig(config, activation, configPath, requireConfigApiUrl(config, configPath), activation.apiUrl);
9659
- let agentKey;
9660
- try {
9661
- agentKey = await resolveAgentKey(config, secretProviders);
9662
- } catch (cause) {
9663
- throw verificationError(`could not resolve the managed agent key for "${activation.alias}"`, cause);
9664
- }
9665
- if (!agentKey) throw new AgentServerIdentityError("verification_failed", `managed config for "${activation.alias}" has no agent_key_ref`);
9666
9701
  return {
9667
9702
  config,
9668
- whoami: await callWhoami(connectAgent, {
9669
- agentKey,
9670
- apiUrl: activation.apiUrl
9671
- }, configPath, signal)
9703
+ whoami: await authenticateConfig(config, configPath, activation.apiUrl, secretProviders, connectAgent, signal, teamId)
9672
9704
  };
9673
9705
  }
9674
- async function verifyExternalActivation(store, activation, secretProviders, connectAgent, signal) {
9706
+ async function verifyExternalActivation(store, activation, secretProviders, connectAgent, signal, teamId) {
9675
9707
  const central = activation.configPath === store.agentPath(activation.alias);
9676
9708
  if (!central) externalAgentLocation(activation.configPath);
9677
9709
  assertTrustedConfigApiUrl(activation.configApiUrl);
9678
9710
  const config = await readCurrentConfig(activation.configPath);
9679
- if (central && !config.agent_key_ref) throw new AgentServerIdentityError("unsupported_credential", `central identity "${activation.alias}" needs a stored agent key before the Agent Server can run it`);
9711
+ if (central && !hasAgentKeyConfiguration(config)) throw new AgentServerIdentityError("unsupported_credential", `central identity "${activation.alias}" needs a stored agent key before the Agent Server can run it`);
9680
9712
  assertActivatedConfig(config, activation, activation.configPath, requireTrustedConfigApiUrl(config, activation.configPath), activation.configApiUrl);
9681
9713
  const effectiveApiUrl = requireTrustedApiOverride(activation.apiUrl, activation.configApiUrl, activation.configPath);
9682
9714
  return {
9683
9715
  config,
9684
- whoami: await authenticateConfig(config, activation.configPath, effectiveApiUrl, secretProviders, connectAgent, signal)
9716
+ whoami: await authenticateConfig(config, activation.configPath, effectiveApiUrl, secretProviders, connectAgent, signal, teamId)
9685
9717
  };
9686
9718
  }
9687
9719
  /** Never let request or activation metadata redirect persisted credentials. */
@@ -9741,18 +9773,20 @@ function externalAgentLocation(configPath) {
9741
9773
  agentRoot: dirname(moltnetDir)
9742
9774
  };
9743
9775
  }
9744
- async function authenticateConfig(config, configPath, apiUrl, secretProviders, connectAgent, signal) {
9776
+ async function authenticateConfig(config, configPath, apiUrl, secretProviders, connectAgent, signal, teamId) {
9745
9777
  let agentKey;
9746
9778
  try {
9747
- agentKey = await resolveAgentKey(config, secretProviders);
9779
+ agentKey = await resolveAgentKey(config, secretProviders, teamId);
9748
9780
  } catch (cause) {
9749
- throw verificationError(`could not resolve the daemon agent key from ${configPath}`, cause);
9781
+ throw verificationError(`could not resolve the daemon agent key from ${configPath}; select a team and enroll or repair its stored key`, cause);
9750
9782
  }
9751
9783
  if (!agentKey) throw new AgentServerIdentityError("unsupported_credential", `external daemon config at ${configPath} must contain an agent_key_ref`);
9752
- return callWhoami(connectAgent, {
9784
+ const whoami = await callWhoami(connectAgent, {
9753
9785
  agentKey,
9754
9786
  ...apiUrl ? { apiUrl } : {}
9755
9787
  }, configPath, signal);
9788
+ if (!matchesCredentialTeam(whoami, selectAgentKeyReference(config, teamId)?.teamId)) throw new AgentServerIdentityError("verification_failed", "authenticated credential team binding does not match the selected team");
9789
+ return whoami;
9756
9790
  }
9757
9791
  async function callWhoami(connectAgent, options, source, signal) {
9758
9792
  try {
@@ -9797,7 +9831,7 @@ function publicAgentView(store, activation) {
9797
9831
  ...activation.boundTeamId ? { teamId: activation.boundTeamId } : {},
9798
9832
  apiUrl: activation.apiUrl,
9799
9833
  createdAt: activation.createdAt,
9800
- hasAgentKey: Boolean(config?.agent_key_ref),
9834
+ hasAgentKey: Boolean(config && hasAgentKeyConfiguration(config)),
9801
9835
  hasPrivateKey: Boolean(config?.keys.private_key_ref)
9802
9836
  };
9803
9837
  }
@@ -9942,15 +9976,16 @@ var RunManager = class {
9942
9976
  ...target.extraArgs
9943
9977
  ];
9944
9978
  if (activation.source === "managed") {
9945
- if (!config.agent_key_ref || !config.keys.private_key_ref) throw new AgentServerRunError("invalid_spec", `managed config for "${activation.alias}" is missing canonical secret references`);
9979
+ const reference = selectAgentKeyReference(config, spec.teamId)?.reference;
9980
+ if (!reference || !config.keys.private_key_ref) throw new AgentServerRunError("invalid_spec", `managed config for "${activation.alias}" is missing canonical secret references`);
9946
9981
  env["MOLTNET_API_URL"] = activation.apiUrl;
9947
- env["MOLTNET_AGENT_KEY_REF"] = formatSecretReferenceString(config.agent_key_ref);
9982
+ env["MOLTNET_AGENT_KEY_REF"] = formatSecretReferenceString(reference);
9948
9983
  env["MOLTNET_PRIVATE_KEY_REF"] = formatSecretReferenceString(config.keys.private_key_ref);
9949
9984
  env["MOLTNET_SECRET_ROOT"] = this.store.secretsDir;
9950
9985
  } else {
9951
9986
  env["MOLTNET_API_URL"] = activation.apiUrl ?? activation.configApiUrl;
9952
9987
  try {
9953
- const agentKey = await resolveAgentKey(config, this.options.externalSecretProviders);
9988
+ const agentKey = await resolveAgentKey(config, this.options.externalSecretProviders, spec.teamId);
9954
9989
  if (!agentKey) throw new Error("external daemon config has no agent key");
9955
9990
  env["MOLTNET_AGENT_KEY"] = agentKey;
9956
9991
  env["MOLTNET_PRIVATE_KEY"] = await resolveIdentitySeed(config, this.options.externalSecretProviders);
@@ -9990,7 +10025,10 @@ var RunManager = class {
9990
10025
  }
9991
10026
  async startReserved(spec, signal) {
9992
10027
  this.assertStartOpen(signal);
9993
- const agent = await (this.options.verifyActivationImpl ?? verifyAgentActivation)(this.store, spec.agent, this.options.secretProviders, this.options.externalSecretProviders, void 0, signal);
10028
+ const agent = await (this.options.verifyActivationImpl ?? verifyAgentActivation)(this.store, spec.agent, this.options.secretProviders, this.options.externalSecretProviders, void 0, signal, spec.teamId).catch((cause) => {
10029
+ if (cause instanceof AgentServerIdentityError && cause.code === "verification_failed") throw new AgentServerIdentityError(cause.code, `Cannot start agent "${spec.agent}" for team "${spec.teamId}": credential verification failed. Check the selected team key and activation.`, { cause });
10030
+ throw cause;
10031
+ });
9994
10032
  this.assertStartOpen(signal);
9995
10033
  if (agent.boundTeamId && agent.boundTeamId !== spec.teamId) throw new AgentServerRunError("invalid_spec", `agent "${spec.agent}" has a key bound to team ${agent.boundTeamId}; start the run with that team, or create a new agent with an enrollment token from team ${spec.teamId}`);
9996
10034
  const id = `${Date.now().toString(36)}-${randomBytes(4).toString("hex")}`;
@@ -10119,7 +10157,7 @@ var RunManager = class {
10119
10157
  }
10120
10158
  async resolveRuntimeModule(spec, activated, cwd) {
10121
10159
  const profiles = await resolveRuntimeProfiles({
10122
- agent: await this.connectActivatedAgent(activated),
10160
+ agent: await this.connectActivatedAgent(activated, spec.teamId),
10123
10161
  profiles: spec.profiles,
10124
10162
  teamId: spec.teamId,
10125
10163
  cwd
@@ -10141,21 +10179,13 @@ var RunManager = class {
10141
10179
  if (kind === "gondolin_pi") return void 0;
10142
10180
  throw new AgentServerRunError("invalid_spec", `No local runtime is registered for profile kind "${kind}".`);
10143
10181
  }
10144
- async connectActivatedAgent(activated) {
10182
+ async connectActivatedAgent(activated, teamId) {
10145
10183
  const { activation, config } = activated;
10146
- if (activation.source === "managed") {
10147
- const agentKey = await resolveAgentKey(config, this.options.secretProviders);
10148
- if (!agentKey) throw new AgentServerRunError("invalid_spec", `managed agent "${activation.alias}" has no agent key`);
10149
- return connect({
10150
- agentKey,
10151
- apiUrl: activation.apiUrl
10152
- });
10153
- }
10154
- const agentKey = await resolveAgentKey(config, this.options.externalSecretProviders);
10155
- if (!agentKey) throw new AgentServerRunError("invalid_spec", `external agent "${activation.alias}" has no agent key`);
10184
+ const agentKey = await resolveAgentKey(config, activation.source === "managed" ? this.options.secretProviders : this.options.externalSecretProviders, teamId);
10185
+ if (!agentKey) throw new AgentServerRunError("invalid_spec", `agent "${activation.alias}" has no agent key`);
10156
10186
  return connect({
10157
10187
  agentKey,
10158
- apiUrl: activation.apiUrl ?? activation.configApiUrl
10188
+ apiUrl: activation.source === "managed" ? activation.apiUrl : activation.apiUrl ?? activation.configApiUrl
10159
10189
  });
10160
10190
  }
10161
10191
  stop(id) {
@@ -10638,7 +10668,8 @@ var CreateAgentSchema = Type.Union([Type.Object({
10638
10668
  enrollmentToken: Type.String()
10639
10669
  }), Type.Object({
10640
10670
  kind: Type.Literal("external"),
10641
- identityAlias: Type.String()
10671
+ identityAlias: Type.String(),
10672
+ teamId: Type.Optional(Type.String({ format: "uuid" }))
10642
10673
  })]);
10643
10674
  var ReconcileAgentSchema = Type.Object({ action: Type.Union([Type.Literal("resume"), Type.Literal("abandon")]) });
10644
10675
  var ReconcileAgentResultSchema = Type.Union([schemaRef(AgentServerAgentSchema), Type.Object({ abandoned: Type.Literal(true) })], { $id: "ReconcileAgentResult" });
@@ -11151,6 +11182,7 @@ function registerAgentRoutes(app, options, requirePairedOrigin) {
11151
11182
  const entry = await attachExternalAgent(store, options.externalSecretProviders, {
11152
11183
  name: identityAlias,
11153
11184
  configDir: store.identityDir(identityAlias),
11185
+ ...typeof body["teamId"] === "string" ? { teamId: body["teamId"] } : {},
11154
11186
  signal
11155
11187
  });
11156
11188
  return reply.code(201).send(publicAgentView(store, entry.activation));
@@ -11182,7 +11214,7 @@ function identityViews(store) {
11182
11214
  return store.listIdentityAliases().map((alias) => ({
11183
11215
  alias,
11184
11216
  activated: activated.has(alias),
11185
- hasAgentKey: Boolean(store.readAgentConfig(alias)?.agent_key_ref)
11217
+ hasAgentKey: hasAgentKeyConfiguration(store.readAgentConfig(alias) ?? {})
11186
11218
  }));
11187
11219
  }
11188
11220
  function registerProviderRoutes(app, options, requirePairedOrigin) {
@@ -12020,11 +12052,13 @@ async function runSyncSessions(argv) {
12020
12052
  const ctx = await resolveAgentContext(identity.agent, {
12021
12053
  credentialSource: cfg.credentialSource,
12022
12054
  envApiUrl: cfg.apiUrl,
12055
+ teamId: values.team,
12023
12056
  agentRootDir: explicitAgentRootDir
12024
12057
  });
12025
12058
  await validateStartupBinding({
12026
12059
  agent: ctx.agent,
12027
- teamId: values.team
12060
+ teamId: values.team,
12061
+ credentialTeamId: ctx.credentialTeamId
12028
12062
  });
12029
12063
  const stateDirs = ensureDaemonStateDirs(agentRootDir);
12030
12064
  const result = await syncRuntimeSessions({
@@ -12209,7 +12243,7 @@ async function writeCache(cache) {
12209
12243
  }
12210
12244
  //#endregion
12211
12245
  //#region src/version.ts
12212
- var DAEMON_VERSION = "0.59.0";
12246
+ var DAEMON_VERSION = "0.60.0";
12213
12247
  //#endregion
12214
12248
  //#region src/cli.ts
12215
12249
  async function runAgentDaemonCli(options) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@themoltnet/agent-daemon",
3
- "version": "0.59.0",
3
+ "version": "0.60.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": "1.0.3",
89
- "@themoltnet/pi-runtime": "0.18.0",
88
+ "@themoltnet/agent-runtime": "1.1.0",
90
89
  "@themoltnet/os-keyring": "0.3.0",
91
- "@themoltnet/sdk": "0.142.0"
90
+ "@themoltnet/sdk": "0.143.0",
91
+ "@themoltnet/pi-runtime": "0.18.1"
92
92
  },
93
93
  "devDependencies": {
94
94
  "@fastify/swagger": "^9.6.1",
@@ -98,17 +98,17 @@
98
98
  "vite": "^8.0.0",
99
99
  "vite-plugin-dts": "^4.5.4",
100
100
  "vitest": "^3.0.0",
101
+ "@moltnet/api-client": "0.1.0",
101
102
  "@moltnet/agent-eval": "0.1.0",
102
103
  "@moltnet/bootstrap": "0.1.0",
103
104
  "@moltnet/crypto-service": "0.1.0",
104
- "@moltnet/api-client": "0.1.0",
105
105
  "@moltnet/execution-integrations": "0.1.0",
106
106
  "@moltnet/execution-plan": "0.1.0",
107
107
  "@moltnet/loopback-companion": "0.1.0",
108
- "@moltnet/observability": "0.1.0",
109
108
  "@moltnet/runtime-profiles": "0.1.0",
110
- "@moltnet/models": "0.1.0",
111
- "@moltnet/tasks": "0.1.0"
109
+ "@moltnet/observability": "0.1.0",
110
+ "@moltnet/tasks": "0.1.0",
111
+ "@moltnet/models": "0.1.0"
112
112
  },
113
113
  "nx": {
114
114
  "projectType": "application",