@themoltnet/agent-daemon 0.59.0 → 0.61.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 (4) hide show
  1. package/README.md +97 -14
  2. package/dist/cli.js +2098 -819
  3. package/dist/pi.js +16 -1
  4. package/package.json +7 -6
package/dist/cli.js CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import { assertRuntimeAdapterSupportsProfile } from "./runtime.js";
3
- import { a as cryptoService, defaultPiDaemonAdapter, i as compileExecutionPlan, n as createExecutionPlanSnapshot, r as parseCredentialRequirements, t as runtimeExecutionOffer } from "./pi.js";
3
+ import { a as enrollmentProofMessage, defaultPiDaemonAdapter, i as compileExecutionPlan, n as createExecutionPlanSnapshot, o as cryptoService, r as parseCredentialRequirements, t as runtimeExecutionOffer } from "./pi.js";
4
4
  import { createRequire } from "node:module";
5
5
  import { pino, transport } from "pino";
6
6
  import { Type } from "typebox";
@@ -8,14 +8,14 @@ import "multiformats/cid";
8
8
  import "multiformats/codecs/json";
9
9
  import "multiformats/hashes/sha2";
10
10
  import "typebox/value";
11
+ import { execFile, spawn } from "node:child_process";
11
12
  import { basename, dirname, isAbsolute, join, resolve, sep } from "node:path";
12
- import { parseArgs, promisify } from "node:util";
13
- import { AgentRuntime, ApiTaskReporter, ApiTaskSource, PollingApiTaskSource, createLocalSeedSigner, resolveAgentIdentity, resolveRuntimeProfile, resolveRuntimeProfiles, validateRuntimeProfilePrerequisites } from "@themoltnet/agent-runtime";
13
+ import { parseArgs, parseEnv, promisify } from "node:util";
14
+ import { CredentialPersistenceError, EnrollmentRecoveryError, FILE_SECRET_PROVIDER, FileSecretProvider, ProjectConfigError, ProvisioningNotStartedError, RegisterIdentityError, WORKSPACE_STRATEGIES, boundedIdentitySignal, canonicalDirectory, connect, createNodeSecretProviderRegistry, enrollTeam, getProjectConfigPath, readProjectConfig, register, resolveProjectBinding } from "@themoltnet/sdk/node";
15
+ import { constants, copyFileSync, createReadStream, createWriteStream, existsSync, lstatSync, mkdirSync, mkdtempSync, openSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, statSync, symlinkSync, writeFileSync } from "node:fs";
16
+ import { AuthenticationError, IDENTITY_ALIAS_PATTERN, MoltNetError, assertIdentityAlias, assertTrustedConfigApiUrl, createExecutorAttestor, formatSecretReferenceString, getConfigDir, getIdentityDir, hasAgentKeyConfiguration, isCanonicalConfig, parseSecretReferenceString, readConfig, requireSecureCredentialApiUrl, resolveAgentKey, resolveEnvSecretReference, resolveIdentitySeed, selectAgentKeyReference, signBytes } from "@themoltnet/sdk";
17
+ import { AgentRuntime, ApiTaskReporter, ApiTaskSource, PollingApiTaskSource, RuntimeProfilePrerequisiteError, createLocalSeedSigner, resolveAgentIdentity, resolveRuntimeProfile, resolveRuntimeProfiles, validateRuntimeProfilePrerequisites } from "@themoltnet/agent-runtime";
14
18
  import { GuestEnvironmentBoundaryError, assertGuestEnvironmentBoundary, createPiRetryTriage, findMainWorktree, isResolvedPathInsideRoot, normalizeRetryTriageResult, redactRetryTriageSecrets, resolveRuntimeProfileModel } from "@themoltnet/pi-runtime";
15
- import { FILE_SECRET_PROVIDER, FileSecretProvider, RegisterIdentityError, boundedIdentitySignal, connect, createNodeSecretProviderRegistry, register } from "@themoltnet/sdk/node";
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";
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";
@@ -28,8 +28,8 @@ import { NodeTracerProvider } from "@opentelemetry/sdk-trace-node";
28
28
  import { ATTR_SERVICE_NAME, ATTR_SERVICE_VERSION } from "@opentelemetry/semantic-conventions";
29
29
  import { homedir, platform, tmpdir } from "node:os";
30
30
  import { PI_MODEL_MODALITIES, writePiConfig } from "@themoltnet/pi-runtime/pi-config";
31
+ import { mkdir, open, readFile, readdir, realpath, rm, stat, writeFile } from "node:fs/promises";
31
32
  import { AsyncLocalStorage } from "node:async_hooks";
32
- import { mkdir, open, readFile, realpath, stat, writeFile } from "node:fs/promises";
33
33
  import { pipeline } from "node:stream/promises";
34
34
  import { createInterface } from "node:readline/promises";
35
35
  import { ModelRuntime, readStoredCredential } from "@earendil-works/pi-coding-agent";
@@ -38,6 +38,8 @@ import { lock, lockSync } from "proper-lockfile";
38
38
  import { isIP } from "node:net";
39
39
  import cors from "@fastify/cors";
40
40
  import helmet from "@fastify/helmet";
41
+ import { createServer } from "node:http";
42
+ import { createRemoteJWKSet, jwtVerify } from "jose";
41
43
  import { Transform, Writable } from "node:stream";
42
44
  import { pathToFileURL } from "node:url";
43
45
  import { StringDecoder } from "node:string_decoder";
@@ -774,13 +776,24 @@ var CREDENTIAL_SCOPES = {
774
776
  TaskManage: "task:manage",
775
777
  TaskRead: "task:read",
776
778
  TaskWrite: "task:write",
779
+ TeamJoin: "team:join",
777
780
  TeamManage: "team:manage",
778
781
  TeamRead: "team:read"
779
782
  };
780
783
  var ALL_CREDENTIAL_SCOPES = Object.freeze(Object.values(CREDENTIAL_SCOPES));
781
784
  /**
782
- * Minimum grant for the agent daemon. Task credentials attenuate this further
783
- * to `task:execute` alone.
785
+ * What the agent daemon cannot run without, checked against
786
+ * `GET /agents/whoami` at startup. Task credentials attenuate it further to
787
+ * `task:execute` alone.
788
+ *
789
+ * This is the **boot floor**, and deliberately not the same list as
790
+ * `AGENT_CREDENTIAL_SCOPES`. A credential's scopes are fixed when it is minted
791
+ * and `POST /agent-keys` caps a new key at the scopes of the credential
792
+ * requesting it, so no key can ever widen itself. A scope added here therefore
793
+ * stops every daemon already in the field, and only a human with a Console
794
+ * session can mint the replacement. Add one only when the daemon genuinely
795
+ * cannot work without it; anything a caller merely benefits from belongs in
796
+ * `DAEMON_OPTIONAL_SCOPES`, where absence costs a capability instead.
784
797
  *
785
798
  * `crypto:sign` is part of the minimum because host-capability signing runs on
786
799
  * the daemon's own credential: the local seed signer calls the signing-request
@@ -788,7 +801,7 @@ var ALL_CREDENTIAL_SCOPES = Object.freeze(Object.values(CREDENTIAL_SCOPES));
788
801
  * cleanly and then fails the first time guest code signs a diary entry or a
789
802
  * commit.
790
803
  */
791
- var AGENT_CREDENTIAL_SCOPES = [
804
+ var DAEMON_MINIMUM_SCOPES = [
792
805
  CREDENTIAL_SCOPES.AgentProfile,
793
806
  CREDENTIAL_SCOPES.CryptoSign,
794
807
  CREDENTIAL_SCOPES.RuntimeRead,
@@ -796,6 +809,22 @@ var AGENT_CREDENTIAL_SCOPES = [
796
809
  CREDENTIAL_SCOPES.TaskClaim,
797
810
  CREDENTIAL_SCOPES.TaskExecute
798
811
  ];
812
+ /**
813
+ * Read and enrollment authority a daemon uses when it has it, and runs without
814
+ * when it does not: reading the teams it belongs to and their diaries, and
815
+ * joining a team it is not yet a member of.
816
+ *
817
+ * Which product surface each one enables is deliberately not recorded here.
818
+ * That mapping belongs to whatever consumes the scope and changes with it,
819
+ * while the scope names are the contract and do not.
820
+ */
821
+ var DAEMON_OPTIONAL_SCOPES = [
822
+ CREDENTIAL_SCOPES.DiaryRead,
823
+ CREDENTIAL_SCOPES.TeamRead,
824
+ CREDENTIAL_SCOPES.TeamJoin
825
+ ];
826
+ /** What a newly issued agent key should carry: the floor plus the rest. */
827
+ var AGENT_CREDENTIAL_SCOPES = [...DAEMON_MINIMUM_SCOPES, ...DAEMON_OPTIONAL_SCOPES];
799
828
  CREDENTIAL_SCOPES.AgentProfile, CREDENTIAL_SCOPES.TaskRead, CREDENTIAL_SCOPES.TaskWrite;
800
829
  CREDENTIAL_SCOPES.AgentProfile, CREDENTIAL_SCOPES.DiaryRead, CREDENTIAL_SCOPES.PackRead, CREDENTIAL_SCOPES.RuntimeRead, CREDENTIAL_SCOPES.TaskRead, CREDENTIAL_SCOPES.TeamRead;
801
830
  Object.freeze(ALL_CREDENTIAL_SCOPES.filter((scope) => scope !== CREDENTIAL_SCOPES.HumanProfile));
@@ -819,6 +848,7 @@ var MCP_CLIENT_SCOPES = [
819
848
  CREDENTIAL_SCOPES.TaskManage,
820
849
  CREDENTIAL_SCOPES.TaskRead,
821
850
  CREDENTIAL_SCOPES.TaskWrite,
851
+ CREDENTIAL_SCOPES.TeamJoin,
822
852
  CREDENTIAL_SCOPES.TeamManage,
823
853
  CREDENTIAL_SCOPES.TeamRead
824
854
  ];
@@ -829,6 +859,23 @@ Object.freeze([...[
829
859
  "offline_access"
830
860
  ], ...MCP_CLIENT_SCOPES]);
831
861
  //#endregion
862
+ //#region ../../libs/models/src/operator-oauth.ts
863
+ /** Public protocol constants shared by consent, native control, and Console. */
864
+ var OPERATOR_OAUTH = Object.freeze({
865
+ protocolVersion: 2,
866
+ provisioningScope: "moltnet:provision",
867
+ localControlScope: "moltnet:local-control",
868
+ provisioningAudience: "moltnet:provisioning",
869
+ localControlAudience: "moltnet:agent-server",
870
+ nativeClientId: "moltnet-native",
871
+ consoleClientId: "moltnet-console",
872
+ approvalTransportGraceSeconds: 30,
873
+ callbackPort: 17375,
874
+ consoleLifetimeSeconds: 900,
875
+ nativeLifetimeSeconds: 300,
876
+ serverPort: 17374
877
+ });
878
+ //#endregion
832
879
  //#region ../../libs/models/src/preview-sign.ts
833
880
  function schemaRef$2(schema, id) {
834
881
  return Type.Unsafe(Type.Ref(id));
@@ -1172,17 +1219,17 @@ Type.Object({
1172
1219
  Type.Literal("executor"),
1173
1220
  Type.Literal("member")
1174
1221
  ])),
1175
- maxUses: Type.Optional(Type.Integer({
1176
- minimum: 1,
1177
- default: 1
1178
- })),
1179
1222
  expiresInHours: Type.Optional(Type.Integer({
1180
1223
  minimum: 1,
1181
1224
  maximum: 720,
1182
1225
  default: 168
1183
1226
  }))
1184
1227
  });
1185
- Type.Object({ code: Type.String({ minLength: 1 }) });
1228
+ Type.Object({
1229
+ code: Type.String({ minLength: 1 }),
1230
+ issueAgentKey: Type.Optional(Type.Literal(true)),
1231
+ expectedTeamId: Type.Optional(UuidSchema)
1232
+ });
1186
1233
  Type.Object({ role: Type.Union([
1187
1234
  Type.Literal("manager"),
1188
1235
  Type.Literal("executor"),
@@ -1207,8 +1254,7 @@ Type.Object({
1207
1254
  Type.Literal("executor"),
1208
1255
  Type.Literal("member")
1209
1256
  ]),
1210
- maxUses: Type.Integer(),
1211
- useCount: Type.Integer(),
1257
+ usedAt: Type.Union([Type.String({ format: "date-time" }), Type.Null()]),
1212
1258
  expiresAt: DateTimeUnsafe,
1213
1259
  createdAt: DateTimeUnsafe
1214
1260
  });
@@ -1239,11 +1285,7 @@ Type.Object({
1239
1285
  });
1240
1286
  Type.Object({
1241
1287
  teamId: UuidSchema,
1242
- role: Type.Union([
1243
- Type.Literal("manager"),
1244
- Type.Literal("executor"),
1245
- Type.Literal("member")
1246
- ])
1288
+ role: TeamRoleSchema
1247
1289
  });
1248
1290
  Type.Object({
1249
1291
  updated: Type.Boolean(),
@@ -1427,6 +1469,7 @@ var ProblemCodeSchema = Type.Union([
1427
1469
  Type.Literal("FORBIDDEN"),
1428
1470
  Type.Literal("NOT_FOUND"),
1429
1471
  Type.Literal("CONFLICT"),
1472
+ Type.Literal("PROJECT_MISMATCH"),
1430
1473
  Type.Literal("UNSUPPORTED_MEDIA_TYPE"),
1431
1474
  Type.Literal("VALIDATION_FAILED"),
1432
1475
  Type.Literal("INVALID_CHALLENGE"),
@@ -1507,6 +1550,34 @@ Type.Intersect([ConflictProblemDetailsSchema, Type.Object({ flagged: Type.Option
1507
1550
  threats: Type.Array(Type.Ref("InjectionThreat"))
1508
1551
  }, { additionalProperties: false }))) })], { $id: "InjectionConflictProblemDetails" });
1509
1552
  Type.Intersect([ProblemDetailsSchema, Type.Object({ errors: Type.Array(Type.Ref("ValidationError")) })], { $id: "ValidationProblemDetails" });
1553
+ Type.Object({
1554
+ id: Type.String({ format: "uuid" }),
1555
+ teamId: Type.String({ format: "uuid" }),
1556
+ creatorAgentId: Type.Union([Type.String({ format: "uuid" }), Type.Null()]),
1557
+ creatorHumanId: Type.Union([Type.String({ format: "uuid" }), Type.Null()]),
1558
+ name: Type.String(),
1559
+ description: Type.Union([Type.String(), Type.Null()]),
1560
+ defaultDiaryId: Type.Union([Type.String({ format: "uuid" }), Type.Null()]),
1561
+ archived: Type.Boolean(),
1562
+ createdAt: Type.String({ format: "date-time" }),
1563
+ updatedAt: Type.String({ format: "date-time" })
1564
+ });
1565
+ var CreateProjectSchema = Type.Object({
1566
+ name: Type.String({
1567
+ minLength: 1,
1568
+ maxLength: 255,
1569
+ pattern: "\\S"
1570
+ }),
1571
+ description: Type.Optional(Type.Union([Type.String({ maxLength: 1e4 }), Type.Null()])),
1572
+ defaultDiaryId: Type.Optional(Type.Union([Type.String({ format: "uuid" }), Type.Null()]))
1573
+ }, { additionalProperties: false });
1574
+ Type.Object({
1575
+ ...Type.Partial(CreateProjectSchema).properties,
1576
+ archived: Type.Optional(Type.Boolean())
1577
+ }, {
1578
+ additionalProperties: false,
1579
+ minProperties: 1
1580
+ });
1510
1581
  Type.Union([
1511
1582
  Type.Literal("pack"),
1512
1583
  Type.Literal("entry"),
@@ -1945,7 +2016,7 @@ var RuntimeProfileMaxBashTimeouts = Type.Integer({
1945
2016
  minimum: 0,
1946
2017
  maximum: 1e3
1947
2018
  });
1948
- Type.Object({
2019
+ var RuntimeProfile = Type.Object({
1949
2020
  id: Type.String({ format: "uuid" }),
1950
2021
  teamId: Type.String({ format: "uuid" }),
1951
2022
  name: RuntimeProfileName,
@@ -3331,6 +3402,7 @@ Type.Object({
3331
3402
  title: Type.Union([Type.String(), Type.Null()]),
3332
3403
  tags: Type.Array(Type.String()),
3333
3404
  teamId: Uuid,
3405
+ projectId: Type.Union([Uuid, Type.Null()]),
3334
3406
  diaryId: Type.Union([Uuid, Type.Null()]),
3335
3407
  outputKind: OutputKind,
3336
3408
  input: Type.Record(Type.String(), Type.Unknown()),
@@ -3439,278 +3511,32 @@ Type.Object({
3439
3511
  additionalProperties: false
3440
3512
  });
3441
3513
  //#endregion
3442
- //#region src/lib/help.ts
3443
- var COMMON_REQUIRED_FLAGS = `\
3444
- -a, --agent <name> MoltNet agent identity. Agent-key auth is
3445
- configless; OAuth2 reads moltnet.json.
3446
- --profile <uuid|name> Remote runtime profile. Repeat for poll/drain
3447
- to declare priority order. Provider, model,
3448
- sandbox policy, prerequisites, and runtime
3449
- execution policy come from the selected profile.`;
3450
- var COMMON_OPTIONAL_FLAGS = `\
3451
- --sandbox <path> Deprecated. Remote runtime profiles define
3452
- sandbox policy.
3453
- --agent-root <path> Directory that owns .moltnet/<agent>. Default:
3454
- CWD, with git root fallback when available.
3455
- --git-author <"Name <email>">
3456
- Non-secret git identity projected into the
3457
- guest for host-brokered commit signing. Default:
3458
- host git config. Configless agent-key runs must
3459
- provide this flag or MOLTNET_GIT_AUTHOR.
3460
- Env: MOLTNET_GIT_AUTHOR.
3461
- --heartbeat-interval-ms <n> Reporter heartbeat cadence. Default: 60000.
3462
- --warm-retention-sec <n> Resumability window for runtime slots
3463
- (Pi sessions + reusable worktrees) after use.
3464
- Default: 1800.
3465
- --debug Verbose logging: also log successful list/claim
3466
- outcomes (candidate counts, claim attempts).`;
3467
- var REGISTERED_TASK_TYPES = Object.keys(BUILT_IN_TASK_TYPES).sort();
3468
- function knownTaskTypesList() {
3469
- return REGISTERED_TASK_TYPES.join(", ");
3514
+ //#region src/lib/identity-pin.ts
3515
+ /** Compare every pinned field without choosing a caller-specific error type. */
3516
+ function assessIdentityPin(current, expected) {
3517
+ for (const [field, label] of [["publicKey", "public key"], ["fingerprint", "fingerprint"]]) if (!current[field] || current[field] !== expected[field]) return {
3518
+ ok: false,
3519
+ field,
3520
+ label
3521
+ };
3522
+ return { ok: true };
3470
3523
  }
3471
- var ROOT_USAGE = `\
3472
- agent-daemon long-running task worker for MoltNet.
3473
-
3474
- Usage: agent-daemon [--runtime <module>] <command> [...flags]
3475
-
3476
- Runtime:
3477
- --runtime <module> Trusted local file or installed package whose
3478
- default export is a DaemonRuntimeAdapter.
3479
- Omit to use the built-in gondolin_pi runtime.
3480
-
3481
- Commands:
3482
- poll Long-running worker. Polls the task queue and claims tasks
3483
- matching the configured filter until SIGINT/SIGTERM.
3484
- once Claim and execute one specific queued task by id, then exit.
3485
- drain Poll until the queue has nothing claimable, then exit.
3486
- Useful for batch eval runs and demos.
3487
- server Loopback supervisor for console-managed runs: pairing,
3488
- agent/provider config store, and start/stop of poll/drain
3489
- child processes. Binds 127.0.0.1 only.
3490
- server trust
3491
- Install the per-user macOS local-HTTPS CA after explicit consent.
3492
- providers Manage configured endpoints and Pi OAuth subscriptions without
3493
- starting the Agent Server. See \`agent-daemon providers --help\`.
3494
- sync-sessions
3495
- Repair durable runtime-session checkpoints from local slot files.
3496
- update check
3497
- Check the stable MoltNet agent release without reading credentials.
3498
-
3499
- Run \`agent-daemon <command> --help\` for command-specific flags.
3500
-
3501
- Prerequisites:
3502
- - configless: MOLTNET_AGENT_KEY (or MOLTNET_AGENT_KEY_REF) and
3503
- 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)
3506
-
3507
- No key yet? Mint one with the CLI (--store writes agent_key_ref into
3508
- moltnet.json and keeps the secret in a provider):
3509
-
3510
- moltnet teams list # find the team id
3511
- moltnet agents keys create --team-id <team-uuid> \\
3512
- --name <agent>-daemon --store
3513
-
3514
- https://docs.themolt.net/operate/agent-keys#run-the-daemon-with-an-agent-key
3515
- - --profile — remote runtime profile supplies provider/model/sandbox
3516
- policy and CWD is used as the VM mountPath.
3517
-
3518
- Registered task types: ${knownTaskTypesList()}`;
3519
- var POLL_HELP = `\
3520
- agent-daemon poll — long-running task worker.
3521
-
3522
- Usage:
3523
- agent-daemon poll --team <uuid> --agent <name> --profile <uuid|name> [...]
3524
-
3525
- Required:
3526
- --team <uuid> Team whose queue to serve. The daemon must be
3527
- a member of this team (canAccessTeam permit).
3528
- ${COMMON_REQUIRED_FLAGS}
3529
-
3530
- Optional:
3531
- --task-types <csv> Whitelist of task types to claim. Default:
3532
- accept any registered type. Known types:
3533
- ${knownTaskTypesList()}
3534
- --correlation-id <uuid> Restrict claims to one orchestration run.
3535
- --diary-ids <csv> Further client-side filter on task.diaryId.
3536
- --poll-interval-ms <n> Idle backoff floor. Default: 2000.
3537
- --max-poll-interval-ms <n> Idle backoff ceiling. Default: 30000.
3538
- --list-limit <n> Page size per list call. Default: 10.
3539
- ${COMMON_OPTIONAL_FLAGS}
3540
-
3541
- Example:
3542
- agent-daemon poll \\
3543
- --team 6743b4b1-6b93-46e2-a048-19490f04f91a \\
3544
- --task-types curate_pack,fulfill_brief \\
3545
- --agent legreffier \\
3546
- --profile github-linear \\
3547
- --profile local-fallback
3548
-
3549
- Stops cleanly on SIGINT/SIGTERM (drains the in-flight task before exit).`;
3550
- var ONCE_HELP = `\
3551
- agent-daemon once — execute one specific queued task by id, then exit.
3552
-
3553
- Usage:
3554
- agent-daemon once --task-id <uuid> --agent <name> --profile <uuid|name> [...]
3555
-
3556
- Required:
3557
- -t, --task-id <uuid> Task to claim and execute. Must already be
3558
- in 'queued' status.
3559
- ${COMMON_REQUIRED_FLAGS}
3560
-
3561
- Optional:
3562
- --team <uuid> Team scope for resolving --profile by name.
3563
- Required only when --profile is a name.
3564
- ${COMMON_OPTIONAL_FLAGS}
3565
-
3566
- Example:
3567
- agent-daemon once \\
3568
- --task-id 26004a77-bc10-43ef-a79f-c8e62faf59b1 \\
3569
- --agent legreffier \\
3570
- --profile github-linear
3571
-
3572
- Exits 0 on completed, 1 on failed/cancelled/runtime-error.`;
3573
- var DRAIN_HELP = `\
3574
- agent-daemon drain — poll until the queue is empty, then exit.
3575
-
3576
- Usage:
3577
- agent-daemon drain --team <uuid> --agent <name> --profile <uuid|name> [...]
3578
-
3579
- Same flags as \`poll\`. The only behavioural difference: \`drain\` exits
3580
- when a list call confirms no claimable tasks remain (vs \`poll\` which
3581
- sleeps and retries forever).
3582
-
3583
- Required:
3584
- --team <uuid> Team whose queue to drain.
3585
- ${COMMON_REQUIRED_FLAGS}
3586
-
3587
- Optional:
3588
- --task-types <csv> Whitelist. Known types: ${knownTaskTypesList()}
3589
- --correlation-id <uuid> Restrict claims to one orchestration run.
3590
- --wait-for-first-task-sec <n>
3591
- Wait this long for an initially empty run before
3592
- exiting. After the first claim, exit on empty.
3593
- --wait-after-task-sec <n> Require the queue to remain empty for this long
3594
- after a claim before exiting.
3595
- --diary-ids <csv> Diary filter.
3596
- --poll-interval-ms <n> Default: 2000.
3597
- --max-poll-interval-ms <n> Default: 30000.
3598
- --list-limit <n> Default: 10.
3599
- ${COMMON_OPTIONAL_FLAGS}
3600
-
3601
- Example:
3602
- agent-daemon drain \\
3603
- --team 6743b4b1-6b93-46e2-a048-19490f04f91a \\
3604
- --task-types judge_pack \\
3605
- --agent legreffier \\
3606
- --profile eval-judge`;
3607
- var SYNC_SESSIONS_HELP = `\
3608
- agent-daemon sync-sessions — repair durable runtime-session checkpoints.
3609
-
3610
- Usage:
3611
- agent-daemon sync-sessions --team <uuid> --agent <name> [...]
3612
-
3613
- Scans this daemon's team-scoped runtime slots, compares local Pi session files
3614
- with durable runtime-session metadata, and uploads missing or stale checkpoints.
3615
-
3616
- Required:
3617
- --team <uuid> Team whose runtime slots to inspect.
3618
- -a, --agent <name> MoltNet agent identity. Reads credentials
3619
- from <agent-root>/.moltnet/<name>/moltnet.json.
3620
-
3621
- Optional:
3622
- --runtime-profile-id <uuid> Limit repair to one runtime profile.
3623
- --state <active|idle> Limit scanned slots by state. Default: all.
3624
- --limit <n> Max slots to scan, 1..200. Default: 100.
3625
- --dry-run Report missing/stale sessions without uploading.
3626
- --agent-root <path> Directory that owns .moltnet/<agent>. Default:
3627
- CWD, with git root fallback when available.
3628
- --debug Accepted for consistency; no extra output yet.
3629
-
3630
- Example:
3631
- agent-daemon sync-sessions \\
3632
- --team 6743b4b1-6b93-46e2-a048-19490f04f91a \\
3633
- --agent legreffier \\
3634
- --state idle`;
3635
- function isHelpFlag(args) {
3636
- return args.includes("--help") || args.includes("-h");
3524
+ function assessAgentStartupPin(current, expected) {
3525
+ for (const [field, label] of [
3526
+ ["subjectId", "subject id"],
3527
+ ["subjectType", "subject type"],
3528
+ ["publicKey", "public key"],
3529
+ ["fingerprint", "fingerprint"]
3530
+ ]) if (!current[field] || current[field] !== expected[field]) return {
3531
+ ok: false,
3532
+ field,
3533
+ label
3534
+ };
3535
+ return { ok: true };
3637
3536
  }
3638
- var AGENT_SERVER_HELP = `\
3639
- agent-daemon server loopback supervisor for console-managed runs.
3640
-
3641
- Binds 127.0.0.1 only. A paired Console origin configures agents and
3642
- providers (secret references only) and starts/stops poll/drain runs as
3643
- child processes of this supervisor.
3644
-
3645
- Options:
3646
- --port <n> Loopback port. Default: 17374.
3647
- Env: MOLTNET_AGENT_SERVER_PORT.
3648
- --allowed-origins <csv> Exact Console origins allowed to pair.
3649
- Default: https://console.themolt.net.
3650
- Env: MOLTNET_AGENT_SERVER_ALLOWED_ORIGINS.
3651
- --root <path> Config root. Default: ~/.config/moltnet
3652
- (or MOLTNET_AGENT_SERVER_ROOT).
3653
- --api-url <url> Default MoltNet API for new managed agents.
3654
- Default: https://api.themolt.net.
3655
- --heartbeat-interval-ms <n> Child reporter heartbeat cadence. Default: 60000.
3656
- --warm-retention-sec <n> Child session/workspace retention. Default: 1800.
3657
- --supervised Also stop gracefully when stdin reaches EOF.
3658
-
3659
- On macOS, the first interactive run asks to trust a per-user local CA in the
3660
- login keychain and serves HTTPS. Native supervisors use:
3661
- server trust --status --json
3662
- server trust --yes --json
3663
- server trust --remove --yes --json
3664
- Run \`agent-daemon server trust --remove\` interactively to remove that exact
3665
- CA. Linux continues to use the Chromium PNA HTTP path.
3666
- `;
3667
- var PROVIDERS_HELP = `\
3668
- moltnet-agent providers — manage local model providers.
3669
-
3670
- Usage:
3671
- moltnet-agent providers list [--json] [--root <path>]
3672
- moltnet-agent providers set <id> [--base-url <url>] [--api <pi-api-kind>]
3673
- [--model <id> ... | --clear-models]
3674
- [--model-input <id>=text,image ...]
3675
- [--api-key-stdin | --clear-api-key] [--root <path>]
3676
- moltnet-agent providers discover <id> [--save] [--json] [--root <path>]
3677
- moltnet-agent providers remove <id> [--yes] [--root <path>]
3678
- moltnet-agent providers login <id> [--auth-method <method-id>]
3679
- [--root <path>]
3680
- moltnet-agent providers logout <id> [--yes] [--root <path>]
3681
-
3682
- The default root is ~/.config/moltnet. MOLTNET_AGENT_SERVER_ROOT remains the
3683
- environment override. API keys are accepted only from redirected stdin; they
3684
- are stored separately and providers.json contains only a secret reference.
3685
-
3686
- --model declares a text-only model. --model-input declares a model together
3687
- with the input modalities it accepts, and is what makes a vision model usable:
3688
- a model with no declared modalities is text-only to Pi, which drops image
3689
- content parts before the request leaves the runtime.
3690
- `;
3691
- //#endregion
3692
- //#region src/lib/identity-pin.ts
3693
- /** Compare every pinned field without choosing a caller-specific error type. */
3694
- function assessIdentityPin(current, expected) {
3695
- for (const [field, label] of [["publicKey", "public key"], ["fingerprint", "fingerprint"]]) if (!current[field] || current[field] !== expected[field]) return {
3696
- ok: false,
3697
- field,
3698
- label
3699
- };
3700
- return { ok: true };
3701
- }
3702
- function assessAgentStartupPin(current, expected) {
3703
- for (const [field, label] of [
3704
- ["subjectId", "subject id"],
3705
- ["subjectType", "subject type"],
3706
- ["publicKey", "public key"],
3707
- ["fingerprint", "fingerprint"]
3708
- ]) if (!current[field] || current[field] !== expected[field]) return {
3709
- ok: false,
3710
- field,
3711
- label
3712
- };
3713
- return { ok: true };
3537
+ /** A configured team slot must authenticate with that exact team ceiling. */
3538
+ function matchesCredentialTeam(current, credentialTeamId) {
3539
+ return !credentialTeamId || current.credentialBinding?.bindingScope === "team" && current.credentialBinding.boundTeamId === credentialTeamId;
3714
3540
  }
3715
3541
  //#endregion
3716
3542
  //#region src/lib/agent-context.ts
@@ -3721,7 +3547,7 @@ function assessAgentStartupPin(current, expected) {
3721
3547
  * `docs/.vitepress/config.ts`.
3722
3548
  */
3723
3549
  var AGENT_KEYS_DOC_URL = "https://docs.themolt.net/operate/agent-keys#run-the-daemon-with-an-agent-key";
3724
- /** Scopes a daemon key must carry; mirrors `DAEMON_REQUIRED_SCOPES`. */
3550
+ /** Scopes a new daemon key should be minted with; mirrors `DAEMON_RECOMMENDED_SCOPES`. */
3725
3551
  var DAEMON_KEY_SCOPES = AGENT_CREDENTIAL_SCOPES;
3726
3552
  /**
3727
3553
  * Report where `connect()` will find the key, without ever reading the secret
@@ -3755,7 +3581,7 @@ function detectCredentialSource(env) {
3755
3581
  function assessStartupBinding(whoami, teamId) {
3756
3582
  if (whoami.subjectType !== "agent") return {
3757
3583
  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).`
3584
+ reason: `the daemon must authenticate as an agent, but whoami reported subjectType "${whoami.subjectType}". Provide agent credentials (an agent key).`
3759
3585
  };
3760
3586
  const boundTeamId = whoami.credentialBinding?.bindingScope === "team" ? whoami.credentialBinding.boundTeamId : void 0;
3761
3587
  if (teamId && boundTeamId && boundTeamId !== teamId) return {
@@ -3768,8 +3594,7 @@ function assessStartupBinding(whoami, teamId) {
3768
3594
  * Validate at startup — after `connect()`, before polling — that the connected
3769
3595
  * credential can operate as `teamId`, failing fast with an actionable message
3770
3596
  * 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
3597
+ * credential sources and also checks API reachability and subject type. Returns the `whoami` so the caller can log the resolved
3773
3598
  * identity (never the secret).
3774
3599
  */
3775
3600
  async function validateStartupBinding(options) {
@@ -3785,6 +3610,7 @@ async function validateStartupBinding(options) {
3785
3610
  setTimeout(resolve, 100 * attempt);
3786
3611
  });
3787
3612
  }
3613
+ if (!matchesCredentialTeam(whoami, options.credentialTeamId)) throw new Error("Daemon startup validation failed: selected team credential has a different binding.");
3788
3614
  const assessment = assessStartupBinding(whoami, options.teamId);
3789
3615
  if (!assessment.ok) throw new Error(`Daemon startup validation failed: ${assessment.reason}`);
3790
3616
  const expected = options.expectedAgent;
@@ -3800,16 +3626,26 @@ async function validateStartupBinding(options) {
3800
3626
  async function resolveAgentContext(agentName, options = {}) {
3801
3627
  assertIdentityAlias(agentName);
3802
3628
  const { agentDir, agentRootDir } = resolveIdentityLocation(agentName, options.agentRootDir, { requireConfig: options.credentialSource !== "environment" });
3803
- if (options.credentialSource === "environment") return {
3804
- agentDir,
3805
- agentRootDir,
3806
- agent: await connect({ secretProviders: createNodeSecretProviderRegistry() }),
3807
- credentialSource: "environment"
3808
- };
3629
+ const projectApiUrl = options.projectApiUrl;
3630
+ if (options.credentialSource === "environment") {
3631
+ if (projectApiUrl) assertTrustedConfigApiUrl(projectApiUrl, options.envApiUrl?.trim() || "https://api.themolt.net");
3632
+ return {
3633
+ agentDir,
3634
+ agentRootDir,
3635
+ agent: await connect({
3636
+ ...projectApiUrl ? { apiUrl: projectApiUrl } : {},
3637
+ secretProviders: createNodeSecretProviderRegistry()
3638
+ }),
3639
+ credentialSource: "environment"
3640
+ };
3641
+ }
3809
3642
  const config = await readConfig(agentDir);
3810
- if (!config?.agent_key_ref) throw new Error(agentKeyRequiredMessage(agentDir, agentName));
3643
+ if (!config || !hasAgentKeyConfiguration(config)) throw new Error(agentKeyRequiredMessage(agentDir, agentName));
3644
+ const configuredApiUrl = resolveConfigApiUrl(config, options.envApiUrl);
3645
+ if (options.envApiUrl?.trim()) requireSecureCredentialApiUrl(options.envApiUrl);
3646
+ if (projectApiUrl) assertTrustedConfigApiUrl(projectApiUrl, options.envApiUrl?.trim() || configuredApiUrl || "https://api.themolt.net");
3811
3647
  const secretProviders = createNodeSecretProviderRegistry();
3812
- const agentKey = await resolveAgentKey(config, secretProviders);
3648
+ const agentKey = await resolveAgentKey(config, secretProviders, options.teamId);
3813
3649
  if (!agentKey) throw new Error(agentKeyRequiredMessage(agentDir, agentName));
3814
3650
  return {
3815
3651
  agentDir,
@@ -3818,9 +3654,10 @@ async function resolveAgentContext(agentName, options = {}) {
3818
3654
  configDir: agentDir,
3819
3655
  secretProviders,
3820
3656
  agentKey,
3821
- apiUrl: resolveConfigApiUrl(config, options.envApiUrl)
3657
+ apiUrl: projectApiUrl ?? configuredApiUrl
3822
3658
  }),
3823
- credentialSource: "config"
3659
+ credentialSource: "config",
3660
+ credentialTeamId: selectAgentKeyReference(config, options.teamId)?.teamId
3824
3661
  };
3825
3662
  }
3826
3663
  /**
@@ -3831,7 +3668,7 @@ async function resolveAgentContext(agentName, options = {}) {
3831
3668
  */
3832
3669
  function agentKeyRequiredMessage(agentDir, agentName) {
3833
3670
  return [
3834
- `${join(agentDir, "moltnet.json")} has no "agent_key_ref". The daemon`,
3671
+ `${join(agentDir, "moltnet.json")} has no "agent_key_ref" or "agent_key_refs". The daemon`,
3835
3672
  `requires a team-bound agent key; OAuth2 client_credentials is no longer`,
3836
3673
  `accepted.`,
3837
3674
  ``,
@@ -3844,7 +3681,7 @@ function agentKeyRequiredMessage(agentDir, agentName) {
3844
3681
  ` --scopes ${DAEMON_KEY_SCOPES.join(",")} \\`,
3845
3682
  ` --store`,
3846
3683
  ``,
3847
- `--store writes "agent_key_ref" into moltnet.json and keeps the secret in`,
3684
+ `--store writes the team slot in "agent_key_refs" and keeps the secret in`,
3848
3685
  `a provider, so the key itself never lands in the file. Omit --scopes to`,
3849
3686
  `get the same daemon minimum by default.`,
3850
3687
  ``,
@@ -3906,6 +3743,13 @@ function isTransientWhoamiError(error) {
3906
3743
  const statusCode = error.statusCode;
3907
3744
  return typeof statusCode === "number" && (statusCode === 408 || statusCode === 429 || statusCode >= 500);
3908
3745
  }
3746
+ /** Read only non-secret endpoint metadata before selecting a team credential. */
3747
+ async function resolveSelectionApiUrl(agentName, options) {
3748
+ if (options.envApiUrl?.trim()) return options.envApiUrl.trim();
3749
+ if (options.credentialSource === "environment") throw new Error("Set MOLTNET_API_URL for an environment-key worker");
3750
+ const { agentDir } = resolveIdentityLocation(agentName, options.agentRootDir, { requireConfig: true });
3751
+ return resolveConfigApiUrl(await readConfig(agentDir) ?? {}) ?? "https://api.themolt.net";
3752
+ }
3909
3753
  //#endregion
3910
3754
  //#region src/config.ts
3911
3755
  /**
@@ -3974,7 +3818,19 @@ function activatePiCodingAgentDir(path, env = {}) {
3974
3818
  Object.assign(process.env, env);
3975
3819
  }
3976
3820
  function loadAgentServerEnvConfig() {
3821
+ const issuer = process.env["MOLTNET_OPERATOR_OAUTH_ISSUER"];
3822
+ const publicUrl = process.env["MOLTNET_OPERATOR_OAUTH_PUBLIC_URL"] ?? issuer;
3823
+ const nativeClientId = process.env["MOLTNET_NATIVE_OAUTH_CLIENT_ID"];
3824
+ const consoleClientId = process.env["MOLTNET_CONSOLE_OAUTH_CLIENT_ID"];
3825
+ const apiUrl = process.env["MOLTNET_OPERATOR_API_URL"];
3977
3826
  return {
3827
+ operatorOAuth: {
3828
+ ...issuer ? { issuer } : {},
3829
+ ...publicUrl ? { publicUrl } : {},
3830
+ ...nativeClientId ? { nativeClientId } : {},
3831
+ ...consoleClientId ? { consoleClientId } : {},
3832
+ ...apiUrl ? { apiUrl } : {}
3833
+ },
3978
3834
  port: process.env["MOLTNET_AGENT_SERVER_PORT"] ?? "",
3979
3835
  allowedOrigins: process.env["MOLTNET_AGENT_SERVER_ALLOWED_ORIGINS"] ?? "",
3980
3836
  root: process.env["MOLTNET_AGENT_SERVER_ROOT"] ?? "",
@@ -3994,6 +3850,378 @@ function loadUpdateEnvConfig() {
3994
3850
  };
3995
3851
  }
3996
3852
  //#endregion
3853
+ //#region src/lib/run-project-selection.ts
3854
+ var execFileAsync$2 = promisify(execFile);
3855
+ var GIT_TIMEOUT_MS = 1e4;
3856
+ var GIT_MAX_OUTPUT_BYTES = 64 * 1024;
3857
+ async function validateGitSource(source) {
3858
+ const inherited = processEnvSnapshot();
3859
+ try {
3860
+ const { stdout } = await execFileAsync$2("git", [
3861
+ "rev-parse",
3862
+ "--show-toplevel",
3863
+ "--verify",
3864
+ "HEAD^{commit}"
3865
+ ], {
3866
+ cwd: source,
3867
+ env: {
3868
+ PATH: inherited.PATH,
3869
+ HOME: inherited.HOME,
3870
+ GIT_CONFIG_NOSYSTEM: "1",
3871
+ GIT_CONFIG_GLOBAL: "/dev/null"
3872
+ },
3873
+ timeout: GIT_TIMEOUT_MS,
3874
+ killSignal: "SIGKILL",
3875
+ maxBuffer: GIT_MAX_OUTPUT_BYTES
3876
+ });
3877
+ const top = stdout.trim().split("\n")[0];
3878
+ if (await canonicalDirectory(top) === source) return;
3879
+ } catch (cause) {
3880
+ throw new ProjectConfigError("selection", `git-worktree source ${source} requires a Git repository root with a committed revision: ${cause instanceof Error ? cause.message : String(cause)}`, { cause });
3881
+ }
3882
+ throw new ProjectConfigError("selection", `git-worktree source ${source} must be the Git repository root, not a subdirectory`);
3883
+ }
3884
+ function projectRunOptionDefs() {
3885
+ return {
3886
+ project: { type: "string" },
3887
+ binding: { type: "string" },
3888
+ general: { type: "boolean" },
3889
+ "config-file": { type: "string" },
3890
+ "state-dir": { type: "string" },
3891
+ source: { type: "string" },
3892
+ "workspace-strategy": { type: "string" }
3893
+ };
3894
+ }
3895
+ function strategy(value) {
3896
+ if (value === void 0) return void 0;
3897
+ if (WORKSPACE_STRATEGIES.includes(value)) return value;
3898
+ throw new ProjectConfigError("validation", `Unknown workspace strategy ${value}; choose ${WORKSPACE_STRATEGIES.join(", ")}`);
3899
+ }
3900
+ /** Resolve once at worker startup. No credentials, remote calls, hooks or workspace creation. */
3901
+ async function resolveRunProjectSelection(args) {
3902
+ const env = processEnvSnapshot();
3903
+ const inherited = env.MOLTNET_ACTIVE_IDENTITY === args.agent && !args.general;
3904
+ const bindingName = args.binding ?? (!args["config-file"] && !args.project && inherited ? env.MOLTNET_PROJECT_BINDING : void 0);
3905
+ const projectId = args.project ?? (!args["config-file"] && !args.binding && inherited ? env.MOLTNET_PROJECT_ID : void 0);
3906
+ const configPath = resolve(args.cwd, args["config-file"] ?? (inherited ? env.MOLTNET_PROJECT_CONFIG : void 0) ?? getProjectConfigPath());
3907
+ if (args.general && (args.project || args.binding)) throw new ProjectConfigError("selection", "General work cannot also declare a project or binding");
3908
+ const overrideStrategy = strategy(args["workspace-strategy"]);
3909
+ const stateRootDir = args["state-dir"] ? resolve(args.cwd, args["state-dir"]) : void 0;
3910
+ let binding = null;
3911
+ if (!args.general) try {
3912
+ binding = await resolveProjectBinding(await readProjectConfig(configPath), {
3913
+ configPath,
3914
+ cwd: args.cwd,
3915
+ binding: bindingName,
3916
+ projectId,
3917
+ native: !bindingName && !projectId,
3918
+ teamId: args.team,
3919
+ apiUrl: args.apiUrl || void 0,
3920
+ overrides: {
3921
+ ...args.source === void 0 ? {} : { source: args.source },
3922
+ ...overrideStrategy === void 0 ? {} : { strategy: overrideStrategy }
3923
+ }
3924
+ });
3925
+ if (!binding && (bindingName || projectId)) throw new ProjectConfigError("selection", `No matching project binding ${bindingName ?? projectId} in ${configPath} for endpoint ${args.apiUrl ?? "(unspecified)"}; run moltnet projects setup or select --binding/--general`);
3926
+ } catch (cause) {
3927
+ throw new ProjectConfigError(cause instanceof ProjectConfigError ? cause.kind : "selection", `Project selection in ${configPath} (binding ${bindingName ?? projectId ?? "ancestor"}, endpoint ${args.apiUrl ?? "unspecified"}): ${cause instanceof Error ? cause.message : String(cause)}. Use moltnet projects setup to register a folder, or select --binding/--general.`, { cause });
3928
+ }
3929
+ const workspaceStrategy = binding?.strategy ?? overrideStrategy ?? "existing";
3930
+ if (workspaceStrategy === "none" && args.source !== void 0) throw new ProjectConfigError("selection", "No-workspace execution cannot specify a source");
3931
+ let source = binding?.source;
3932
+ if (workspaceStrategy !== "none" && !source) source = await canonicalDirectory(resolve(args.cwd, args.source ?? "."));
3933
+ if (workspaceStrategy === "isolated-directory" || binding?.hooks?.afterCreate || binding?.hooks?.beforeRun) throw new ProjectConfigError("selection", `Binding ${binding?.name ?? "(run override)"} in ${configPath}: this runtime does not support isolated-directory preparation or setup hooks; choose a supported binding`);
3934
+ if (workspaceStrategy === "git-worktree" && source) await validateGitSource(source);
3935
+ return {
3936
+ configPath,
3937
+ selectedBy: args.binding || args.project ? "explicit" : bindingName ? "activation" : binding ? "ancestor" : "general",
3938
+ projectId: binding?.projectId ?? null,
3939
+ teamId: binding?.teamId ?? args.team,
3940
+ apiUrl: binding?.apiUrl ?? (args.apiUrl || void 0),
3941
+ ...binding ? { binding } : {},
3942
+ source,
3943
+ strategy: workspaceStrategy,
3944
+ stateRootDir,
3945
+ workspaceExplicit: Boolean(binding || overrideStrategy || args.source)
3946
+ };
3947
+ }
3948
+ /** A selected location fixes this worker's strategy; saved profiles are never mutated. */
3949
+ function applyProjectWorkspacePolicy(profile, selection) {
3950
+ if (!selection.workspaceExplicit) return profile;
3951
+ if (selection.strategy === "isolated-directory") throw new Error("This runtime does not yet support isolated-directory preparation");
3952
+ if (selection.binding?.hooks?.afterCreate || selection.binding?.hooks?.beforeRun) throw new Error("This runtime does not yet support project setup hooks");
3953
+ const mode = selection.strategy === "existing" ? "shared_mount" : selection.strategy === "git-worktree" ? "dedicated_worktree" : "none";
3954
+ if (profile.allowedWorkspaceModes.length && !profile.allowedWorkspaceModes.includes(mode)) throw new Error(`Workspace strategy ${selection.strategy} is not allowed by profile ${profile.name}`);
3955
+ return {
3956
+ ...profile,
3957
+ mountPath: selection.source ?? profile.mountPath,
3958
+ defaultWorkspaceMode: mode,
3959
+ allowedWorkspaceModes: [mode]
3960
+ };
3961
+ }
3962
+ var PROJECT_RUN_FLAGS = ` --binding <name> Select a saved local project location.
3963
+ --project <uuid> Select a project and its unambiguous binding.
3964
+ --general Serve General work (projectId: null).
3965
+ --config-file <path> Explicit project bindings JSON.
3966
+ --source <path> Run-only source folder override.
3967
+ --workspace-strategy <name> existing, git-worktree, none; isolated-directory
3968
+ is reserved and currently unsupported.
3969
+ --state-dir <path> Supervisor/session state, separate from source.
3970
+ Default: profile mount root (existing state retained).`;
3971
+ //#endregion
3972
+ //#region src/lib/help.ts
3973
+ var COMMON_REQUIRED_FLAGS = `\
3974
+ -a, --agent <name> MoltNet agent identity. Agent-key auth is
3975
+ configless; OAuth2 reads moltnet.json.
3976
+ --profile <uuid|name> Remote runtime profile. Repeat for poll/drain
3977
+ to declare priority order. Provider, model,
3978
+ sandbox policy, prerequisites, and runtime
3979
+ execution policy come from the selected profile.`;
3980
+ var COMMON_OPTIONAL_FLAGS = `\
3981
+ --sandbox <path> Deprecated. Remote runtime profiles define
3982
+ sandbox policy.
3983
+ --agent-root <path> Explicit legacy identity bundle location.
3984
+ Omitted: use the central identity store.
3985
+ ${PROJECT_RUN_FLAGS}
3986
+ --git-author <"Name <email>">
3987
+ Non-secret git identity projected into the
3988
+ guest for host-brokered commit signing. Default:
3989
+ host git config. Configless agent-key runs must
3990
+ provide this flag or MOLTNET_GIT_AUTHOR.
3991
+ Env: MOLTNET_GIT_AUTHOR.
3992
+ --heartbeat-interval-ms <n> Reporter heartbeat cadence. Default: 60000.
3993
+ --warm-retention-sec <n> Resumability window for runtime slots
3994
+ (Pi sessions + reusable worktrees) after use.
3995
+ Default: 1800.
3996
+ --debug Verbose logging: also log successful list/claim
3997
+ outcomes (candidate counts, claim attempts).`;
3998
+ var REGISTERED_TASK_TYPES = Object.keys(BUILT_IN_TASK_TYPES).sort();
3999
+ function knownTaskTypesList() {
4000
+ return REGISTERED_TASK_TYPES.join(", ");
4001
+ }
4002
+ var ROOT_USAGE = `\
4003
+ agent-daemon — long-running task worker for MoltNet.
4004
+
4005
+ Usage: agent-daemon [--runtime <module>] <command> [...flags]
4006
+
4007
+ Runtime:
4008
+ --runtime <module> Trusted local file or installed package whose
4009
+ default export is a DaemonRuntimeAdapter.
4010
+ Omit to use the built-in gondolin_pi runtime.
4011
+
4012
+ Commands:
4013
+ poll Long-running worker. Polls the task queue and claims tasks
4014
+ matching the configured filter until SIGINT/SIGTERM.
4015
+ once Claim and execute one specific queued task by id, then exit.
4016
+ drain Poll until the queue has nothing claimable, then exit.
4017
+ Useful for batch eval runs and demos.
4018
+ server Loopback supervisor for console-managed runs: OAuth local control,
4019
+ agent/provider config store, and start/stop of poll/drain
4020
+ child processes. Binds 127.0.0.1 only.
4021
+ server trust
4022
+ Install the per-user macOS local-HTTPS CA after explicit consent.
4023
+ providers Manage configured endpoints and Pi OAuth subscriptions without
4024
+ starting the Agent Server. See \`agent-daemon providers --help\`.
4025
+ sync-sessions
4026
+ Repair durable runtime-session checkpoints from local slot files.
4027
+ update check
4028
+ Check the stable MoltNet agent release without reading credentials.
4029
+
4030
+ Run \`agent-daemon <command> --help\` for command-specific flags.
4031
+
4032
+ Prerequisites:
4033
+ - configless: MOLTNET_AGENT_KEY (or MOLTNET_AGENT_KEY_REF) and
4034
+ MOLTNET_PRIVATE_KEY (or MOLTNET_PRIVATE_KEY_REF); no agent files
4035
+ - config-based: ~/.config/moltnet/identities/<agent>/moltnet.json
4036
+ carrying agent_key_refs or agent_key_ref (OAuth2 is not accepted)
4037
+ --agent-root explicitly selects a legacy .moltnet/<agent> bundle
4038
+
4039
+ No key yet? Mint one with the CLI (--store writes the team slot into
4040
+ moltnet.json and keeps the secret in a provider):
4041
+
4042
+ moltnet teams list # find the team id
4043
+ moltnet agents keys create --team-id <team-uuid> \\
4044
+ --name <agent>-daemon --store
4045
+
4046
+ https://docs.themolt.net/operate/agent-keys#run-the-daemon-with-an-agent-key
4047
+ - --profile — remote runtime profile supplies provider/model/sandbox
4048
+ policy and CWD is used as the VM mountPath.
4049
+
4050
+ Registered task types: ${knownTaskTypesList()}`;
4051
+ var POLL_HELP = `\
4052
+ agent-daemon poll — long-running task worker.
4053
+
4054
+ Usage:
4055
+ agent-daemon poll --team <uuid> --agent <name> --profile <uuid|name> [...]
4056
+
4057
+ Required:
4058
+ --team <uuid> Team whose queue to serve. The daemon must be
4059
+ a member of this team (canAccessTeam permit).
4060
+ ${COMMON_REQUIRED_FLAGS}
4061
+
4062
+ Optional:
4063
+ --task-types <csv> Whitelist of task types to claim. Default:
4064
+ accept any registered type. Known types:
4065
+ ${knownTaskTypesList()}
4066
+ --correlation-id <uuid> Restrict claims to one orchestration run.
4067
+ --diary-ids <csv> Further client-side filter on task.diaryId.
4068
+ --poll-interval-ms <n> Idle backoff floor. Default: 2000.
4069
+ --max-poll-interval-ms <n> Idle backoff ceiling. Default: 30000.
4070
+ --list-limit <n> Page size per list call. Default: 10.
4071
+ ${COMMON_OPTIONAL_FLAGS}
4072
+
4073
+ Example:
4074
+ agent-daemon poll \\
4075
+ --team 6743b4b1-6b93-46e2-a048-19490f04f91a \\
4076
+ --task-types curate_pack,fulfill_brief \\
4077
+ --agent legreffier \\
4078
+ --profile github-linear \\
4079
+ --profile local-fallback
4080
+
4081
+ Stops cleanly on SIGINT/SIGTERM (drains the in-flight task before exit).`;
4082
+ var ONCE_HELP = `\
4083
+ agent-daemon once — execute one specific queued task by id, then exit.
4084
+
4085
+ Usage:
4086
+ agent-daemon once --task-id <uuid> --agent <name> --profile <uuid|name> [...]
4087
+
4088
+ Required:
4089
+ -t, --task-id <uuid> Task to claim and execute. Must already be
4090
+ in 'queued' status.
4091
+ ${COMMON_REQUIRED_FLAGS}
4092
+
4093
+ Optional:
4094
+ --team <uuid> Team scope for resolving --profile by name.
4095
+ Required only when --profile is a name.
4096
+ ${COMMON_OPTIONAL_FLAGS}
4097
+
4098
+ Example:
4099
+ agent-daemon once \\
4100
+ --task-id 26004a77-bc10-43ef-a79f-c8e62faf59b1 \\
4101
+ --agent legreffier \\
4102
+ --profile github-linear
4103
+
4104
+ Exits 0 on completed, 1 on failed/cancelled/runtime-error.`;
4105
+ var DRAIN_HELP = `\
4106
+ agent-daemon drain — poll until the queue is empty, then exit.
4107
+
4108
+ Usage:
4109
+ agent-daemon drain --team <uuid> --agent <name> --profile <uuid|name> [...]
4110
+
4111
+ Same flags as \`poll\`. The only behavioural difference: \`drain\` exits
4112
+ when a list call confirms no claimable tasks remain (vs \`poll\` which
4113
+ sleeps and retries forever).
4114
+
4115
+ Required:
4116
+ --team <uuid> Team whose queue to drain.
4117
+ ${COMMON_REQUIRED_FLAGS}
4118
+
4119
+ Optional:
4120
+ --task-types <csv> Whitelist. Known types: ${knownTaskTypesList()}
4121
+ --correlation-id <uuid> Restrict claims to one orchestration run.
4122
+ --wait-for-first-task-sec <n>
4123
+ Wait this long for an initially empty run before
4124
+ exiting. After the first claim, exit on empty.
4125
+ --wait-after-task-sec <n> Require the queue to remain empty for this long
4126
+ after a claim before exiting.
4127
+ --diary-ids <csv> Diary filter.
4128
+ --poll-interval-ms <n> Default: 2000.
4129
+ --max-poll-interval-ms <n> Default: 30000.
4130
+ --list-limit <n> Default: 10.
4131
+ ${COMMON_OPTIONAL_FLAGS}
4132
+
4133
+ Example:
4134
+ agent-daemon drain \\
4135
+ --team 6743b4b1-6b93-46e2-a048-19490f04f91a \\
4136
+ --task-types judge_pack \\
4137
+ --agent legreffier \\
4138
+ --profile eval-judge`;
4139
+ var SYNC_SESSIONS_HELP = `\
4140
+ agent-daemon sync-sessions — repair durable runtime-session checkpoints.
4141
+
4142
+ Usage:
4143
+ agent-daemon sync-sessions --team <uuid> --agent <name> [...]
4144
+
4145
+ Scans this daemon's team-scoped runtime slots, compares local Pi session files
4146
+ with durable runtime-session metadata, and uploads missing or stale checkpoints.
4147
+
4148
+ Required:
4149
+ --team <uuid> Team whose runtime slots to inspect.
4150
+ -a, --agent <name> MoltNet agent identity. Reads credentials
4151
+ from <agent-root>/.moltnet/<name>/moltnet.json.
4152
+
4153
+ Optional:
4154
+ --runtime-profile-id <uuid> Limit repair to one runtime profile.
4155
+ --state <active|idle> Limit scanned slots by state. Default: all.
4156
+ --limit <n> Max slots to scan, 1..200. Default: 100.
4157
+ --dry-run Report missing/stale sessions without uploading.
4158
+ --agent-root <path> Explicit legacy identity bundle location.
4159
+ Omitted: use the central identity store.
4160
+ ${PROJECT_RUN_FLAGS}
4161
+ --debug Accepted for consistency; no extra output yet.
4162
+
4163
+ Example:
4164
+ agent-daemon sync-sessions \\
4165
+ --team 6743b4b1-6b93-46e2-a048-19490f04f91a \\
4166
+ --agent legreffier \\
4167
+ --state idle`;
4168
+ function isHelpFlag(args) {
4169
+ return args.includes("--help") || args.includes("-h");
4170
+ }
4171
+ var AGENT_SERVER_HELP = `\
4172
+ agent-daemon server — loopback supervisor for console-managed runs.
4173
+
4174
+ Binds 127.0.0.1 only. An authorized Console origin configures agents and
4175
+ providers (secret references only) and starts/stops poll/drain runs as
4176
+ child processes of this supervisor.
4177
+
4178
+ Options:
4179
+ --port <n> Loopback port. Default: 17374.
4180
+ Env: MOLTNET_AGENT_SERVER_PORT.
4181
+ --allowed-origins <csv> Exact Console origins allowed local control.
4182
+ Default: https://console.themolt.net.
4183
+ Env: MOLTNET_AGENT_SERVER_ALLOWED_ORIGINS.
4184
+ --root <path> Config root. Default: ~/.config/moltnet
4185
+ (or MOLTNET_AGENT_SERVER_ROOT).
4186
+ --api-url <url> Default MoltNet API for new managed agents.
4187
+ Default: https://api.themolt.net.
4188
+ --heartbeat-interval-ms <n> Child reporter heartbeat cadence. Default: 60000.
4189
+ --warm-retention-sec <n> Child session/workspace retention. Default: 1800.
4190
+ --supervised Also stop gracefully when stdin reaches EOF.
4191
+
4192
+ On macOS, the first interactive run asks to trust a per-user local CA in the
4193
+ login keychain and serves HTTPS. Native supervisors use:
4194
+ server trust --status --json
4195
+ server trust --yes --json
4196
+ server trust --remove --yes --json
4197
+ Run \`agent-daemon server trust --remove\` interactively to remove that exact
4198
+ CA. Linux continues to use the Chromium PNA HTTP path.
4199
+ `;
4200
+ var PROVIDERS_HELP = `\
4201
+ moltnet-agent providers — manage local model providers.
4202
+
4203
+ Usage:
4204
+ moltnet-agent providers list [--json] [--root <path>]
4205
+ moltnet-agent providers set <id> [--base-url <url>] [--api <pi-api-kind>]
4206
+ [--model <id> ... | --clear-models]
4207
+ [--model-input <id>=text,image ...]
4208
+ [--api-key-stdin | --clear-api-key] [--root <path>]
4209
+ moltnet-agent providers discover <id> [--save] [--json] [--root <path>]
4210
+ moltnet-agent providers remove <id> [--yes] [--root <path>]
4211
+ moltnet-agent providers login <id> [--auth-method <method-id>]
4212
+ [--root <path>]
4213
+ moltnet-agent providers logout <id> [--yes] [--root <path>]
4214
+
4215
+ The default root is ~/.config/moltnet. MOLTNET_AGENT_SERVER_ROOT remains the
4216
+ environment override. API keys are accepted only from redirected stdin; they
4217
+ are stored separately and providers.json contains only a secret reference.
4218
+
4219
+ --model declares a text-only model. --model-input declares a model together
4220
+ with the input modalities it accepts, and is what makes a vision model usable:
4221
+ a model with no declared modalities is text-only to Pi, which drops image
4222
+ content parts before the request leaves the runtime.
4223
+ `;
4224
+ //#endregion
3997
4225
  //#region src/lib/abort-active-attempt.ts
3998
4226
  /** Best-effort signal cleanup; lease expiry remains the final backstop. */
3999
4227
  async function abortActiveAttemptOnSignal(opts) {
@@ -4173,6 +4401,7 @@ function slugifySessionComponent(input) {
4173
4401
  }
4174
4402
  //#endregion
4175
4403
  //#region src/lib/task-execution-plan.ts
4404
+ var WorkspaceModeMismatchError = class extends Error {};
4176
4405
  function buildDaemonTaskExecutionPlan(task, stateDirs, identity, warmRetentionSec, runtimeProfileWorkspacePolicy = {}, attemptN) {
4177
4406
  const descriptor = deriveTaskSessionDescriptor(task);
4178
4407
  const workspaceMode = resolveTaskWorkspaceMode(task, descriptor.policy, runtimeProfileWorkspacePolicy);
@@ -4246,6 +4475,7 @@ function resolveTaskWorkspaceMode(task, policy, runtimeProfileWorkspacePolicy) {
4246
4475
  const requestedWorkspace = policy.acceptsInputWorkspaceOverride && typeof task.input.execution?.workspace === "string" ? task.input.execution.workspace : null;
4247
4476
  if (isRuntimeProfileWorkspaceMode(requestedWorkspace)) {
4248
4477
  if (allowed.has(requestedWorkspace)) return toDaemonWorkspaceMode(requestedWorkspace);
4478
+ if (runtimeProfileWorkspacePolicy.workspaceExplicit) throw new WorkspaceModeMismatchError(`Requested workspace mode ${requestedWorkspace} is not allowed by profile ${runtimeProfileWorkspacePolicy.profileName ?? "(selected)"}; allowed: ${[...allowed].join(", ")}`);
4249
4479
  }
4250
4480
  if (profileDefault && allowed.has(profileDefault)) return toDaemonWorkspaceMode(profileDefault);
4251
4481
  if (allowed.has(policy.workspaceMode)) return policy.workspaceMode;
@@ -4353,8 +4583,8 @@ function assertPlanAllowedByWorkspacePolicy(plan, policy, runtimeProfileId) {
4353
4583
  "dedicated_worktree"
4354
4584
  ]);
4355
4585
  const effectiveMode = planToRuntimeProfileWorkspaceMode(plan);
4356
- if (plan.workspaceRevision && effectiveMode !== "dedicated_worktree") throw new Error(`Runtime profile "${runtimeProfileId}" does not allow "dedicated_worktree", required by a revision-pinned task (resolved workspace mode "${effectiveMode}")`);
4357
- if (!allowed.has(effectiveMode)) throw new Error(`Runtime profile forbids final workspace mode "${effectiveMode}" for this task`);
4586
+ if (plan.workspaceRevision && effectiveMode !== "dedicated_worktree") throw new WorkspaceModeMismatchError(`Runtime profile "${runtimeProfileId}" does not allow "dedicated_worktree", required by a revision-pinned task (resolved workspace mode "${effectiveMode}")`);
4587
+ if (!allowed.has(effectiveMode)) throw new WorkspaceModeMismatchError(`Runtime profile "${policy?.profileName ?? runtimeProfileId}" forbids final workspace mode "${effectiveMode}"; allowed: ${[...allowed].join(", ")}`);
4358
4588
  }
4359
4589
  function planToRuntimeProfileWorkspaceMode(plan) {
4360
4590
  if (plan.workspaceMode === "scratch_mount") return "none";
@@ -4541,7 +4771,8 @@ function resolveProducerWorkspaceCopySource(producer, stateDirs) {
4541
4771
  if (isDisposableScratchWorkspace(producer, stateDirs)) return null;
4542
4772
  throw new ProducerContextResolutionError(`Producer workspace path is missing on disk: ${workspacePath}`);
4543
4773
  }
4544
- const sharedMountRoot = dirname(dirname(stateDirs.rootDir));
4774
+ const sharedMountRoot = stateDirs.mountPath;
4775
+ if (!sharedMountRoot) throw new ProducerContextResolutionError("Shared producer mount root was not supplied by the runtime profile");
4545
4776
  if (!existsSync(sharedMountRoot)) throw new ProducerContextResolutionError(`Shared producer mount root is missing on disk: ${sharedMountRoot}`);
4546
4777
  return sharedMountRoot;
4547
4778
  }
@@ -4558,7 +4789,14 @@ function recoverScratchWorkspacePath(producer, stateDirs) {
4558
4789
  }
4559
4790
  //#endregion
4560
4791
  //#region src/lib/executor-attestation.ts
4561
- var DAEMON_REQUIRED_SCOPES = AGENT_CREDENTIAL_SCOPES;
4792
+ /**
4793
+ * The startup gate, which is the boot floor rather than the issuance default.
4794
+ * Gating on the wider `AGENT_CREDENTIAL_SCOPES` would refuse every key minted
4795
+ * before a scope was added to it, and a key cannot widen itself.
4796
+ */
4797
+ var DAEMON_REQUIRED_SCOPES = DAEMON_MINIMUM_SCOPES;
4798
+ /** What `moltnet agents keys create` should mint for a new daemon. */
4799
+ var DAEMON_RECOMMENDED_SCOPES = AGENT_CREDENTIAL_SCOPES;
4562
4800
  async function resolveExecutorSigningPrivateKey(input) {
4563
4801
  if (input.credentialSource === "environment") {
4564
4802
  const privateKey = input.configuredPrivateKey.trim();
@@ -4585,7 +4823,7 @@ async function resolveExecutorSigningPrivateKey(input) {
4585
4823
  function validateDaemonScopes(whoami) {
4586
4824
  const available = new Set(whoami.scopes ?? []);
4587
4825
  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(" ")}.`);
4826
+ 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
4827
  }
4590
4828
  async function validateExecutorSigningIdentity(input) {
4591
4829
  let publicKey;
@@ -5522,7 +5760,6 @@ var AgentServerStore = class {
5522
5760
  activations: {}
5523
5761
  };
5524
5762
  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`);
5525
- 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");
5526
5763
  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");
5527
5764
  for (const [alias, activation] of Object.entries(state.activations)) validateActivation(alias, activation);
5528
5765
  for (const [alias, registration] of Object.entries(state.pendingRegistrations)) {
@@ -5626,6 +5863,17 @@ var AgentServerStore = class {
5626
5863
  if (activation.source === "managed") delete state.pendingRegistrations[alias];
5627
5864
  this.writeAgentServerState(state);
5628
5865
  }
5866
+ writeCredentialMetadata(alias, teamId, metadata) {
5867
+ const activation = this.readActivation(alias);
5868
+ if (!activation) throw new AgentServerStoreError("not_found", "Identity is not activated");
5869
+ this.writeActivation({
5870
+ ...activation,
5871
+ credentialHealth: {
5872
+ ...activation.credentialHealth,
5873
+ [teamId]: metadata
5874
+ }
5875
+ });
5876
+ }
5629
5877
  listActivations() {
5630
5878
  return Object.values(this.readAgentServerState().activations).sort((a, b) => a.alias.localeCompare(b.alias));
5631
5879
  }
@@ -5691,6 +5939,24 @@ var AgentServerStore = class {
5691
5939
  writeRun(record) {
5692
5940
  writeJsonAtomic(join(this.runDir(record.id), "run.json"), record);
5693
5941
  }
5942
+ /** Status polling reads history without blocking the supervisor event loop. */
5943
+ async listRunsAsync(limit, includeIds = []) {
5944
+ let ids;
5945
+ try {
5946
+ ids = await readdir(this.runsDir);
5947
+ } catch {
5948
+ return [];
5949
+ }
5950
+ const selected = ids.filter((id) => NAME_RE.test(id)).sort().reverse().slice(0, Math.max(0, limit));
5951
+ return (await Promise.all([...new Set([...includeIds, ...selected])].map(async (id) => {
5952
+ try {
5953
+ return JSON.parse(await readFile(join(this.runDir(id), "run.json"), "utf8"));
5954
+ } catch (error) {
5955
+ if (error.code === "ENOENT") return null;
5956
+ throw error;
5957
+ }
5958
+ }))).filter((record) => record !== null).sort((a, b) => b.startedAt.localeCompare(a.startedAt));
5959
+ }
5694
5960
  listRuns(limit = Number.POSITIVE_INFINITY) {
5695
5961
  let ids;
5696
5962
  try {
@@ -5921,14 +6187,32 @@ function mergePiModels(store, repo) {
5921
6187
  //#endregion
5922
6188
  //#region src/lib/state-dir.ts
5923
6189
  function ensureDaemonStateDirs(mountPath) {
5924
- const rootDir = join(mountPath, ".moltnet", "d");
6190
+ const rootDir = canonicalStatePath(join(mountPath, ".moltnet", "d"));
5925
6191
  const piSessionsDir = join(rootDir, "pi-sessions");
5926
- mkdirSync(piSessionsDir, { recursive: true });
6192
+ mkdirSync(rootDir, {
6193
+ recursive: true,
6194
+ mode: 448
6195
+ });
6196
+ mkdirSync(piSessionsDir, {
6197
+ recursive: true,
6198
+ mode: 448
6199
+ });
5927
6200
  return {
5928
- rootDir,
5929
- piSessionsDir
6201
+ rootDir: realpathSync(rootDir),
6202
+ piSessionsDir: realpathSync(piSessionsDir)
5930
6203
  };
5931
6204
  }
6205
+ function canonicalStatePath(path) {
6206
+ let parent = resolve(path);
6207
+ const missing = [];
6208
+ while (!existsSync(parent)) {
6209
+ missing.unshift(basename(parent));
6210
+ const next = dirname(parent);
6211
+ if (next === parent) throw new Error(`Cannot resolve daemon state root ${path}`);
6212
+ parent = next;
6213
+ }
6214
+ return join(realpathSync(parent), ...missing);
6215
+ }
5932
6216
  //#endregion
5933
6217
  //#region src/lib/prepare-runtime-profile.ts
5934
6218
  /** Validate and prepare a profile through the shared daemon execution path. */
@@ -5950,7 +6234,8 @@ async function prepareRuntimeProfile(input) {
5950
6234
  rootDir: profile.mountPath,
5951
6235
  path: profile.source
5952
6236
  };
5953
- const stateDirs = ensureDaemonStateDirs(sandbox.rootDir);
6237
+ const stateDirs = ensureDaemonStateDirs(input.stateRootDir ?? sandbox.rootDir);
6238
+ stateDirs.mountPath = profile.mountPath;
5954
6239
  const slotIdentity = {
5955
6240
  agentName: input.agentName,
5956
6241
  runtimeProfileId: profile.id,
@@ -5967,6 +6252,8 @@ async function prepareRuntimeProfile(input) {
5967
6252
  slotIdentity,
5968
6253
  warmRetentionSec: input.warmRetentionSec,
5969
6254
  workspacePolicy: {
6255
+ workspaceExplicit: input.workspaceExplicit,
6256
+ profileName: profile.name,
5970
6257
  defaultWorkspaceMode: profile.defaultWorkspaceMode,
5971
6258
  allowedWorkspaceModes: profile.allowedWorkspaceModes
5972
6259
  },
@@ -5977,6 +6264,32 @@ async function prepareRuntimeProfile(input) {
5977
6264
  };
5978
6265
  }
5979
6266
  //#endregion
6267
+ //#region src/lib/project-task-source.ts
6268
+ function createProjectOnceSource(selection, options) {
6269
+ return new ApiTaskSource({
6270
+ ...options,
6271
+ projectId: selection.projectId
6272
+ });
6273
+ }
6274
+ function createProjectPollingSource(selection, options) {
6275
+ return new PollingApiTaskSource({
6276
+ ...options,
6277
+ projectId: selection.projectId,
6278
+ isTaskEligible: (task) => {
6279
+ if (!selection.workspaceExplicit) return true;
6280
+ if (resolveTaskWorkspaceRevision(task.input) && selection.strategy !== "git-worktree") return false;
6281
+ if (!getTaskExecutionPolicy(task.taskType).acceptsInputWorkspaceOverride) return true;
6282
+ const requested = task.input?.execution?.workspace;
6283
+ if (!requested || ![
6284
+ "none",
6285
+ "shared_mount",
6286
+ "dedicated_worktree"
6287
+ ].includes(requested)) return true;
6288
+ return requested === (selection.strategy === "existing" ? "shared_mount" : selection.strategy === "git-worktree" ? "dedicated_worktree" : "none");
6289
+ }
6290
+ });
6291
+ }
6292
+ //#endregion
5980
6293
  //#region src/lib/runtime-context.ts
5981
6294
  var storage = new AsyncLocalStorage();
5982
6295
  function runWithDaemonRuntimeContext(context, callback) {
@@ -6556,6 +6869,7 @@ async function runPolling(opts) {
6556
6869
  args: opts.argv,
6557
6870
  options: {
6558
6871
  ...runtimeCommandOptionDefs(),
6872
+ ...projectRunOptionDefs(),
6559
6873
  team: { type: "string" },
6560
6874
  "task-types": { type: "string" },
6561
6875
  "correlation-id": { type: "string" },
@@ -6572,12 +6886,12 @@ async function runPolling(opts) {
6572
6886
  }
6573
6887
  }
6574
6888
  });
6575
- if (!values.team) {
6889
+ if (!values.team && !values.binding && !values.project && !values["config-file"]) {
6576
6890
  console.error("Missing required flag: --team\n");
6577
6891
  console.error(opts.helpText);
6578
6892
  return 1;
6579
6893
  }
6580
- const teamId = values.team;
6894
+ let teamId = values.team ?? "";
6581
6895
  const profileValues = parseProfileValues(values.profile);
6582
6896
  if (profileValues.length === 0) {
6583
6897
  console.error("Missing required flag: --profile\n");
@@ -6620,6 +6934,41 @@ async function runPolling(opts) {
6620
6934
  }
6621
6935
  if (taskTypes.length === 0) console.error(`[${opts.modeLabel}] --task-types is empty — daemon will accept any registered type. Pass an explicit list to limit scope (e.g. --task-types fulfill_brief).`);
6622
6936
  const cfg = loadConfig();
6937
+ let selection;
6938
+ try {
6939
+ const endpoint = await resolveSelectionApiUrl(identity.agent, {
6940
+ agentRootDir: values["agent-root"] ? resolve(process.cwd(), values["agent-root"]) : void 0,
6941
+ credentialSource: cfg.credentialSource,
6942
+ envApiUrl: cfg.apiUrl
6943
+ });
6944
+ selection = await resolveRunProjectSelection({
6945
+ agent: identity.agent,
6946
+ cwd: process.cwd(),
6947
+ apiUrl: endpoint,
6948
+ binding: values.binding,
6949
+ project: values.project,
6950
+ team: values.team,
6951
+ general: values.general,
6952
+ "config-file": values["config-file"],
6953
+ "state-dir": values["state-dir"],
6954
+ source: values.source,
6955
+ "workspace-strategy": values["workspace-strategy"]
6956
+ });
6957
+ teamId = selection.teamId ?? "";
6958
+ if (!teamId) throw new Error("Select --team or a binding with a team");
6959
+ } catch (error) {
6960
+ await logDaemonStartupFailure({
6961
+ serviceName: "agent-daemon.selection",
6962
+ level: cfg.logLevel || "info",
6963
+ gate: "project_selection",
6964
+ agent: identity.agent,
6965
+ credentialSource: cfg.credentialSource,
6966
+ error
6967
+ });
6968
+ console.error(error instanceof Error ? error.message : String(error));
6969
+ console.error(opts.helpText);
6970
+ return 1;
6971
+ }
6623
6972
  const credentialSources = {
6624
6973
  profileRequirements: cfg.profileCredentialRequirements,
6625
6974
  bindings: cfg.credentialBindings
@@ -6632,11 +6981,14 @@ async function runPolling(opts) {
6632
6981
  const resolvedContext = await resolveAgentContext(identity.agent, {
6633
6982
  agentRootDir: explicitAgentRootDir,
6634
6983
  credentialSource: cfg.credentialSource,
6635
- envApiUrl: cfg.apiUrl
6984
+ envApiUrl: cfg.apiUrl,
6985
+ projectApiUrl: selection.binding?.apiUrl,
6986
+ teamId
6636
6987
  });
6637
6988
  gate = "authenticate_and_bind";
6638
6989
  const whoami = await validateStartupBinding({
6639
6990
  agent: resolvedContext.agent,
6991
+ credentialTeamId: resolvedContext.credentialTeamId,
6640
6992
  teamId,
6641
6993
  expectedAgent: cfg.expectedAgent
6642
6994
  });
@@ -6685,18 +7037,25 @@ async function runPolling(opts) {
6685
7037
  throw error;
6686
7038
  }
6687
7039
  })();
6688
- const daemonRootDir = explicitAgentRootDir ?? process.cwd();
6689
- const resolvedProfiles = await resolveRuntimeProfiles({
7040
+ const daemonRootDir = selection.binding || values.source ? selection.source ?? process.cwd() : explicitAgentRootDir ?? process.cwd();
7041
+ const resolvedProfiles = (await resolveRuntimeProfiles({
6690
7042
  agent: ctx.agent,
6691
7043
  profiles: profileValues,
6692
7044
  teamId,
6693
7045
  cwd: daemonRootDir
6694
- });
7046
+ })).map((profile) => applyProjectWorkspacePolicy(profile, selection));
6695
7047
  const { logger, shutdown: shutdownLogger } = createRootLogger({
6696
7048
  name: `agent-daemon.${opts.modeLabel}`,
6697
7049
  level: cfg.logLevel || (identity.debug ? "debug" : "info")
6698
7050
  });
6699
7051
  const rootLogger = logger.child({
7052
+ projectId: selection.projectId,
7053
+ binding: selection.binding?.name,
7054
+ selectedBy: selection.selectedBy,
7055
+ source: daemonRootDir,
7056
+ strategy: selection.strategy,
7057
+ stateRootDir: selection.stateRootDir ?? daemonRootDir,
7058
+ apiUrl: selection.apiUrl,
6700
7059
  mode: opts.modeLabel,
6701
7060
  agent: identity.agent,
6702
7061
  teamId,
@@ -6719,6 +7078,8 @@ async function runPolling(opts) {
6719
7078
  agent: ctx.agent,
6720
7079
  agentName: identity.agent,
6721
7080
  profile,
7081
+ stateRootDir: selection.stateRootDir,
7082
+ workspaceExplicit: selection.workspaceExplicit,
6722
7083
  prerequisiteEnv: cfg.profilePrerequisiteEnv,
6723
7084
  runtimeAdapter,
6724
7085
  runtimeInstanceId,
@@ -6750,6 +7111,9 @@ async function runPolling(opts) {
6750
7111
  agent: ctx.agent,
6751
7112
  endpoint: cfg.otelEndpoint,
6752
7113
  resourceAttributes: {
7114
+ "moltnet.project.id": selection.projectId ?? "general",
7115
+ "moltnet.project.selection": selection.selectedBy,
7116
+ "moltnet.workspace.strategy": selection.strategy,
6753
7117
  "moltnet.team.id": teamId,
6754
7118
  "moltnet.agent.name": identity.agent,
6755
7119
  "moltnet.credential.source": ctx.credentialSource,
@@ -6868,7 +7232,7 @@ async function runPolling(opts) {
6868
7232
  try {
6869
7233
  runtime = new AgentRuntime({
6870
7234
  logger: rootLogger,
6871
- source: new PollingApiTaskSource({
7235
+ source: createProjectPollingSource(selection, {
6872
7236
  agent: ctx.agent,
6873
7237
  teamId,
6874
7238
  taskTypes: taskTypes.length > 0 ? taskTypes : void 0,
@@ -6990,7 +7354,7 @@ async function runPolling(opts) {
6990
7354
  error: {
6991
7355
  code: err instanceof ProducerContextResolutionError ? "producer_context_missing" : "execution_plan_failed",
6992
7356
  message,
6993
- retryable: false
7357
+ retryable: err instanceof WorkspaceModeMismatchError
6994
7358
  }
6995
7359
  };
6996
7360
  }
@@ -7190,6 +7554,7 @@ async function runOnce(argv, runtimeAdapter = defaultPiDaemonAdapter) {
7190
7554
  args: argv,
7191
7555
  options: {
7192
7556
  ...runtimeCommandOptionDefs(),
7557
+ ...projectRunOptionDefs(),
7193
7558
  "task-id": {
7194
7559
  type: "string",
7195
7560
  short: "t"
@@ -7227,6 +7592,39 @@ async function runOnce(argv, runtimeAdapter = defaultPiDaemonAdapter) {
7227
7592
  return 1;
7228
7593
  }
7229
7594
  const cfg = loadConfig();
7595
+ let selection;
7596
+ try {
7597
+ const endpoint = await resolveSelectionApiUrl(identity.agent, {
7598
+ agentRootDir: values["agent-root"] ? resolve(process.cwd(), values["agent-root"]) : void 0,
7599
+ credentialSource: cfg.credentialSource,
7600
+ envApiUrl: cfg.apiUrl
7601
+ });
7602
+ selection = await resolveRunProjectSelection({
7603
+ agent: identity.agent,
7604
+ cwd: process.cwd(),
7605
+ apiUrl: endpoint,
7606
+ binding: values.binding,
7607
+ project: values.project,
7608
+ team: values.team,
7609
+ general: values.general,
7610
+ "config-file": values["config-file"],
7611
+ "state-dir": values["state-dir"],
7612
+ source: values.source,
7613
+ "workspace-strategy": values["workspace-strategy"]
7614
+ });
7615
+ } catch (error) {
7616
+ await logDaemonStartupFailure({
7617
+ serviceName: "agent-daemon.selection",
7618
+ level: cfg.logLevel || "info",
7619
+ gate: "project_selection",
7620
+ agent: identity.agent,
7621
+ credentialSource: cfg.credentialSource,
7622
+ error
7623
+ });
7624
+ console.error(error instanceof Error ? error.message : String(error));
7625
+ console.error(ONCE_HELP);
7626
+ return 1;
7627
+ }
7230
7628
  const credentialSources = {
7231
7629
  profileRequirements: cfg.profileCredentialRequirements,
7232
7630
  bindings: cfg.credentialBindings
@@ -7239,12 +7637,15 @@ async function runOnce(argv, runtimeAdapter = defaultPiDaemonAdapter) {
7239
7637
  const resolvedContext = await resolveAgentContext(identity.agent, {
7240
7638
  agentRootDir: explicitAgentRootDir,
7241
7639
  credentialSource: cfg.credentialSource,
7242
- envApiUrl: cfg.apiUrl
7640
+ envApiUrl: cfg.apiUrl,
7641
+ projectApiUrl: selection.binding?.apiUrl,
7642
+ teamId: selection.teamId
7243
7643
  });
7244
7644
  gate = "authenticate_and_bind";
7245
7645
  const whoami = await validateStartupBinding({
7246
7646
  agent: resolvedContext.agent,
7247
- teamId: values.team,
7647
+ credentialTeamId: resolvedContext.credentialTeamId,
7648
+ teamId: selection.teamId,
7248
7649
  expectedAgent: cfg.expectedAgent
7249
7650
  });
7250
7651
  gate = "resolve_signing_material";
@@ -7291,18 +7692,25 @@ async function runOnce(argv, runtimeAdapter = defaultPiDaemonAdapter) {
7291
7692
  throw error;
7292
7693
  }
7293
7694
  })();
7294
- const daemonRootDir = explicitAgentRootDir ?? process.cwd();
7295
- const profile = await resolveRuntimeProfile({
7695
+ const daemonRootDir = selection.binding || values.source ? selection.source ?? process.cwd() : explicitAgentRootDir ?? process.cwd();
7696
+ const profile = applyProjectWorkspacePolicy(await resolveRuntimeProfile({
7296
7697
  agent: ctx.agent,
7297
7698
  profile: values.profile,
7298
- teamId: values.team,
7699
+ teamId: selection.teamId,
7299
7700
  cwd: daemonRootDir
7300
- });
7701
+ }), selection);
7301
7702
  const { logger, shutdown: shutdownLogger } = createRootLogger({
7302
7703
  name: "agent-daemon.once",
7303
7704
  level: cfg.logLevel || (identity.debug ? "debug" : "info")
7304
7705
  });
7305
7706
  const rootLogger = logger.child({
7707
+ projectId: selection.projectId,
7708
+ binding: selection.binding?.name,
7709
+ selectedBy: selection.selectedBy,
7710
+ source: daemonRootDir,
7711
+ strategy: selection.strategy,
7712
+ stateRootDir: selection.stateRootDir ?? daemonRootDir,
7713
+ apiUrl: selection.apiUrl,
7306
7714
  mode: "once",
7307
7715
  agent: identity.agent,
7308
7716
  provider: profile.provider,
@@ -7326,6 +7734,8 @@ async function runOnce(argv, runtimeAdapter = defaultPiDaemonAdapter) {
7326
7734
  agent: ctx.agent,
7327
7735
  agentName: identity.agent,
7328
7736
  profile,
7737
+ stateRootDir: selection.stateRootDir,
7738
+ workspaceExplicit: selection.workspaceExplicit,
7329
7739
  prerequisiteEnv: cfg.profilePrerequisiteEnv,
7330
7740
  runtimeAdapter,
7331
7741
  runtimeInstanceId,
@@ -7343,6 +7753,9 @@ async function runOnce(argv, runtimeAdapter = defaultPiDaemonAdapter) {
7343
7753
  agent: ctx.agent,
7344
7754
  endpoint: cfg.otelEndpoint,
7345
7755
  resourceAttributes: {
7756
+ "moltnet.project.id": selection.projectId ?? "general",
7757
+ "moltnet.project.selection": selection.selectedBy,
7758
+ "moltnet.workspace.strategy": selection.strategy,
7346
7759
  "moltnet.task.id": taskId,
7347
7760
  "moltnet.agent.name": identity.agent,
7348
7761
  "moltnet.credential.source": ctx.credentialSource,
@@ -7537,7 +7950,7 @@ async function runOnce(argv, runtimeAdapter = defaultPiDaemonAdapter) {
7537
7950
  });
7538
7951
  runtime = new AgentRuntime({
7539
7952
  logger: rootLogger,
7540
- source: new ApiTaskSource({
7953
+ source: createProjectOnceSource(selection, {
7541
7954
  agent: ctx.agent,
7542
7955
  taskId,
7543
7956
  teamId: profile.teamId,
@@ -7630,6 +8043,18 @@ function runPoll(argv, runtimeAdapter) {
7630
8043
  //#region src/lib/provider-lock.ts
7631
8044
  var DEFAULT_LOCK_TIMEOUT_MS = 3e4;
7632
8045
  var LOCK_WAIT_WARNING_MS = 1e3;
8046
+ /**
8047
+ * When to warn that we are waiting on another process.
8048
+ *
8049
+ * A fixed 1s threshold is silently useless to a caller whose whole budget is
8050
+ * shorter than that: the timeout wins the race and the operation fails with no
8051
+ * word of *why* it failed, which is the one thing the caller needs. So the
8052
+ * threshold scales down with the budget, and never sits so close to the
8053
+ * deadline that whether it fires depends on timer drift.
8054
+ */
8055
+ function warningThresholdMs(timeoutMs) {
8056
+ return Math.min(LOCK_WAIT_WARNING_MS, Math.floor(timeoutMs / 2));
8057
+ }
7633
8058
  var ProviderLockError = class extends Error {
7634
8059
  name = "ProviderLockError";
7635
8060
  constructor(code, message, options) {
@@ -7658,6 +8083,7 @@ async function withNamedProviderLock(root, name, work, options) {
7658
8083
  let compromised;
7659
8084
  const startedAt = Date.now();
7660
8085
  const timeoutMs = options.timeoutMs ?? DEFAULT_LOCK_TIMEOUT_MS;
8086
+ const warnAfterMs = warningThresholdMs(timeoutMs);
7661
8087
  const lockfilePath = join(locksDir, `${name}.lock`);
7662
8088
  let warned = false;
7663
8089
  let release;
@@ -7665,7 +8091,7 @@ async function withNamedProviderLock(root, name, work, options) {
7665
8091
  if (options.signal?.aborted) throw new ProviderLockError("lock_aborted", `provider lock acquisition was cancelled for "${name}"`, { cause: options.signal.reason });
7666
8092
  const elapsedMs = Date.now() - startedAt;
7667
8093
  if (elapsedMs >= timeoutMs) throw new ProviderLockError("lock_timeout", `timed out waiting for provider lock "${name}"`);
7668
- if (!warned && elapsedMs >= LOCK_WAIT_WARNING_MS) {
8094
+ if (!warned && elapsedMs >= warnAfterMs) {
7669
8095
  warned = true;
7670
8096
  options.logger?.warn({
7671
8097
  code: "provider_lock_contended",
@@ -8848,29 +9274,97 @@ function registerLoopbackSecurity(app, options) {
8848
9274
  });
8849
9275
  }
8850
9276
  //#endregion
8851
- //#region ../../libs/loopback-companion/src/fetch-metadata.ts
8852
- function headerValue(headers, name) {
8853
- const value = headers[name];
8854
- return typeof value === "string" ? value : void 0;
8855
- }
8856
- /**
8857
- * Require that a request is a top-level browser navigation (used by local
8858
- * approval pages that must be opened as a document, never fetched).
8859
- */
8860
- function assertNavigationRequest(headers) {
8861
- const site = headerValue(headers, "sec-fetch-site");
8862
- const mode = headerValue(headers, "sec-fetch-mode");
8863
- const destination = headerValue(headers, "sec-fetch-dest");
8864
- if (site !== "cross-site" && site !== "same-origin" && site !== "none" || mode !== "navigate" || destination !== "document") throw new LoopbackViolationError("navigation_required", "Request must be opened as a browser navigation");
9277
+ //#region src/lib/agent-server/connection-settings.ts
9278
+ var RELEASE_CONNECTION = {
9279
+ apiUrl: "https://api.themolt.net",
9280
+ issuer: "https://auth.themolt.net",
9281
+ publicUrl: "https://auth.themolt.net",
9282
+ nativeClientId: "moltnet-native",
9283
+ consoleClientId: "moltnet-console"
9284
+ };
9285
+ var KEYS = Object.keys(RELEASE_CONNECTION);
9286
+ function validateConnectionOverrides(value) {
9287
+ if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("Connection settings must be an object");
9288
+ const result = {};
9289
+ for (const [key, raw] of Object.entries(value)) {
9290
+ if (!KEYS.includes(key) || typeof raw !== "string" || !raw.trim()) throw new Error(`Invalid connection setting: ${key}`);
9291
+ const text = raw.trim();
9292
+ if (key.endsWith("ClientId")) {
9293
+ if (text.length > 255 || /\s/u.test(text)) throw new Error("Client IDs must not contain whitespace");
9294
+ } else {
9295
+ const url = new URL(text);
9296
+ if (url.username || url.password || url.search || url.hash || url.protocol !== "https:" && !(url.protocol === "http:" && (key === "issuer" || isLoopbackHostname(url.hostname)))) throw new Error("Connection URLs require HTTPS (HTTP is allowed only on loopback)");
9297
+ }
9298
+ result[key] = text;
9299
+ }
9300
+ return result;
8865
9301
  }
8866
- /**
8867
- * Reject an explicit cross-site Fetch-Metadata signal. Only the explicit
8868
- * `cross-site` value is rejected: Safari may omit Fetch Metadata on
8869
- * same-origin form submissions, so the absence of the header is not treated
8870
- * as a violation — callers keep their one-time token as the primary control.
8871
- */
8872
- function rejectExplicitCrossSite(headers) {
8873
- if (headerValue(headers, "sec-fetch-site") === "cross-site") throw new LoopbackViolationError("cross_site_rejected", "Request must not originate cross-site");
9302
+ /** Local administration only. Never exposed through browser authorization. */
9303
+ var ConnectionSettingsStore = class {
9304
+ path;
9305
+ environment;
9306
+ constructor(root, environment = {}) {
9307
+ this.root = root;
9308
+ this.path = join(root, "connection-settings.json");
9309
+ this.environment = validateConnectionOverrides(environment);
9310
+ }
9311
+ view() {
9312
+ let overrides = {};
9313
+ try {
9314
+ overrides = validateConnectionOverrides(JSON.parse(readFileSync(this.path, "utf8")));
9315
+ } catch (error) {
9316
+ if (error.code !== "ENOENT") throw error;
9317
+ }
9318
+ return {
9319
+ defaults: RELEASE_CONNECTION,
9320
+ overrides,
9321
+ environment: this.environment,
9322
+ effective: {
9323
+ ...RELEASE_CONNECTION,
9324
+ ...overrides,
9325
+ ...this.environment
9326
+ }
9327
+ };
9328
+ }
9329
+ stateRoot(settings = this.view().effective) {
9330
+ if (this.environment.apiUrl && this.environment.issuer) return this.root;
9331
+ return connectionStateRoot(this.root, settings);
9332
+ }
9333
+ save(value) {
9334
+ const overrides = validateConnectionOverrides(value);
9335
+ const current = this.view();
9336
+ for (const key of KEYS) {
9337
+ if (this.environment[key] !== void 0 && overrides[key] !== current.overrides[key]) throw new Error(`${key} is managed by the launch environment`);
9338
+ if (overrides[key] === RELEASE_CONNECTION[key]) delete overrides[key];
9339
+ }
9340
+ mkdirSync(this.root, {
9341
+ recursive: true,
9342
+ mode: 448
9343
+ });
9344
+ const effective = {
9345
+ ...RELEASE_CONNECTION,
9346
+ ...overrides,
9347
+ ...this.environment
9348
+ };
9349
+ rmSync(join(this.stateRoot(effective), "operator.json"), { force: true });
9350
+ const temporary = `${this.path}.${randomUUID()}.tmp`;
9351
+ writeFileSync(temporary, JSON.stringify(overrides, null, 2) + "\n", {
9352
+ mode: 384,
9353
+ flag: "wx"
9354
+ });
9355
+ try {
9356
+ renameSync(temporary, this.path);
9357
+ } finally {
9358
+ rmSync(temporary, { force: true });
9359
+ }
9360
+ return this.view();
9361
+ }
9362
+ };
9363
+ /** A custom service gets its own identities, keys and runtime configuration. */
9364
+ function connectionStateRoot(root, settings) {
9365
+ const identity = [settings.apiUrl.replace(/\/$/u, ""), settings.issuer.replace(/\/$/u, "")];
9366
+ if (identity[0] === RELEASE_CONNECTION.apiUrl && identity[1] === RELEASE_CONNECTION.issuer) return root;
9367
+ return join(root, "environments", createHash("sha256").update(JSON.stringify(identity)).digest("hex"));
8874
9368
  }
8875
9369
  //#endregion
8876
9370
  //#region src/lib/agent-server/lock.ts
@@ -8929,162 +9423,249 @@ async function withAgentServerLock(root, work, options) {
8929
9423
  }
8930
9424
  }
8931
9425
  //#endregion
8932
- //#region src/lib/agent-server/pairing.ts
9426
+ //#region src/lib/agent-server/native-grant.ts
9427
+ /**
9428
+ * Environment variable the supervising desktop app uses to hand this server
9429
+ * process its control token.
9430
+ *
9431
+ * The parent generates the token, passes it here, and keeps it in native
9432
+ * memory — it is never written to disk, never printed to stdout (the desktop
9433
+ * app surfaces server output in its WebView), and never reaches the renderer.
9434
+ */
9435
+ var NATIVE_TOKEN_ENV = "MOLTNET_AGENT_SERVER_NATIVE_TOKEN";
9436
+ /** 32 bytes of entropy, base64url-encoded, is 43 characters. */
9437
+ var MIN_TOKEN_LENGTH = 32;
8933
9438
  /**
8934
- * One-click pairing ceremony for the agent server, mirroring the signer's
8935
- * session/ceremony pattern (#2062 design):
9439
+ * Consume the supervisor's native token from the environment and grant it.
8936
9440
  *
8937
- * 1. Console (allowed origin) POSTs `/v1/pairings` pending pairing bound
8938
- * to that origin, with a one-time confirmation token.
8939
- * 2. Console opens `http://127.0.0.1:<port>/pairings/<id>` in a new tab
8940
- * a navigation-gated local approval page naming the origin.
8941
- * 3. One click POSTs the confirmation form (explicit cross-site rejected;
8942
- * the one-time token is the primary CSRF control).
8943
- * 4. Console claims `/v1/pairings/<id>/claim` from the same origin and
8944
- * receives the bearer token exactly once; only its SHA-256 remains in
8945
- * this supervisor process.
9441
+ * Consuming matters as much as granting: spawned run children inherit this
9442
+ * process environment, so a token left in place would hand every
9443
+ * task-executing agent control of the Agent Server supervising it. The
9444
+ * variable is deleted whether or not the value turns out to be usable.
8946
9445
  *
8947
- * The token exists for shared-machine cross-user protection and to bind
8948
- * "this console session is the operator" — browser-vs-browser isolation is
8949
- * already covered by the loopback-companion origin checks. Grants are
8950
- * deliberately process-scoped: after the listening socket changes owners, a
8951
- * token disclosed to an impostor on that port cannot authenticate to a later
8952
- * supervisor process.
9446
+ * @returns whether a native grant was issued.
8953
9447
  */
8954
- var PENDING_TTL_MS = 600 * 1e3;
8955
- var AgentServerPairingError = class extends Error {
8956
- name = "AgentServerPairingError";
8957
- constructor(code, message) {
8958
- super(message);
8959
- this.code = code;
8960
- }
9448
+ function applyNativeClientGrant(options) {
9449
+ const { nativeGrant, env } = options;
9450
+ const token = env[NATIVE_TOKEN_ENV];
9451
+ delete env[NATIVE_TOKEN_ENV];
9452
+ if (typeof token !== "string" || token.length === 0) return false;
9453
+ if (token.length < MIN_TOKEN_LENGTH) throw new Error(`${NATIVE_TOKEN_ENV} must be at least 32 characters of unguessable entropy`);
9454
+ nativeGrant.grantNative(token);
9455
+ return true;
9456
+ }
9457
+ var NativeGrantError = class extends Error {
9458
+ code = "native_token_invalid";
8961
9459
  };
8962
- function sha256Hex(value) {
8963
- return createHash("sha256").update(value, "utf8").digest("hex");
8964
- }
8965
- function safeEqual(a, b) {
8966
- const left = Buffer.from(a, "utf8");
8967
- const right = Buffer.from(b, "utf8");
8968
- return left.length === right.length && timingSafeEqual(left, right);
8969
- }
8970
- var PairingService = class {
8971
- pending = /* @__PURE__ */ new Map();
8972
- paired = /* @__PURE__ */ new Map();
8973
- constructor(options = {}) {
8974
- this.options = options;
9460
+ var NativeGrantService = class {
9461
+ digest;
9462
+ grantNative(token) {
9463
+ if (!token) throw new NativeGrantError("Native token must not be empty");
9464
+ this.digest = createHash("sha256").update(token).digest();
8975
9465
  }
8976
- now() {
8977
- return this.options.now?.() ?? Date.now();
9466
+ verify(origin, token) {
9467
+ const digest = createHash("sha256").update(token).digest();
9468
+ if (origin !== "moltnet-agent-desktop://native" || !this.digest || !timingSafeEqual(this.digest, digest)) throw new NativeGrantError("Native token is not valid");
9469
+ }
9470
+ };
9471
+ //#endregion
9472
+ //#region src/lib/agent-server/operator-oauth.ts
9473
+ var InvalidOperatorGrantError = class extends Error {};
9474
+ var LOCAL_SCOPE = OPERATOR_OAUTH.localControlScope;
9475
+ /** Trusted native controller owns the verifier, callback and token exchange. */
9476
+ var OperatorOAuth = class {
9477
+ instance = randomUUID();
9478
+ keys;
9479
+ active = false;
9480
+ pending;
9481
+ operator;
9482
+ constructor(config, root, openBrowser = (url) => {
9483
+ const command = process.platform === "darwin" ? "open" : process.platform === "win32" ? "explorer.exe" : "xdg-open";
9484
+ return new Promise((resolve, reject) => {
9485
+ execFile(command, [url], (error) => {
9486
+ if (error) reject(/* @__PURE__ */ new Error("Could not open Console approval"));
9487
+ else resolve();
9488
+ });
9489
+ });
9490
+ }) {
9491
+ this.config = config;
9492
+ this.root = root;
9493
+ this.openBrowser = openBrowser;
9494
+ const endpoints = [
9495
+ config.authorizationUrl,
9496
+ config.tokenUrl,
9497
+ config.jwksUrl
9498
+ ].map((value) => new URL(value));
9499
+ if (endpoints.some((url) => url.username || url.password || url.hash || url.protocol !== "https:" && !(url.protocol === "http:" && isLoopbackHostname(url.hostname))) || endpoints.some((url) => url.origin !== endpoints[0].origin)) throw new Error("OAuth endpoints must share a trusted secure origin");
9500
+ this.keys = createRemoteJWKSet(new URL(config.jwksUrl));
9501
+ try {
9502
+ const value = JSON.parse(readFileSync(join(root, "operator.json"), "utf8"));
9503
+ if (!value || typeof value !== "object" || !("issuer" in value) || !("subject" in value) || typeof value.issuer !== "string" || typeof value.subject !== "string") throw new Error("Invalid operator");
9504
+ this.operator = {
9505
+ issuer: value.issuer,
9506
+ subject: value.subject
9507
+ };
9508
+ } catch (error) {
9509
+ if (error.code !== "ENOENT") throw error;
9510
+ this.operator = null;
9511
+ }
8978
9512
  }
8979
- token() {
8980
- return this.options.randomToken?.() ?? randomBytes(32).toString("base64url");
9513
+ cancel() {
9514
+ this.pending?.abort();
8981
9515
  }
8982
- sweep() {
8983
- const now = this.now();
8984
- for (const [id, pairing] of this.pending) if (pairing.expiresAt <= now) this.pending.delete(id);
9516
+ removeOperator() {
9517
+ this.cancel();
9518
+ rmSync(join(this.root, "operator.json"), { force: true });
9519
+ this.operator = null;
8985
9520
  }
8986
- start(origin) {
8987
- this.sweep();
8988
- const pairingId = randomBytes(12).toString("hex");
8989
- this.pending.set(pairingId, {
8990
- origin,
8991
- confirmToken: this.token(),
8992
- expiresAt: this.now() + PENDING_TTL_MS,
8993
- approved: false,
8994
- bearerToken: null
8995
- });
9521
+ metadata() {
8996
9522
  return {
8997
- pairingId,
8998
- approvalPath: `/pairings/${pairingId}`
9523
+ protocolVersion: OPERATOR_OAUTH.protocolVersion,
9524
+ instance: this.instance,
9525
+ issuer: this.config.issuer,
9526
+ authorizationUrl: this.config.authorizationUrl,
9527
+ tokenUrl: this.config.tokenUrl,
9528
+ clientId: this.config.consoleClientId,
9529
+ operatorConfigured: !!this.operator
8999
9530
  };
9000
9531
  }
9001
- /** Data for the local approval page. */
9002
- approval(pairingId) {
9003
- const pairing = this.require(pairingId);
9004
- if (pairing.approved) throw new AgentServerPairingError("pairing_invalid", "Pairing is already approved");
9532
+ async verify(token, scope, clientId) {
9533
+ const { payload } = await jwtVerify(token, this.keys, {
9534
+ algorithms: ["RS256"],
9535
+ issuer: this.config.issuer,
9536
+ audience: scope === LOCAL_SCOPE ? OPERATOR_OAUTH.localControlAudience : OPERATOR_OAUTH.provisioningAudience,
9537
+ requiredClaims: [
9538
+ "exp",
9539
+ "iat",
9540
+ "sub"
9541
+ ],
9542
+ maxTokenAge: clientId === this.config.nativeClientId ? OPERATOR_OAUTH.nativeLifetimeSeconds : OPERATOR_OAUTH.consoleLifetimeSeconds
9543
+ });
9544
+ const claims = payload.ext;
9545
+ const scopes = typeof payload.scope === "string" ? payload.scope.split(" ") : payload.scp;
9546
+ if (!Array.isArray(scopes) || scopes.length !== 1 || scopes[0] !== scope || payload.client_id !== clientId || claims?.["moltnet:subject_type"] !== "human" || claims["moltnet:identity_id"] !== payload.sub || claims["moltnet:instance"] !== this.instance) throw new InvalidOperatorGrantError("Invalid operator grant");
9005
9547
  return {
9006
- origin: pairing.origin,
9007
- confirmToken: pairing.confirmToken
9548
+ issuer: payload.iss,
9549
+ subject: payload.sub,
9550
+ provisioning: claims["moltnet:provisioning"]
9008
9551
  };
9009
9552
  }
9010
- confirm(pairingId, confirmToken) {
9011
- const pairing = this.require(pairingId);
9012
- if (pairing.approved || !safeEqual(pairing.confirmToken, confirmToken)) throw new AgentServerPairingError("pairing_invalid", "Confirmation token is not valid");
9013
- pairing.approved = true;
9014
- pairing.bearerToken = this.token();
9015
- return { origin: pairing.origin };
9016
- }
9017
- claim(pairingId, origin) {
9018
- const pairing = this.require(pairingId);
9019
- if (pairing.origin !== origin) throw new AgentServerPairingError("pairing_origin_mismatch", "Pairing belongs to a different origin");
9020
- if (!pairing.approved || !pairing.bearerToken) throw new AgentServerPairingError("pairing_not_approved", "Pairing has not been approved yet");
9021
- const token = pairing.bearerToken;
9022
- this.pending.delete(pairingId);
9023
- this.paired.set(origin, sha256Hex(token));
9024
- return { token };
9025
- }
9026
- verify(origin, token) {
9027
- const tokenHash = this.paired.get(origin);
9028
- if (!tokenHash || !safeEqual(tokenHash, sha256Hex(token))) throw new AgentServerPairingError("pairing_token_invalid", "Pairing token is not valid for this origin");
9029
- }
9030
- require(pairingId) {
9031
- this.sweep();
9032
- const pairing = this.pending.get(pairingId);
9033
- if (!pairing) throw new AgentServerPairingError("pairing_not_found", "Pairing was not found or has expired");
9034
- return pairing;
9553
+ async verifyBrowser(token) {
9554
+ const operator = await this.verify(token, LOCAL_SCOPE, this.config.consoleClientId);
9555
+ if (!this.operator || operator.issuer !== this.operator.issuer || operator.subject !== this.operator.subject) throw new InvalidOperatorGrantError("Native operator sign-in required");
9556
+ }
9557
+ async authorize(grant, signal) {
9558
+ if (this.active) throw new Error("An approval is already pending");
9559
+ this.active = true;
9560
+ const state = randomBytes(32).toString("base64url");
9561
+ const verifier = randomBytes(32).toString("base64url");
9562
+ const callback = `http://127.0.0.1:${this.config.callbackPort}/oauth/callback`;
9563
+ const scope = grant ? OPERATOR_OAUTH.provisioningScope : LOCAL_SCOPE;
9564
+ const controller = new AbortController();
9565
+ this.pending = controller;
9566
+ const timeout = setTimeout(() => controller.abort(), OPERATOR_OAUTH.nativeLifetimeSeconds * 1e3);
9567
+ const abort = () => controller.abort();
9568
+ signal?.addEventListener("abort", abort, { once: true });
9569
+ let server;
9570
+ try {
9571
+ if (signal?.aborted) throw new Error("Approval cancelled");
9572
+ const code = await new Promise((resolve, reject) => {
9573
+ let consumed = false;
9574
+ server = createServer((req, res) => {
9575
+ const url = new URL(req.url ?? "/", callback);
9576
+ res.setHeader("Cache-Control", "no-store");
9577
+ res.setHeader("Content-Security-Policy", "default-src 'none'; frame-ancestors 'none'");
9578
+ if (req.method !== "GET" || req.headers.host !== `127.0.0.1:${this.config.callbackPort}` || url.pathname !== "/oauth/callback" || url.searchParams.getAll("state").length !== 1 || url.searchParams.get("state") !== state || consumed) {
9579
+ res.writeHead(400);
9580
+ res.end("Invalid callback");
9581
+ return;
9582
+ }
9583
+ consumed = true;
9584
+ if (url.searchParams.has("error") || url.searchParams.getAll("code").length !== 1 || !url.searchParams.get("code")) {
9585
+ res.end("Approval cancelled. Return to Desktop.");
9586
+ reject(/* @__PURE__ */ new Error("Approval cancelled"));
9587
+ return;
9588
+ }
9589
+ res.end("Approval received. Return to Desktop.");
9590
+ resolve(url.searchParams.get("code"));
9591
+ });
9592
+ server.once("error", reject);
9593
+ controller.signal.addEventListener("abort", () => reject(/* @__PURE__ */ new Error("Approval cancelled")), { once: true });
9594
+ server.listen(this.config.callbackPort, "127.0.0.1", () => {
9595
+ const url = new URL(this.config.authorizationUrl);
9596
+ for (const [key, value] of Object.entries({
9597
+ client_id: this.config.nativeClientId,
9598
+ response_type: "code",
9599
+ scope,
9600
+ audience: grant ? OPERATOR_OAUTH.provisioningAudience : OPERATOR_OAUTH.localControlAudience,
9601
+ redirect_uri: callback,
9602
+ state,
9603
+ code_challenge_method: "S256",
9604
+ code_challenge: createHash("sha256").update(verifier).digest("base64url"),
9605
+ prompt: "consent",
9606
+ instance: this.instance,
9607
+ ...grant ? { provisioning: JSON.stringify(grant) } : {}
9608
+ })) url.searchParams.set(key, value);
9609
+ try {
9610
+ Promise.resolve(this.openBrowser(url.href)).catch(() => reject(/* @__PURE__ */ new Error("Could not open Console approval")));
9611
+ } catch {
9612
+ reject(/* @__PURE__ */ new Error("Could not open Console approval"));
9613
+ }
9614
+ });
9615
+ });
9616
+ const response = await fetch(this.config.tokenUrl, {
9617
+ method: "POST",
9618
+ redirect: "error",
9619
+ signal: controller.signal,
9620
+ headers: { "content-type": "application/x-www-form-urlencoded" },
9621
+ body: new URLSearchParams({
9622
+ grant_type: "authorization_code",
9623
+ code,
9624
+ code_verifier: verifier,
9625
+ client_id: this.config.nativeClientId,
9626
+ redirect_uri: callback
9627
+ })
9628
+ });
9629
+ if (!response.ok) throw new Error("Approval exchange lost; request fresh approval");
9630
+ const tokens = await response.json();
9631
+ if (!tokens.access_token || tokens.refresh_token) throw new Error("Invalid approval token response");
9632
+ const operator = await this.verify(tokens.access_token, scope, this.config.nativeClientId);
9633
+ controller.signal.throwIfAborted();
9634
+ if (grant) {
9635
+ const actual = operator.provisioning;
9636
+ if (!actual || !Array.isArray(actual.scopes) || !actual.scopes.every((scope) => typeof scope === "string") || actual.agentId !== grant.agentId || actual.teamId !== grant.teamId || actual.operation !== grant.operation || actual.idempotencyKey !== grant.idempotencyKey || [...actual.scopes].sort().join(" ") !== [...grant.scopes].sort().join(" ")) throw new Error("Approval target differs from native request");
9637
+ }
9638
+ if (this.operator && (this.operator.issuer !== operator.issuer || this.operator.subject !== operator.subject)) throw new Error("Change the operator through native administration first");
9639
+ if (!this.operator) {
9640
+ try {
9641
+ writeFileSync(join(this.root, "operator.json"), JSON.stringify({
9642
+ issuer: operator.issuer,
9643
+ subject: operator.subject
9644
+ }), {
9645
+ mode: 384,
9646
+ flag: "wx"
9647
+ });
9648
+ } catch (error) {
9649
+ if (error.code !== "EEXIST") throw error;
9650
+ const pinned = JSON.parse(readFileSync(join(this.root, "operator.json"), "utf8"));
9651
+ if (!pinned || typeof pinned !== "object" || !("issuer" in pinned) || !("subject" in pinned) || pinned.issuer !== operator.issuer || pinned.subject !== operator.subject) throw new Error("Change the operator through native administration first");
9652
+ }
9653
+ this.operator = {
9654
+ issuer: operator.issuer,
9655
+ subject: operator.subject
9656
+ };
9657
+ }
9658
+ return tokens.access_token;
9659
+ } finally {
9660
+ clearTimeout(timeout);
9661
+ signal?.removeEventListener("abort", abort);
9662
+ server?.close();
9663
+ server?.closeAllConnections();
9664
+ this.active = false;
9665
+ this.pending = void 0;
9666
+ }
9035
9667
  }
9036
9668
  };
9037
- function escapeHtml(value) {
9038
- return value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll("\"", "&quot;");
9039
- }
9040
- /** Minimal, dependency-free local approval page. */
9041
- function renderPairingApprovalPage(input) {
9042
- return `<!doctype html>
9043
- <html lang="en">
9044
- <head>
9045
- <meta charset="utf-8" />
9046
- <meta name="viewport" content="width=device-width, initial-scale=1" />
9047
- <title>MoltNet Agent — approve connection</title>
9048
- <style>
9049
- :root { color-scheme: light dark; }
9050
- body { margin: 0; font: 16px/1.5 system-ui, sans-serif; display: grid; place-items: center; min-height: 100vh; background: Canvas; color: CanvasText; }
9051
- main { max-width: 26rem; padding: 2rem; border: 1px solid color-mix(in srgb, CanvasText 20%, transparent); border-radius: 12px; }
9052
- h1 { font-size: 1.2rem; margin: 0 0 0.5rem; }
9053
- code { font-size: 0.95em; word-break: break-all; }
9054
- button { margin-top: 1.25rem; font: inherit; padding: 0.6rem 1.4rem; border-radius: 8px; border: 1px solid color-mix(in srgb, CanvasText 30%, transparent); cursor: pointer; }
9055
- p.small { font-size: 0.85rem; opacity: 0.75; }
9056
- </style>
9057
- </head>
9058
- <body>
9059
- <script>
9060
- // The Console must remain this popup's opener until it finishes navigating
9061
- // from about:blank. Safari rejects that cross-origin navigation otherwise.
9062
- // Once this trusted local approval document has loaded, it needs no opener.
9063
- window.opener = null;
9064
- <\/script>
9065
- <main>
9066
- <h1>Allow this site to manage local MoltNet agents?</h1>
9067
- <p><code>${escapeHtml(input.origin)}</code> asks to configure agents and start or stop local daemon runs on this machine.</p>
9068
- <p class="small">Approve only if you opened that page yourself. This grant lasts until the local supervisor stops.</p>
9069
- <form method="post" action="/pairings/${escapeHtml(input.pairingId)}/confirm">
9070
- <input type="hidden" name="confirmToken" value="${escapeHtml(input.confirmToken)}" />
9071
- <button type="submit">Approve</button>
9072
- </form>
9073
- </main>
9074
- </body>
9075
- </html>
9076
- `;
9077
- }
9078
- function renderPairingResultPage(input) {
9079
- return `<!doctype html>
9080
- <html lang="en">
9081
- <head><meta charset="utf-8" /><meta name="viewport" content="width=device-width, initial-scale=1" /><title>${escapeHtml(input.title)}</title>
9082
- <style>:root{color-scheme:light dark}body{margin:0;font:16px/1.5 system-ui,sans-serif;display:grid;place-items:center;min-height:100vh;background:Canvas;color:CanvasText}main{max-width:26rem;padding:2rem}</style>
9083
- </head>
9084
- <body><main role="status"><h1>${escapeHtml(input.title)}</h1><p>${escapeHtml(input.message)}</p><p>You can close this tab.</p></main></body>
9085
- </html>
9086
- `;
9087
- }
9088
9669
  //#endregion
9089
9670
  //#region src/lib/agent-server/provider-login.ts
9090
9671
  /**
@@ -9540,9 +10121,10 @@ async function createManagedAgent(store, secrets, input, connectAgent = connect)
9540
10121
  */
9541
10122
  function incompleteRegistrationMessage(alias, known) {
9542
10123
  const endpoint = `POST /v1/agents/${alias}/reconcile`;
10124
+ const recovery = known.recoveryPath ? ` Inspect the protected recovery file at ${known.recoveryPath}; it may be incomplete.` : "";
9543
10125
  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}`;
10126
+ if (known.subjectId) return `the remote agent ${known.subjectId} was registered but local activation is incomplete. ${reconcile}${recovery}`;
10127
+ return `registration for "${alias}" may have completed on the server${known.fingerprint ? ` (fingerprint ${known.fingerprint})` : ""}; look the agent up first. ${reconcile}${recovery}`;
9546
10128
  }
9547
10129
  /** Resume a fully persisted registration or explicitly abandon local recovery. */
9548
10130
  async function reconcileManagedRegistration(store, secrets, aliasInput, action, connectAgent = connect, signal) {
@@ -9563,8 +10145,9 @@ async function reconcileManagedRegistration(store, secrets, aliasInput, action,
9563
10145
  store.clearPendingRegistration(alias);
9564
10146
  return null;
9565
10147
  }
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)]);
10148
+ const reference = config ? selectAgentKeyReference(config)?.reference : void 0;
10149
+ 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`);
10150
+ const [agentKey, privateKeyState] = await Promise.all([secrets.read(reference.key), secrets.probe(config.keys.private_key_ref.key)]);
9568
10151
  if (!agentKey || privateKeyState !== "present") throw new AgentServerIdentityError("registration_incomplete", `pending registration for "${alias}" is missing persisted secret material`);
9569
10152
  const apiUrl = requireConfigApiUrl(config, store.agentPath(alias));
9570
10153
  const whoami = await callWhoami(connectAgent, {
@@ -9600,9 +10183,9 @@ async function attachExternalAgent(store, secretProviders, input, connectAgent =
9600
10183
  try {
9601
10184
  if (!central) externalAgentLocation(configPath);
9602
10185
  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`);
10186
+ 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
10187
  const configApiUrl = requireTrustedConfigApiUrl(config, configPath);
9605
- const whoami = await authenticateConfig(config, configPath, requireTrustedApiOverride(input.apiUrl, configApiUrl, configPath), secretProviders, connectAgent, input.signal);
10188
+ const whoami = await authenticateConfig(config, configPath, requireTrustedApiOverride(input.apiUrl, configApiUrl, configPath), secretProviders, connectAgent, input.signal, input.teamId);
9606
10189
  const identity = identityFromConfig(config);
9607
10190
  assertIdentityMatches(identity, whoami, `external config ${configPath}`, "authenticated whoami");
9608
10191
  assertSubjectMatches(whoami, config, "authenticated whoami", `external config ${configPath}`);
@@ -9628,62 +10211,6 @@ async function attachExternalAgent(store, secretProviders, input, connectAgent =
9628
10211
  releaseAlias();
9629
10212
  }
9630
10213
  }
9631
- /** Load and authenticate the current config, then refresh its derived pin. */
9632
- async function verifyAgentActivation(store, alias, managedSecretProviders, externalSecretProviders, connectAgent = connect, signal) {
9633
- 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);
9635
- assertSubjectMatches(verified.whoami, verified.config, "authenticated whoami", `agent "${activation.alias}" config`);
9636
- if (verified.whoami.subjectId !== activation.subjectId) throw new AgentServerIdentityError("verification_failed", `authenticated whoami subject does not match agent "${activation.alias}" pinned activation`);
9637
- const identity = identityFromConfig(verified.config);
9638
- assertIdentityMatches(verified.whoami, identity, "authenticated whoami", `agent "${activation.alias}" config`);
9639
- 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`);
9641
- const refreshed = {
9642
- ...activation,
9643
- ...identity
9644
- };
9645
- if (activation.publicKey !== refreshed.publicKey || activation.fingerprint !== refreshed.fingerprint) {
9646
- store.writeActivation(refreshed);
9647
- process.stderr.write(`agent-server: refreshed the authenticated signing identity for ${JSON.stringify(activation.alias)}\n`);
9648
- }
9649
- return {
9650
- activation: refreshed,
9651
- config: verified.config,
9652
- ...boundTeamId ? { boundTeamId } : {}
9653
- };
9654
- }
9655
- async function verifyManagedActivation(store, activation, secretProviders, connectAgent, signal) {
9656
- const configPath = store.agentPath(activation.alias);
9657
- const config = await readCurrentConfig(configPath);
9658
- 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
- return {
9667
- config,
9668
- whoami: await callWhoami(connectAgent, {
9669
- agentKey,
9670
- apiUrl: activation.apiUrl
9671
- }, configPath, signal)
9672
- };
9673
- }
9674
- async function verifyExternalActivation(store, activation, secretProviders, connectAgent, signal) {
9675
- const central = activation.configPath === store.agentPath(activation.alias);
9676
- if (!central) externalAgentLocation(activation.configPath);
9677
- assertTrustedConfigApiUrl(activation.configApiUrl);
9678
- 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`);
9680
- assertActivatedConfig(config, activation, activation.configPath, requireTrustedConfigApiUrl(config, activation.configPath), activation.configApiUrl);
9681
- const effectiveApiUrl = requireTrustedApiOverride(activation.apiUrl, activation.configApiUrl, activation.configPath);
9682
- return {
9683
- config,
9684
- whoami: await authenticateConfig(config, activation.configPath, effectiveApiUrl, secretProviders, connectAgent, signal)
9685
- };
9686
- }
9687
10214
  /** Never let request or activation metadata redirect persisted credentials. */
9688
10215
  function requireTrustedApiOverride(override, configApiUrl, configPath) {
9689
10216
  if (!override) return configApiUrl;
@@ -9741,18 +10268,20 @@ function externalAgentLocation(configPath) {
9741
10268
  agentRoot: dirname(moltnetDir)
9742
10269
  };
9743
10270
  }
9744
- async function authenticateConfig(config, configPath, apiUrl, secretProviders, connectAgent, signal) {
10271
+ async function authenticateConfig(config, configPath, apiUrl, secretProviders, connectAgent, signal, teamId) {
9745
10272
  let agentKey;
9746
10273
  try {
9747
- agentKey = await resolveAgentKey(config, secretProviders);
10274
+ agentKey = await resolveAgentKey(config, secretProviders, teamId);
9748
10275
  } catch (cause) {
9749
- throw verificationError(`could not resolve the daemon agent key from ${configPath}`, cause);
10276
+ throw verificationError(`could not resolve the daemon agent key from ${configPath}; select a team and enroll or repair its stored key`, cause);
9750
10277
  }
9751
10278
  if (!agentKey) throw new AgentServerIdentityError("unsupported_credential", `external daemon config at ${configPath} must contain an agent_key_ref`);
9752
- return callWhoami(connectAgent, {
10279
+ const whoami = await callWhoami(connectAgent, {
9753
10280
  agentKey,
9754
10281
  ...apiUrl ? { apiUrl } : {}
9755
10282
  }, configPath, signal);
10283
+ if (!matchesCredentialTeam(whoami, selectAgentKeyReference(config, teamId)?.teamId)) throw new AgentServerIdentityError("verification_failed", "authenticated credential team binding does not match the selected team");
10284
+ return whoami;
9756
10285
  }
9757
10286
  async function callWhoami(connectAgent, options, source, signal) {
9758
10287
  try {
@@ -9797,7 +10326,7 @@ function publicAgentView(store, activation) {
9797
10326
  ...activation.boundTeamId ? { teamId: activation.boundTeamId } : {},
9798
10327
  apiUrl: activation.apiUrl,
9799
10328
  createdAt: activation.createdAt,
9800
- hasAgentKey: Boolean(config?.agent_key_ref),
10329
+ hasAgentKey: Boolean(config && hasAgentKeyConfiguration(config)),
9801
10330
  hasPrivateKey: Boolean(config?.keys.private_key_ref)
9802
10331
  };
9803
10332
  }
@@ -9811,14 +10340,137 @@ function publicAgentView(store, activation) {
9811
10340
  ...activation.boundTeamId ? { teamId: activation.boundTeamId } : {},
9812
10341
  createdAt: activation.createdAt
9813
10342
  };
9814
- }
9815
- function boundTeamIdFromWhoami(whoami) {
9816
- return whoami.credentialBinding?.bindingScope === "team" ? whoami.credentialBinding.boundTeamId ?? void 0 : void 0;
9817
- }
9818
- function requireActivation(store, alias) {
9819
- const activation = store.readActivation(alias);
9820
- if (!activation) throw new AgentServerStoreError("not_found", `agent "${alias}" is not configured`);
9821
- return activation;
10343
+ }
10344
+ function boundTeamIdFromWhoami(whoami) {
10345
+ return whoami.credentialBinding?.bindingScope === "team" ? whoami.credentialBinding.boundTeamId ?? void 0 : void 0;
10346
+ }
10347
+ function requireActivation(store, alias) {
10348
+ const activation = store.readActivation(alias);
10349
+ if (!activation) throw new AgentServerStoreError("not_found", `agent "${alias}" is not configured`);
10350
+ return activation;
10351
+ }
10352
+ /** Enrollment may recover a central identity before its first online activation. */
10353
+ async function loadEnrollmentIdentity(store, alias) {
10354
+ if (store.readActivation(alias)) return loadAgentActivation(store, alias);
10355
+ const configPath = store.agentPath(alias);
10356
+ const config = await readCurrentConfig(configPath);
10357
+ if (!isCanonicalConfig(config)) throw new AgentServerIdentityError("verification_failed", "Enrollment requires a canonical agent identity");
10358
+ const apiUrl = requireTrustedConfigApiUrl(config, configPath);
10359
+ return {
10360
+ config,
10361
+ activation: {
10362
+ source: "external",
10363
+ alias,
10364
+ configPath,
10365
+ configApiUrl: apiUrl,
10366
+ apiUrl,
10367
+ subjectId: config.subject_id,
10368
+ ...identityFromConfig(config),
10369
+ createdAt: config.registered_at
10370
+ }
10371
+ };
10372
+ }
10373
+ /** Resolve pinned local identity state without requiring a live API credential. */
10374
+ async function loadAgentActivation(store, alias) {
10375
+ const activation = requireActivation(store, alias);
10376
+ const configPath = activation.source === "managed" ? store.agentPath(alias) : activation.configPath;
10377
+ if (activation.source === "external" && configPath !== store.agentPath(alias)) externalAgentLocation(configPath);
10378
+ const config = await readCurrentConfig(configPath);
10379
+ const apiUrl = activation.source === "managed" ? activation.apiUrl : activation.configApiUrl;
10380
+ assertActivatedConfig(config, activation, configPath, requireTrustedConfigApiUrl(config, configPath), apiUrl);
10381
+ requireTrustedApiOverride(activation.apiUrl, apiUrl, configPath);
10382
+ return {
10383
+ activation,
10384
+ config
10385
+ };
10386
+ }
10387
+ //#endregion
10388
+ //#region src/lib/agent-server/team-credentials.ts
10389
+ var AGENT_SERVER_REQUIRED_SCOPES = [
10390
+ ...DAEMON_MINIMUM_SCOPES,
10391
+ "team:read",
10392
+ "diary:read"
10393
+ ];
10394
+ var TeamCredentialError = class extends Error {
10395
+ constructor(blocker) {
10396
+ super(blocker.message);
10397
+ this.blocker = blocker;
10398
+ }
10399
+ };
10400
+ function credentialBlocker(error) {
10401
+ return error instanceof TeamCredentialError ? error.blocker : {
10402
+ code: "agent_key_unavailable",
10403
+ message: "This team credential could not be verified or read its team resources.",
10404
+ remedy: "Check connectivity and team access, or renew this team credential."
10405
+ };
10406
+ }
10407
+ var snapshots = /* @__PURE__ */ new WeakMap();
10408
+ /** Capture is internal to the verifier; exported for injected verifier test doubles. */
10409
+ function captureTeamCredential(agent, snapshot) {
10410
+ snapshots.set(agent, snapshot);
10411
+ return agent;
10412
+ }
10413
+ function requireCredentialSnapshot(agent) {
10414
+ const snapshot = snapshots.get(agent);
10415
+ if (!snapshot) throw new Error("A verified team credential snapshot is required");
10416
+ return snapshot;
10417
+ }
10418
+ /** The only supervised credential path. No fallback reference or OAuth resolution. */
10419
+ async function verifyTeamActivation(store, alias, managed, external, connectImpl = connect, signal, teamId) {
10420
+ const activated = await loadAgentActivation(store, alias);
10421
+ const { config, activation } = activated;
10422
+ const reference = teamId ? config.agent_key_refs?.[teamId] : void 0;
10423
+ if (!teamId || !reference) throw new TeamCredentialError({
10424
+ code: "agent_key_missing",
10425
+ message: "No credential is indexed for this team.",
10426
+ remedy: "Enroll into this team or explicitly index its existing team-bound credential."
10427
+ });
10428
+ let agentKey;
10429
+ try {
10430
+ const resolved = await resolveAgentKey({
10431
+ ...config,
10432
+ agent_key_ref: void 0,
10433
+ agent_key_refs: { [teamId]: reference }
10434
+ }, activation.source === "managed" ? managed : external, teamId);
10435
+ if (!resolved) throw new Error("Missing selected key");
10436
+ agentKey = resolved;
10437
+ } catch {
10438
+ throw new TeamCredentialError({
10439
+ code: "agent_key_unavailable",
10440
+ message: "The selected team credential is unavailable.",
10441
+ remedy: "Repair its secret provider or renew this team credential."
10442
+ });
10443
+ }
10444
+ const client = await connectImpl({
10445
+ agentKey,
10446
+ apiUrl: activation.apiUrl ?? (activation.source === "external" ? activation.configApiUrl : void 0),
10447
+ signal
10448
+ });
10449
+ const whoami = await client.agents.whoami({ signal });
10450
+ if (whoami.subjectType !== "agent" || whoami.subjectId !== activation.subjectId || whoami.publicKey !== config.keys.public_key || whoami.fingerprint !== config.keys.fingerprint || whoami.publicKey !== activation.publicKey || whoami.fingerprint !== activation.fingerprint || whoami.credentialBinding?.bindingScope !== "team" || whoami.credentialBinding.boundTeamId !== teamId) throw new TeamCredentialError({
10451
+ code: "agent_key_binding_invalid",
10452
+ message: "The selected credential does not match this identity and team.",
10453
+ remedy: "Renew the selected team credential."
10454
+ });
10455
+ const metadata = {
10456
+ keyId: whoami.credentialBinding.keyId,
10457
+ ...Object.hasOwn(whoami.credentialBinding, "expiresAt") ? { expiresAt: whoami.credentialBinding.expiresAt } : {},
10458
+ scopes: [...whoami.scopes ?? []],
10459
+ verifiedAt: (/* @__PURE__ */ new Date()).toISOString()
10460
+ };
10461
+ store.writeCredentialMetadata(alias, teamId, metadata);
10462
+ const missing = AGENT_SERVER_REQUIRED_SCOPES.filter((scope) => !metadata.scopes.includes(scope));
10463
+ if (missing.length) throw new TeamCredentialError({
10464
+ code: "agent_key_scopes_insufficient",
10465
+ message: `This credential lacks ${missing.join(", ")}.`,
10466
+ remedy: "Renew through Console approval with the required desktop scopes."
10467
+ });
10468
+ activated.boundTeamId = teamId;
10469
+ return captureTeamCredential(activated, {
10470
+ agentKey,
10471
+ client,
10472
+ metadata
10473
+ });
9822
10474
  }
9823
10475
  //#endregion
9824
10476
  //#region src/lib/agent-server/runs.ts
@@ -9859,6 +10511,19 @@ var INHERITED_MOLTNET_ENV_NAMES = new Set([
9859
10511
  "MOLTNET_SIGNER_URL",
9860
10512
  "MOLTNET_TRACE_IDLE_POLLING"
9861
10513
  ]);
10514
+ /** The runtime kind bundled with the agent; needs no registration. */
10515
+ var BUILT_IN_RUNTIME_KIND = "gondolin_pi";
10516
+ /** Persist only process status; worker stderr can contain provider secrets. */
10517
+ function describeFailure(code, signal) {
10518
+ if (signal) return {
10519
+ code: "run_signalled",
10520
+ message: `The worker was stopped by ${signal}.`
10521
+ };
10522
+ return {
10523
+ code: "run_failed",
10524
+ message: `The worker exited with code ${code ?? "unknown"}. Open the log for detail.`
10525
+ };
10526
+ }
9862
10527
  var AgentServerRunError = class extends Error {
9863
10528
  name = "AgentServerRunError";
9864
10529
  constructor(code, message) {
@@ -9941,22 +10606,16 @@ var RunManager = class {
9941
10606
  String(runtimeSettings.warmRetentionSec),
9942
10607
  ...target.extraArgs
9943
10608
  ];
10609
+ env["MOLTNET_AGENT_KEY"] = requireCredentialSnapshot(agent).agentKey;
10610
+ env["MOLTNET_API_URL"] = activation.apiUrl ?? (activation.source === "external" ? activation.configApiUrl : "");
9944
10611
  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`);
9946
- env["MOLTNET_API_URL"] = activation.apiUrl;
9947
- env["MOLTNET_AGENT_KEY_REF"] = formatSecretReferenceString(config.agent_key_ref);
10612
+ if (!config.keys.private_key_ref) throw new AgentServerRunError("invalid_spec", "The managed signing key reference is missing");
9948
10613
  env["MOLTNET_PRIVATE_KEY_REF"] = formatSecretReferenceString(config.keys.private_key_ref);
9949
10614
  env["MOLTNET_SECRET_ROOT"] = this.store.secretsDir;
9950
- } else {
9951
- env["MOLTNET_API_URL"] = activation.apiUrl ?? activation.configApiUrl;
9952
- try {
9953
- const agentKey = await resolveAgentKey(config, this.options.externalSecretProviders);
9954
- if (!agentKey) throw new Error("external daemon config has no agent key");
9955
- env["MOLTNET_AGENT_KEY"] = agentKey;
9956
- env["MOLTNET_PRIVATE_KEY"] = await resolveIdentitySeed(config, this.options.externalSecretProviders);
9957
- } catch {
9958
- throw new AgentServerRunError("invalid_spec", `external credentials for "${activation.alias}" could not be projected`);
9959
- }
10615
+ } else try {
10616
+ env["MOLTNET_PRIVATE_KEY"] = await resolveIdentitySeed(config, this.options.externalSecretProviders);
10617
+ } catch {
10618
+ throw new AgentServerRunError("invalid_spec", "The selected signing key could not be projected");
9960
10619
  }
9961
10620
  env["MOLTNET_EXPECTED_SUBJECT_ID"] = activation.subjectId;
9962
10621
  env["MOLTNET_EXPECTED_SUBJECT_TYPE"] = "agent";
@@ -9990,7 +10649,10 @@ var RunManager = class {
9990
10649
  }
9991
10650
  async startReserved(spec, signal) {
9992
10651
  this.assertStartOpen(signal);
9993
- const agent = await (this.options.verifyActivationImpl ?? verifyAgentActivation)(this.store, spec.agent, this.options.secretProviders, this.options.externalSecretProviders, void 0, signal);
10652
+ const agent = await (this.options.verifyActivationImpl ?? verifyTeamActivation)(this.store, spec.agent, this.options.secretProviders, this.options.externalSecretProviders, void 0, signal, spec.teamId).catch((cause) => {
10653
+ if (cause instanceof TeamCredentialError || cause instanceof AgentServerStoreError) throw cause;
10654
+ throw new AgentServerIdentityError("verification_failed", `Cannot start agent "${spec.agent}" for team "${spec.teamId}": credential verification failed. Check the selected team key and activation.`);
10655
+ });
9994
10656
  this.assertStartOpen(signal);
9995
10657
  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
10658
  const id = `${Date.now().toString(36)}-${randomBytes(4).toString("hex")}`;
@@ -10066,7 +10728,8 @@ var RunManager = class {
10066
10728
  id,
10067
10729
  status: "running",
10068
10730
  pid: child.pid,
10069
- startedAt: (this.options.now?.() ?? /* @__PURE__ */ new Date()).toISOString()
10731
+ startedAt: (this.options.now?.() ?? /* @__PURE__ */ new Date()).toISOString(),
10732
+ credential: requireCredentialSnapshot(agent).metadata
10070
10733
  };
10071
10734
  child.once("exit", (code, signal) => {
10072
10735
  const activeRun = this.active.get(id);
@@ -10081,7 +10744,8 @@ var RunManager = class {
10081
10744
  });
10082
10745
  this.persistRunCompletion(id, spec.agent, {
10083
10746
  status,
10084
- exitCode: code
10747
+ exitCode: code,
10748
+ ...status === "failed" ? { lastError: describeFailure(code, signal) } : {}
10085
10749
  });
10086
10750
  });
10087
10751
  child.once("error", (error) => {
@@ -10092,7 +10756,13 @@ var RunManager = class {
10092
10756
  transition: "spawn_failed",
10093
10757
  ...safeRunError(error)
10094
10758
  });
10095
- this.persistRunCompletion(id, spec.agent, { status: "failed" });
10759
+ this.persistRunCompletion(id, spec.agent, {
10760
+ status: "failed",
10761
+ lastError: {
10762
+ code: "spawn_failed",
10763
+ message: `The worker could not be started: ${error.message}`
10764
+ }
10765
+ });
10096
10766
  });
10097
10767
  this.store.writeRun(record);
10098
10768
  this.log("info", "agent server run started", {
@@ -10119,7 +10789,7 @@ var RunManager = class {
10119
10789
  }
10120
10790
  async resolveRuntimeModule(spec, activated, cwd) {
10121
10791
  const profiles = await resolveRuntimeProfiles({
10122
- agent: await this.connectActivatedAgent(activated),
10792
+ agent: await this.connectAgent(activated, spec.teamId),
10123
10793
  profiles: spec.profiles,
10124
10794
  teamId: spec.teamId,
10125
10795
  cwd
@@ -10141,22 +10811,9 @@ var RunManager = class {
10141
10811
  if (kind === "gondolin_pi") return void 0;
10142
10812
  throw new AgentServerRunError("invalid_spec", `No local runtime is registered for profile kind "${kind}".`);
10143
10813
  }
10144
- async connectActivatedAgent(activated) {
10145
- 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`);
10156
- return connect({
10157
- agentKey,
10158
- apiUrl: activation.apiUrl ?? activation.configApiUrl
10159
- });
10814
+ connectAgent(activated, teamId) {
10815
+ if (activated.boundTeamId !== teamId) throw new AgentServerRunError("invalid_spec", "Snapshot team mismatch");
10816
+ return Promise.resolve(requireCredentialSnapshot(activated).client);
10160
10817
  }
10161
10818
  stop(id) {
10162
10819
  const record = this.store.readRun(id);
@@ -10181,6 +10838,22 @@ var RunManager = class {
10181
10838
  if (!record) throw new AgentServerStoreError("not_found", `run "${id}" was not found`);
10182
10839
  return record;
10183
10840
  }
10841
+ async listAsync(limit) {
10842
+ const activeIds = new Set(this.active.keys());
10843
+ const records = await this.store.listRunsAsync(limit + activeIds.size, [...activeIds]);
10844
+ return [...records.filter((record) => activeIds.has(record.id)), ...records.filter((record) => !activeIds.has(record.id)).slice(0, limit)];
10845
+ }
10846
+ /** Freeze new starts only after local configuration has been persisted. */
10847
+ prepareServerRestart(persist) {
10848
+ if (this.closing || this.starting > 0 || this.active.size > 0) throw new AgentServerRunError("invalid_spec", "Stop running or starting work before changing connection settings");
10849
+ this.closing = true;
10850
+ try {
10851
+ return persist();
10852
+ } catch (error) {
10853
+ this.closing = false;
10854
+ throw error;
10855
+ }
10856
+ }
10184
10857
  list(limit = Number.POSITIVE_INFINITY) {
10185
10858
  if (!Number.isFinite(limit)) return this.store.listRuns();
10186
10859
  const active = [...this.active.keys()].map((id) => this.store.readRun(id)).filter((record) => record !== null).sort((a, b) => b.startedAt.localeCompare(a.startedAt));
@@ -10437,6 +11110,21 @@ var LOCKFILE_NAMES = [
10437
11110
  ];
10438
11111
  /** Local, operator-owned allowlist for executable daemon runtime modules. */
10439
11112
  var RuntimeRegistry = class {
11113
+ displayDigests = /* @__PURE__ */ new Map();
11114
+ displayHash(path) {
11115
+ const stat = statSync(path);
11116
+ const key = String(path);
11117
+ const stamp = `${stat.dev}:${stat.ino}:${stat.size}:${stat.mtimeMs}:${stat.ctimeMs}`;
11118
+ const previous = this.displayDigests.get(key);
11119
+ if (previous?.stamp === stamp) return previous.hash;
11120
+ const hash = hashPath(path);
11121
+ if (this.displayDigests.size >= 128) this.displayDigests.clear();
11122
+ this.displayDigests.set(key, {
11123
+ stamp,
11124
+ hash
11125
+ });
11126
+ return hash;
11127
+ }
10440
11128
  constructor(root) {
10441
11129
  this.root = root;
10442
11130
  }
@@ -10459,7 +11147,7 @@ var RuntimeRegistry = class {
10459
11147
  const adapter = await loadDaemonRuntimeAdapter(specifier, { cwd });
10460
11148
  if (adapter.runtimeKind !== kind) throw new Error(`Runtime module provides "${adapter.runtimeKind}", not registered kind "${kind}".`);
10461
11149
  if (!moduleUrl.startsWith("file:")) throw new Error("Runtime registration must resolve to a local file URL.");
10462
- const entryHash = hashFile(new URL(moduleUrl));
11150
+ const entryHash = hashPath(new URL(moduleUrl));
10463
11151
  const lockfilePath = isPackageSpecifier(specifier) ? findLockfile(cwd) : void 0;
10464
11152
  if (isPackageSpecifier(specifier) && !lockfilePath) throw new Error("Package runtime registration requires a pnpm, npm, Yarn, or Bun lockfile in the current project or a parent directory.");
10465
11153
  const entry = {
@@ -10485,18 +11173,16 @@ var RuntimeRegistry = class {
10485
11173
  writeRegistry(this.path, next);
10486
11174
  return true;
10487
11175
  }
10488
- resolve(kind) {
11176
+ resolve(kind, options = {}) {
10489
11177
  kind = assertStoreName("runtime kind", kind);
10490
11178
  const entry = this.list().find((candidate) => candidate.kind === kind);
10491
11179
  if (!entry) return void 0;
10492
- if (hashFile(new URL(entry.moduleUrl)) !== entry.entryHash) throw new Error(`Registered runtime "${kind}" has changed; re-register it before starting a run.`);
10493
- if (entry.lockfilePath && (!existsSync(entry.lockfilePath) || hashPath(entry.lockfilePath) !== entry.lockfileHash)) throw new Error(`Registered runtime "${kind}" lockfile has changed; re-register it before starting a run.`);
11180
+ const digest = (path) => options.forDisplay ? this.displayHash(path) : hashPath(path);
11181
+ if (digest(new URL(entry.moduleUrl)) !== entry.entryHash) throw new Error(`Registered runtime "${kind}" has changed; re-register it before starting a run.`);
11182
+ if (entry.lockfilePath && (!existsSync(entry.lockfilePath) || digest(entry.lockfilePath) !== entry.lockfileHash)) throw new Error(`Registered runtime "${kind}" lockfile has changed; re-register it before starting a run.`);
10494
11183
  return entry;
10495
11184
  }
10496
11185
  };
10497
- function hashFile(url) {
10498
- return createHash("sha256").update(readFileSync(url)).digest("hex");
10499
- }
10500
11186
  function hashPath(path) {
10501
11187
  return createHash("sha256").update(readFileSync(path)).digest("hex");
10502
11188
  }
@@ -10526,6 +11212,229 @@ function writeRegistry(path, entries) {
10526
11212
  renameSync(temp, path);
10527
11213
  }
10528
11214
  //#endregion
11215
+ //#region src/lib/agent-server/readiness.ts
11216
+ /**
11217
+ * Whether a runtime profile can execute on *this* machine.
11218
+ *
11219
+ * A profile is authored in Console against a team; whether it can run depends
11220
+ * on local facts only the server knows — which provider keys are configured
11221
+ * here and which runtime kinds this machine can produce.
11222
+ *
11223
+ * The prerequisite comparison itself is **not** reimplemented here. Run start
11224
+ * calls `validateRuntimeProfilePrerequisites`, and the catalogue calls the same
11225
+ * function, so the composer cannot promise a run that startup would reject for
11226
+ * a reason the two evaluated differently.
11227
+ */
11228
+ function deriveProfileReadiness(profile, machine) {
11229
+ const blockers = [];
11230
+ const env = {};
11231
+ for (const [name, configured] of machine.providerEnv) if (configured) env[name] = "configured";
11232
+ try {
11233
+ validateRuntimeProfilePrerequisites(profile, env, {
11234
+ tools: machine.inventory?.tools ?? profile.requiredTools,
11235
+ executables: machine.inventory?.executables ?? profile.requiredExecutables
11236
+ });
11237
+ } catch (error) {
11238
+ if (!(error instanceof RuntimeProfilePrerequisiteError)) throw error;
11239
+ for (const name of error.missingEnv) blockers.push({
11240
+ code: "env_missing",
11241
+ message: `${name} is not configured on this machine.`,
11242
+ remedy: "Add the key under Providers, then reopen this run."
11243
+ });
11244
+ for (const name of error.missingTools) blockers.push({
11245
+ code: "tool_missing",
11246
+ message: `The runtime does not provide the tool ${name}.`,
11247
+ remedy: `Use a profile whose runtime provides ${name}, or change the profile in Console.`
11248
+ });
11249
+ for (const name of error.missingExecutables) blockers.push({
11250
+ code: "executable_missing",
11251
+ message: `The runtime does not provide the executable ${name}.`,
11252
+ remedy: `Use a runtime that ships ${name}, or drop the requirement in Console.`
11253
+ });
11254
+ }
11255
+ if (!machine.runtimeKinds.has(profile.runtimeKind)) blockers.push({
11256
+ code: "runtime_unregistered",
11257
+ message: `Runtime kind ${profile.runtimeKind} is not available on this machine.`,
11258
+ remedy: "Register the runtime under Runtimes, then reopen this run."
11259
+ });
11260
+ return {
11261
+ ready: blockers.length === 0,
11262
+ blockers
11263
+ };
11264
+ }
11265
+ //#endregion
11266
+ //#region src/lib/agent-server/catalogue.ts
11267
+ async function buildCatalogue(options) {
11268
+ const { agent, machine, identityDefault } = options;
11269
+ const entries = await Promise.all(agent.teamIds.map(async (teamId) => {
11270
+ try {
11271
+ const result = await agent.readTeam(teamId);
11272
+ if (result.team.id !== teamId) throw new Error("Team response mismatch");
11273
+ const diaries = result.diaries.filter((diary) => diary.teamId === teamId).map(({ id, name }) => ({
11274
+ id,
11275
+ name
11276
+ }));
11277
+ return {
11278
+ team: {
11279
+ teamId,
11280
+ teamName: result.team.name,
11281
+ available: true,
11282
+ blockers: [],
11283
+ credential: result.credential,
11284
+ diaries,
11285
+ defaultDiaryId: resolveDefaultDiary(teamId, diaries, identityDefault)
11286
+ },
11287
+ profiles: result.profiles.filter((profile) => profile.teamId === teamId).map((profile) => ({
11288
+ ...profile,
11289
+ ...deriveProfileReadiness(profile, machine)
11290
+ }))
11291
+ };
11292
+ } catch (error) {
11293
+ return {
11294
+ team: {
11295
+ teamId,
11296
+ teamName: teamId,
11297
+ available: false,
11298
+ blockers: [credentialBlocker(error)],
11299
+ credential: agent.lastVerified(teamId),
11300
+ diaries: [],
11301
+ defaultDiaryId: null
11302
+ },
11303
+ profiles: []
11304
+ };
11305
+ }
11306
+ }));
11307
+ const teams = entries.map(({ team }) => team);
11308
+ const available = teams.filter((team) => team.available);
11309
+ return {
11310
+ teams,
11311
+ defaultTeamId: available.find((team) => team.teamId === identityDefault.teamId)?.teamId ?? available[0]?.teamId ?? null,
11312
+ profiles: entries.flatMap((entry) => entry.profiles)
11313
+ };
11314
+ }
11315
+ function resolveDefaultDiary(teamId, diaries, identityDefault) {
11316
+ const bound = diaries.find((diary) => diary.id === identityDefault.diaryId);
11317
+ if (bound && identityDefault.teamId === teamId) return bound.id;
11318
+ return diaries.length === 1 ? diaries[0]?.id ?? null : null;
11319
+ }
11320
+ //#endregion
11321
+ //#region src/lib/agent-server/enrollment.ts
11322
+ /** Native callers receive metadata only; approval and storage stay local. */
11323
+ async function enrollIdentityTeam(options) {
11324
+ const apiUrl = new URL(options.apiUrl);
11325
+ if (apiUrl.username || apiUrl.password || apiUrl.hash || apiUrl.protocol !== "https:" && !(apiUrl.protocol === "http:" && isLoopbackHostname(apiUrl.hostname))) throw new Error("Provisioning requires a configured secure API endpoint");
11326
+ const { activation, config } = await loadEnrollmentIdentity(options.store, options.alias);
11327
+ const identityApiUrl = activation.apiUrl ?? config.endpoints?.api;
11328
+ if (!identityApiUrl) throw new Error("The identity has no API environment configured");
11329
+ if (new URL(identityApiUrl).href.replace(/\/$/u, "") !== apiUrl.href.replace(/\/$/u, "")) throw new Error("The identity belongs to another API environment. Select that environment in Server settings before enrollment.");
11330
+ const registry = activation.source === "managed" ? options.managed : options.external;
11331
+ const replacement = options.input.mode === "replace" ? { teamId: options.input.teamId } : void 0;
11332
+ const providerName = replacement ? config.agent_key_refs?.[replacement.teamId]?.provider : activation.source === "managed" ? "file" : config.keys.private_key_ref?.provider;
11333
+ const provider = providerName ? registry.get(providerName) : void 0;
11334
+ if (!provider?.capabilities.write) throw new Error("Enrollment requires a writable identity secret provider");
11335
+ const configPath = activation.source === "managed" ? options.store.agentPath(options.alias) : activation.configPath;
11336
+ try {
11337
+ const result = await enrollTeam({
11338
+ idempotencyKey: options.input.idempotencyKey,
11339
+ provisioningContext: {
11340
+ teamId: options.input.teamId,
11341
+ operation: replacement ? "renew" : "enroll",
11342
+ scopes: [...AGENT_SERVER_REQUIRED_SCOPES]
11343
+ },
11344
+ replacement,
11345
+ provision: async () => {
11346
+ const grant = {
11347
+ agentId: config.subject_id,
11348
+ teamId: options.input.teamId,
11349
+ operation: replacement ? "renew" : "enroll",
11350
+ scopes: [...AGENT_SERVER_REQUIRED_SCOPES],
11351
+ idempotencyKey: options.input.idempotencyKey
11352
+ };
11353
+ let token;
11354
+ let agentProof;
11355
+ try {
11356
+ token = await options.oauth.authorize(grant, options.signal);
11357
+ agentProof = replacement ? void 0 : await signBytes(Buffer.from(enrollmentProofMessage({
11358
+ accessToken: token,
11359
+ grant
11360
+ })).toString("base64"), dirname(configPath), registry);
11361
+ } catch (error) {
11362
+ throw new ProvisioningNotStartedError(error);
11363
+ }
11364
+ const response = await fetch(new URL("/oauth2/provision", options.apiUrl), {
11365
+ method: "POST",
11366
+ redirect: "error",
11367
+ signal: options.signal ? AbortSignal.any([options.signal, AbortSignal.timeout(3e4)]) : AbortSignal.timeout(3e4),
11368
+ headers: {
11369
+ authorization: `Bearer ${token}`,
11370
+ "content-type": "application/json"
11371
+ },
11372
+ body: JSON.stringify(agentProof ? { agentProof } : {})
11373
+ });
11374
+ if (!response.ok) throw new Error("Provisioning unavailable; inspect recovery before fresh approval");
11375
+ const agentKey = await response.json();
11376
+ return {
11377
+ teamId: options.input.teamId,
11378
+ role: "member",
11379
+ agentKey
11380
+ };
11381
+ },
11382
+ configDir: dirname(configPath),
11383
+ secretProvider: provider,
11384
+ apiUrl: options.apiUrl
11385
+ });
11386
+ if (!options.store.readActivation(options.alias)) options.store.writeActivation(activation);
11387
+ return {
11388
+ state: "persisted",
11389
+ teamId: result.teamId,
11390
+ keyId: result.key.id
11391
+ };
11392
+ } catch (error) {
11393
+ if (error instanceof ProvisioningNotStartedError) throw error;
11394
+ if (error instanceof CredentialPersistenceError || error instanceof EnrollmentRecoveryError) return {
11395
+ state: "recovery_required",
11396
+ secretCaptured: error.secretCaptured,
11397
+ ...error.issuedKeyId ? { issuedKeyId: error.issuedKeyId } : {},
11398
+ ...error.recoveryPath ? { recoveryId: basename(error.recoveryPath) } : {},
11399
+ message: error.secretCaptured ? "The credential was captured locally but persistence is incomplete. Recover the captured credential before retrying enrollment." : "No credential secret was captured. Approved team membership may already exist. Retained retry context identifies this issuance; inspect it before requesting fresh approval."
11400
+ };
11401
+ throw new Error("Team enrollment could not be completed", { cause: error });
11402
+ }
11403
+ }
11404
+ //#endregion
11405
+ //#region src/lib/agent-server/identity-binding.ts
11406
+ /**
11407
+ * The identity-wide team/diary binding, read from `<identityDir>/env`.
11408
+ *
11409
+ * This mirrors the Go CLI's `identityDefaultBinding` (`context_store.go:219`),
11410
+ * which is the fallback the CLI uses when a working directory has no
11411
+ * location-keyed binding in `contexts.json`.
11412
+ *
11413
+ * The desktop cannot use the CLI's *location* bindings at all — its composer
11414
+ * has no working directory to key on — but it can honour this identity-wide
11415
+ * default, so an operator who ran `moltnet context set` sees their familiar
11416
+ * team preselected rather than an arbitrary first entry.
11417
+ *
11418
+ * Like the CLI, a half-filled pair is treated as no binding: a team without a
11419
+ * diary is exactly the state `validateContextBinding` rejects.
11420
+ */
11421
+ function readIdentityDefaultBinding(identityDir) {
11422
+ let contents;
11423
+ try {
11424
+ contents = readFileSync(join(identityDir, "env"), "utf8");
11425
+ } catch {
11426
+ return {};
11427
+ }
11428
+ const env = parseEnv(contents);
11429
+ const teamId = env["MOLTNET_TEAM_ID"]?.trim();
11430
+ const diaryId = env["MOLTNET_DIARY_ID"]?.trim();
11431
+ if (!teamId || !diaryId) return {};
11432
+ return {
11433
+ teamId,
11434
+ diaryId
11435
+ };
11436
+ }
11437
+ //#endregion
10529
11438
  //#region src/lib/agent-server/protocol.ts
10530
11439
  var DateTime = Type.String({ format: "date-time" });
10531
11440
  var StringList = Type.Array(Type.String());
@@ -10571,6 +11480,63 @@ var AgentServerProviderSchema = Type.Object({
10571
11480
  models: ProviderModelList,
10572
11481
  hasApiKey: Type.Boolean()
10573
11482
  }, { $id: "AgentServerProvider" });
11483
+ var CredentialMetadataSchema = Type.Object({
11484
+ keyId: Type.String(),
11485
+ expiresAt: Type.Optional(Type.Union([DateTime, Type.Null()])),
11486
+ verifiedAt: DateTime,
11487
+ scopes: StringList
11488
+ });
11489
+ var AgentServerCatalogueTeamSchema = Type.Object({
11490
+ teamId: Type.String(),
11491
+ teamName: Type.String(),
11492
+ available: Type.Boolean(),
11493
+ credential: Type.Optional(CredentialMetadataSchema),
11494
+ blockers: Type.Array(Type.Object({
11495
+ code: Type.String(),
11496
+ message: Type.String(),
11497
+ remedy: Type.String()
11498
+ })),
11499
+ diaries: Type.Array(Type.Object({
11500
+ id: Type.String(),
11501
+ name: Type.String()
11502
+ })),
11503
+ defaultDiaryId: Type.Union([Type.String(), Type.Null()])
11504
+ }, { $id: "AgentServerCatalogueTeam" });
11505
+ /**
11506
+ * Composed from the canonical `RuntimeProfile` schema rather than restated, so
11507
+ * the wire contract cannot drift from the profile the API serves — and so the
11508
+ * constrained fields keep their real unions instead of degrading to `string`.
11509
+ */
11510
+ var AgentServerCatalogueProfileSchema = Type.Intersect([Type.Pick(RuntimeProfile, [
11511
+ "id",
11512
+ "name",
11513
+ "teamId",
11514
+ "description",
11515
+ "provider",
11516
+ "model",
11517
+ "runtimeKind",
11518
+ "toolEnforcement",
11519
+ "defaultWorkspaceMode",
11520
+ "maxTurns",
11521
+ "revision",
11522
+ "definitionCid",
11523
+ "requiredEnv",
11524
+ "requiredTools",
11525
+ "requiredExecutables"
11526
+ ]), Type.Object({
11527
+ ready: Type.Boolean(),
11528
+ blockers: Type.Array(Type.Object({
11529
+ code: Type.String(),
11530
+ message: Type.String(),
11531
+ remedy: Type.String()
11532
+ }))
11533
+ })], { $id: "AgentServerCatalogueProfile" });
11534
+ var AgentServerCatalogueSchema = Type.Object({
11535
+ teams: Type.Array(schemaRef(AgentServerCatalogueTeamSchema)),
11536
+ defaultTeamId: Type.Union([Type.String(), Type.Null()]),
11537
+ profiles: Type.Array(schemaRef(AgentServerCatalogueProfileSchema))
11538
+ }, { $id: "AgentServerCatalogue" });
11539
+ var CatalogueQuerySchema = Type.Object({ identity: Type.String({ minLength: 1 }) });
10574
11540
  var AgentServerRunRecordSchema = Type.Object({
10575
11541
  id: Type.String(),
10576
11542
  agent: Type.String(),
@@ -10587,6 +11553,11 @@ var AgentServerRunRecordSchema = Type.Object({
10587
11553
  ]),
10588
11554
  pid: Type.Optional(Type.Number()),
10589
11555
  exitCode: Type.Optional(Type.Union([Type.Number(), Type.Null()])),
11556
+ credential: Type.Optional(CredentialMetadataSchema),
11557
+ lastError: Type.Optional(Type.Object({
11558
+ code: Type.String(),
11559
+ message: Type.String()
11560
+ })),
10590
11561
  startedAt: DateTime,
10591
11562
  endedAt: Type.Optional(DateTime)
10592
11563
  }, { $id: "AgentServerRunRecord" });
@@ -10623,12 +11594,6 @@ var AgentServerStatusSchema = Type.Object({
10623
11594
  warmRetentionSec: Type.Integer({ minimum: 0 })
10624
11595
  })
10625
11596
  }, { $id: "AgentServerStatus" });
10626
- var PairingStartedSchema = Type.Object({
10627
- pairingId: Type.String(),
10628
- approvalPath: Type.String()
10629
- }, { $id: "PairingStarted" });
10630
- var PairingClaimedSchema = Type.Object({ token: Type.String() }, { $id: "PairingClaimed" });
10631
- var PairingParamsSchema = Type.Object({ pairingId: Type.String() });
10632
11597
  var ProviderParamsSchema = Type.Object({ providerId: Type.String() });
10633
11598
  var AgentParamsSchema = Type.Object({ agentName: Type.String() });
10634
11599
  var RunParamsSchema = Type.Object({ runId: Type.String() });
@@ -10638,7 +11603,8 @@ var CreateAgentSchema = Type.Union([Type.Object({
10638
11603
  enrollmentToken: Type.String()
10639
11604
  }), Type.Object({
10640
11605
  kind: Type.Literal("external"),
10641
- identityAlias: Type.String()
11606
+ identityAlias: Type.String(),
11607
+ teamId: Type.Optional(Type.String({ format: "uuid" }))
10642
11608
  })]);
10643
11609
  var ReconcileAgentSchema = Type.Object({ action: Type.Union([Type.Literal("resume"), Type.Literal("abandon")]) });
10644
11610
  var ReconcileAgentResultSchema = Type.Union([schemaRef(AgentServerAgentSchema), Type.Object({ abandoned: Type.Literal(true) })], { $id: "ReconcileAgentResult" });
@@ -10673,19 +11639,20 @@ var AGENT_SERVER_SCHEMAS = [
10673
11639
  AgentServerIdentitySchema,
10674
11640
  AgentServerTaskTypeSchema,
10675
11641
  AgentServerProviderSchema,
11642
+ AgentServerCatalogueTeamSchema,
11643
+ AgentServerCatalogueProfileSchema,
11644
+ AgentServerCatalogueSchema,
10676
11645
  AgentServerRunRecordSchema,
10677
11646
  AgentServerRunSchema,
10678
11647
  AgentServerSubscriptionSchema,
10679
11648
  AgentServerSubscriptionLoginSchema,
10680
11649
  AgentServerStatusSchema,
10681
- PairingStartedSchema,
10682
- PairingClaimedSchema,
10683
11650
  ReconcileAgentResultSchema,
10684
11651
  DiscoverModelsSchema,
10685
11652
  CancelledSubscriptionSchema,
10686
11653
  LogStreamSchema
10687
11654
  ];
10688
- var pairedSecurity = [{ agentServerToken: [] }];
11655
+ var localControlSecurity = [{ agentServerToken: [] }];
10689
11656
  var problemResponse = { default: schemaRef(AgentServerProblemSchema) };
10690
11657
  var AgentServerRouteSchemas = {
10691
11658
  health: {
@@ -10693,27 +11660,10 @@ var AgentServerRouteSchemas = {
10693
11660
  tags: ["system"],
10694
11661
  response: { 200: schemaRef(AgentServerHealthSchema) }
10695
11662
  },
10696
- startPairing: {
10697
- operationId: "startAgentServerPairing",
10698
- tags: ["pairing"],
10699
- response: {
10700
- 201: schemaRef(PairingStartedSchema),
10701
- ...problemResponse
10702
- }
10703
- },
10704
- claimPairing: {
10705
- operationId: "claimAgentServerPairing",
10706
- tags: ["pairing"],
10707
- params: PairingParamsSchema,
10708
- response: {
10709
- 200: schemaRef(PairingClaimedSchema),
10710
- ...problemResponse
10711
- }
10712
- },
10713
11663
  status: {
10714
11664
  operationId: "getAgentServerStatus",
10715
11665
  tags: ["system"],
10716
- security: pairedSecurity,
11666
+ security: localControlSecurity,
10717
11667
  response: {
10718
11668
  200: schemaRef(AgentServerStatusSchema),
10719
11669
  ...problemResponse
@@ -10722,7 +11672,7 @@ var AgentServerRouteSchemas = {
10722
11672
  listAgents: {
10723
11673
  operationId: "listAgentServerAgents",
10724
11674
  tags: ["agents"],
10725
- security: pairedSecurity,
11675
+ security: localControlSecurity,
10726
11676
  response: {
10727
11677
  200: Type.Array(schemaRef(AgentServerAgentSchema)),
10728
11678
  ...problemResponse
@@ -10731,17 +11681,44 @@ var AgentServerRouteSchemas = {
10731
11681
  createAgent: {
10732
11682
  operationId: "createAgentServerAgent",
10733
11683
  tags: ["agents"],
10734
- security: pairedSecurity,
11684
+ security: localControlSecurity,
10735
11685
  body: CreateAgentSchema,
10736
11686
  response: {
10737
11687
  201: schemaRef(AgentServerAgentSchema),
10738
11688
  ...problemResponse
10739
11689
  }
10740
11690
  },
11691
+ enrollTeam: {
11692
+ operationId: "enrollAgentServerTeam",
11693
+ tags: ["agents"],
11694
+ security: localControlSecurity,
11695
+ params: AgentParamsSchema,
11696
+ body: Type.Intersect([Type.Object({
11697
+ teamId: Type.String({ format: "uuid" }),
11698
+ idempotencyKey: Type.String({
11699
+ minLength: 1,
11700
+ maxLength: 256
11701
+ })
11702
+ }), Type.Union([Type.Object({ mode: Type.Literal("enroll") }), Type.Object({ mode: Type.Literal("replace") })])]),
11703
+ response: {
11704
+ 200: Type.Union([Type.Object({
11705
+ state: Type.Literal("persisted"),
11706
+ teamId: Type.String(),
11707
+ keyId: Type.String()
11708
+ }), Type.Object({
11709
+ state: Type.Literal("recovery_required"),
11710
+ secretCaptured: Type.Boolean(),
11711
+ issuedKeyId: Type.Optional(Type.String()),
11712
+ recoveryId: Type.String(),
11713
+ message: Type.String()
11714
+ })]),
11715
+ ...problemResponse
11716
+ }
11717
+ },
10741
11718
  reconcileAgent: {
10742
11719
  operationId: "reconcileAgentServerAgent",
10743
11720
  tags: ["agents"],
10744
- security: pairedSecurity,
11721
+ security: localControlSecurity,
10745
11722
  params: AgentParamsSchema,
10746
11723
  body: ReconcileAgentSchema,
10747
11724
  response: {
@@ -10752,7 +11729,7 @@ var AgentServerRouteSchemas = {
10752
11729
  listProviders: {
10753
11730
  operationId: "listAgentServerProviders",
10754
11731
  tags: ["providers"],
10755
- security: pairedSecurity,
11732
+ security: localControlSecurity,
10756
11733
  response: {
10757
11734
  200: Type.Record(Type.String(), schemaRef(AgentServerProviderSchema)),
10758
11735
  ...problemResponse
@@ -10761,7 +11738,7 @@ var AgentServerRouteSchemas = {
10761
11738
  discoverModels: {
10762
11739
  operationId: "discoverAgentServerProviderModels",
10763
11740
  tags: ["providers"],
10764
- security: pairedSecurity,
11741
+ security: localControlSecurity,
10765
11742
  params: ProviderParamsSchema,
10766
11743
  response: {
10767
11744
  200: schemaRef(DiscoverModelsSchema),
@@ -10771,7 +11748,7 @@ var AgentServerRouteSchemas = {
10771
11748
  putProvider: {
10772
11749
  operationId: "putAgentServerProvider",
10773
11750
  tags: ["providers"],
10774
- security: pairedSecurity,
11751
+ security: localControlSecurity,
10775
11752
  params: ProviderParamsSchema,
10776
11753
  body: PutProviderSchema,
10777
11754
  response: {
@@ -10782,7 +11759,7 @@ var AgentServerRouteSchemas = {
10782
11759
  deleteProvider: {
10783
11760
  operationId: "deleteAgentServerProvider",
10784
11761
  tags: ["providers"],
10785
- security: pairedSecurity,
11762
+ security: localControlSecurity,
10786
11763
  params: ProviderParamsSchema,
10787
11764
  response: {
10788
11765
  204: Type.Any(),
@@ -10792,7 +11769,7 @@ var AgentServerRouteSchemas = {
10792
11769
  listSubscriptions: {
10793
11770
  operationId: "listAgentServerSubscriptions",
10794
11771
  tags: ["subscriptions"],
10795
- security: pairedSecurity,
11772
+ security: localControlSecurity,
10796
11773
  response: {
10797
11774
  200: Type.Array(schemaRef(AgentServerSubscriptionSchema)),
10798
11775
  ...problemResponse
@@ -10801,7 +11778,7 @@ var AgentServerRouteSchemas = {
10801
11778
  startSubscriptionLogin: {
10802
11779
  operationId: "startAgentServerSubscriptionLogin",
10803
11780
  tags: ["subscriptions"],
10804
- security: pairedSecurity,
11781
+ security: localControlSecurity,
10805
11782
  params: ProviderParamsSchema,
10806
11783
  response: {
10807
11784
  201: schemaRef(AgentServerSubscriptionLoginSchema),
@@ -10811,7 +11788,7 @@ var AgentServerRouteSchemas = {
10811
11788
  getSubscriptionLogin: {
10812
11789
  operationId: "getAgentServerSubscriptionLogin",
10813
11790
  tags: ["subscriptions"],
10814
- security: pairedSecurity,
11791
+ security: localControlSecurity,
10815
11792
  params: ProviderParamsSchema,
10816
11793
  response: {
10817
11794
  200: schemaRef(AgentServerSubscriptionLoginSchema),
@@ -10821,17 +11798,27 @@ var AgentServerRouteSchemas = {
10821
11798
  cancelSubscriptionLogin: {
10822
11799
  operationId: "cancelAgentServerSubscriptionLogin",
10823
11800
  tags: ["subscriptions"],
10824
- security: pairedSecurity,
11801
+ security: localControlSecurity,
10825
11802
  params: ProviderParamsSchema,
10826
11803
  response: {
10827
11804
  200: schemaRef(CancelledSubscriptionSchema),
10828
11805
  ...problemResponse
10829
11806
  }
10830
11807
  },
11808
+ catalogue: {
11809
+ operationId: "getAgentServerCatalogue",
11810
+ tags: ["catalogue"],
11811
+ security: localControlSecurity,
11812
+ querystring: CatalogueQuerySchema,
11813
+ response: {
11814
+ 200: schemaRef(AgentServerCatalogueSchema),
11815
+ ...problemResponse
11816
+ }
11817
+ },
10831
11818
  listRuns: {
10832
11819
  operationId: "listAgentServerRuns",
10833
11820
  tags: ["runs"],
10834
- security: pairedSecurity,
11821
+ security: localControlSecurity,
10835
11822
  response: {
10836
11823
  200: Type.Array(schemaRef(AgentServerRunSchema)),
10837
11824
  ...problemResponse
@@ -10840,7 +11827,7 @@ var AgentServerRouteSchemas = {
10840
11827
  startRun: {
10841
11828
  operationId: "startAgentServerRun",
10842
11829
  tags: ["runs"],
10843
- security: pairedSecurity,
11830
+ security: localControlSecurity,
10844
11831
  body: StartRunSchema,
10845
11832
  response: {
10846
11833
  201: schemaRef(AgentServerRunSchema),
@@ -10850,7 +11837,7 @@ var AgentServerRouteSchemas = {
10850
11837
  stopRun: {
10851
11838
  operationId: "stopAgentServerRun",
10852
11839
  tags: ["runs"],
10853
- security: pairedSecurity,
11840
+ security: localControlSecurity,
10854
11841
  params: RunParamsSchema,
10855
11842
  response: {
10856
11843
  200: schemaRef(AgentServerRunRecordSchema),
@@ -10860,7 +11847,7 @@ var AgentServerRouteSchemas = {
10860
11847
  streamRunLogs: {
10861
11848
  operationId: "streamAgentServerRunLogs",
10862
11849
  tags: ["runs"],
10863
- security: pairedSecurity,
11850
+ security: localControlSecurity,
10864
11851
  params: RunParamsSchema,
10865
11852
  response: {
10866
11853
  200: schemaRef(LogStreamSchema),
@@ -10875,9 +11862,8 @@ var AgentServerRouteSchemas = {
10875
11862
  * loopback-companion security profile (#2066): loopback Host enforcement,
10876
11863
  * exact-origin CORS, Fetch-Metadata guards, strict JSON parsing.
10877
11864
  *
10878
- * Everything under `/v1` except the pairing bootstrap requires a paired
10879
- * origin: the `x-moltnet-agent-server-token` header must verify against the
10880
- * origin-bound token issued by the one-click pairing ceremony.
11865
+ * Control routes require a native process grant or an OAuth token bound to
11866
+ * the native operator and this server instance. Origin checks apply to both.
10881
11867
  */
10882
11868
  var AGENT_SERVER_TOKEN_HEADER = "x-moltnet-agent-server-token";
10883
11869
  var BODY_LIMIT = 64 * 1024;
@@ -10993,7 +11979,9 @@ function requestOperationSignal(request, shutdownSignal) {
10993
11979
  return shutdownSignal ? AbortSignal.any([disconnected.signal, shutdownSignal]) : disconnected.signal;
10994
11980
  }
10995
11981
  function buildAgentServer(options) {
10996
- const { pairing } = options;
11982
+ const { nativeGrant } = options;
11983
+ const oauth = options.operatorOAuth;
11984
+ let restartRequired = false;
10997
11985
  const fastifyOptions = {
10998
11986
  bodyLimit: BODY_LIMIT,
10999
11987
  ...options.tls ? { https: options.tls } : {}
@@ -11004,8 +11992,9 @@ function buildAgentServer(options) {
11004
11992
  }) : Fastify(fastifyOptions);
11005
11993
  options.registerOpenApi?.(app);
11006
11994
  for (const schema of AGENT_SERVER_SCHEMAS) app.addSchema(schema);
11995
+ const browserOrigins = new OriginAllowlist(options.allowedOrigins);
11007
11996
  registerLoopbackSecurity(app, {
11008
- allowedOrigins: options.allowedOrigins,
11997
+ isOriginAllowed: (origin) => origin === "moltnet-agent-desktop://native" || browserOrigins.has(origin),
11009
11998
  ...options.selfOrigin ? { selfOrigins: [options.selfOrigin] } : {},
11010
11999
  allowedHeaders: [AGENT_SERVER_TOKEN_HEADER],
11011
12000
  methods: [
@@ -11016,40 +12005,185 @@ function buildAgentServer(options) {
11016
12005
  "OPTIONS"
11017
12006
  ]
11018
12007
  });
12008
+ let verificationWindow = Date.now();
12009
+ let verifications = 0;
12010
+ const browserVerification = /* @__PURE__ */ new WeakMap();
12011
+ function verifyBrowser(request, token) {
12012
+ const previous = browserVerification.get(request);
12013
+ if (previous) return previous;
12014
+ if (Date.now() - verificationWindow >= RATE_LIMIT_WINDOW_MS) {
12015
+ verificationWindow = Date.now();
12016
+ verifications = 0;
12017
+ }
12018
+ if (++verifications > RATE_LIMIT_MAX) throw new AgentServerHttpError(429, "rate_limited", "Too many authorization attempts");
12019
+ if (!oauth) throw new AgentServerHttpError(503, "oauth_unavailable", "Local OAuth is not configured");
12020
+ const pending = oauth.verifyBrowser(token);
12021
+ browserVerification.set(request, pending);
12022
+ return pending;
12023
+ }
11019
12024
  app.register(rateLimit, {
11020
- global: false,
12025
+ global: true,
11021
12026
  max: options.rateLimitMax ?? RATE_LIMIT_MAX,
11022
12027
  timeWindow: RATE_LIMIT_WINDOW_MS,
11023
12028
  errorResponseBuilder: () => new AgentServerHttpError(429, "rate_limited", "Too many requests"),
11024
- keyGenerator: (request) => {
12029
+ keyGenerator: async (request) => {
11025
12030
  const origin = request.headers.origin;
11026
- return isConfiguredOrigin(origin, options) ? `origin:${origin}` : `ip:${request.ip}`;
11027
- }
11028
- });
11029
- app.addContentTypeParser("application/x-www-form-urlencoded", { parseAs: "buffer" }, (_request, body, done) => {
11030
- try {
11031
- const form = new TextDecoder("utf-8", { fatal: true }).decode(typeof body === "string" ? Buffer.from(body) : body);
11032
- done(null, new URLSearchParams(form));
11033
- } catch {
11034
- done(new AgentServerHttpError(400, "invalid_body", "Form body is invalid"), void 0);
12031
+ if (!isConfiguredOrigin(origin, options)) return `ip:${request.ip}`;
12032
+ const presented = request.headers[AGENT_SERVER_TOKEN_HEADER];
12033
+ let authenticated = false;
12034
+ if (typeof presented === "string" && presented.length > 0) try {
12035
+ if (origin === "moltnet-agent-desktop://native") nativeGrant.verify(origin, presented);
12036
+ else {
12037
+ if (!oauth) return `unauth:${origin}:${request.ip}`;
12038
+ await verifyBrowser(request, presented);
12039
+ }
12040
+ authenticated = true;
12041
+ } catch (error) {
12042
+ if (error instanceof AgentServerHttpError && error.statusCode === 429) throw error;
12043
+ if (origin === "moltnet-agent-desktop://native" && !(error instanceof NativeGrantError)) throw error;
12044
+ }
12045
+ return authenticated ? `origin:${origin}` : `unauth:${origin}:${request.ip}`;
11035
12046
  }
11036
12047
  });
11037
- const requirePairedOrigin = (request) => {
12048
+ const requireAuthorizedOrigin = async (request) => {
12049
+ if (restartRequired) throw new AgentServerHttpError(409, "restart_required", "Restart the Agent Server to apply connection settings");
11038
12050
  const origin = requireOriginHeader(request.headers);
11039
12051
  const token = request.headers[AGENT_SERVER_TOKEN_HEADER];
11040
- if (typeof token !== "string" || token.length === 0) throw new AgentServerHttpError(401, "pairing_required", "Pairing token is required");
11041
- pairing.verify(origin, token);
12052
+ if (typeof token !== "string" || token.length === 0) throw new AgentServerHttpError(401, "authorization_required", "Local control token is required");
12053
+ if (origin === "moltnet-agent-desktop://native") nativeGrant.verify(origin, token);
12054
+ else try {
12055
+ if (!oauth) throw new AgentServerHttpError(503, "oauth_unavailable", "Local OAuth is not configured");
12056
+ const admission = browserVerification.get(request);
12057
+ browserVerification.delete(request);
12058
+ await (admission ?? oauth.verifyBrowser(token));
12059
+ } catch (error) {
12060
+ if (error instanceof AgentServerHttpError) throw error;
12061
+ const code = error && typeof error === "object" && "code" in error ? error.code : void 0;
12062
+ const rejected = error instanceof InvalidOperatorGrantError || typeof code === "string" && [
12063
+ "ERR_JWT_EXPIRED",
12064
+ "ERR_JWT_CLAIM_VALIDATION_FAILED",
12065
+ "ERR_JWS_SIGNATURE_VERIFICATION_FAILED",
12066
+ "ERR_JWS_INVALID",
12067
+ "ERR_JWT_INVALID",
12068
+ "ERR_JOSE_ALG_NOT_ALLOWED",
12069
+ "ERR_JWKS_NO_MATCHING_KEY"
12070
+ ].includes(code);
12071
+ request.log.warn({
12072
+ stage: "local-control-authorization",
12073
+ outcome: rejected ? "rejected" : "unavailable",
12074
+ code: typeof code === "string" ? code : void 0
12075
+ }, "Local control authorization failed");
12076
+ if (!rejected) throw new AgentServerHttpError(503, "authorization_unavailable", "Local authorization is unavailable. Check Server settings or retry shortly.");
12077
+ throw new AgentServerHttpError(401, "authorization_required", "Sign in to authorize local control");
12078
+ }
11042
12079
  return origin;
11043
12080
  };
11044
12081
  app.after(() => {
11045
- app.addHook("onRequest", app.rateLimit());
11046
12082
  app.get("/health", { schema: AgentServerRouteSchemas.health }, async () => ({ status: "ok" }));
11047
- registerPairingRoutes(app, pairing);
11048
- registerStatusRoute(app, options, requirePairedOrigin);
11049
- registerAgentRoutes(app, options, requirePairedOrigin);
11050
- registerProviderRoutes(app, options, requirePairedOrigin);
11051
- registerSubscriptionRoutes(app, options, requirePairedOrigin);
11052
- registerRunRoutes(app, options, requirePairedOrigin);
12083
+ app.get("/v1/native/connection-settings", { schema: { hide: true } }, async (request) => {
12084
+ if (await requireAuthorizedOrigin(request) !== "moltnet-agent-desktop://native" || !options.connectionSettings) throw new AgentServerHttpError(403, "native_required", "Native administration required");
12085
+ return options.connectionSettings.view();
12086
+ });
12087
+ app.post("/v1/native/connection-settings", { schema: { hide: true } }, async (request) => {
12088
+ if (await requireAuthorizedOrigin(request) !== "moltnet-agent-desktop://native" || !options.connectionSettings) throw new AgentServerHttpError(403, "native_required", "Native administration required");
12089
+ try {
12090
+ const settings = options.runs.prepareServerRestart(() => options.connectionSettings.save(request.body));
12091
+ oauth?.cancel();
12092
+ restartRequired = true;
12093
+ return settings;
12094
+ } catch (error) {
12095
+ throw new AgentServerHttpError(400, "invalid_connection_settings", error instanceof Error ? error.message : "Invalid connection settings");
12096
+ }
12097
+ });
12098
+ app.get("/oauth/metadata", { schema: {
12099
+ operationId: "getAgentServerOAuthMetadata",
12100
+ tags: ["operator"],
12101
+ response: { 200: {
12102
+ type: "object",
12103
+ required: [
12104
+ "protocolVersion",
12105
+ "instance",
12106
+ "issuer",
12107
+ "authorizationUrl",
12108
+ "tokenUrl",
12109
+ "clientId",
12110
+ "operatorConfigured"
12111
+ ],
12112
+ properties: {
12113
+ protocolVersion: {
12114
+ type: "integer",
12115
+ const: OPERATOR_OAUTH.protocolVersion
12116
+ },
12117
+ instance: {
12118
+ type: "string",
12119
+ format: "uuid"
12120
+ },
12121
+ issuer: { type: "string" },
12122
+ authorizationUrl: { type: "string" },
12123
+ tokenUrl: { type: "string" },
12124
+ clientId: { type: "string" },
12125
+ operatorConfigured: { type: "boolean" }
12126
+ }
12127
+ } }
12128
+ } }, async () => {
12129
+ if (!oauth) throw new AgentServerHttpError(503, "oauth_unavailable", "Local OAuth is not configured");
12130
+ return oauth.metadata();
12131
+ });
12132
+ app.post("/v1/operator/sign-in", { schema: {
12133
+ operationId: "signInAgentServerOperator",
12134
+ response: { 200: {
12135
+ type: "object",
12136
+ properties: { state: { type: "string" } },
12137
+ required: ["state"]
12138
+ } }
12139
+ } }, async (request) => {
12140
+ if (await requireAuthorizedOrigin(request) !== "moltnet-agent-desktop://native" || !oauth) throw new AgentServerHttpError(403, "native_required", "Native administration required");
12141
+ await oauth.authorize(void 0, requestOperationSignal(request, options.shutdownSignal));
12142
+ return { state: "authorized" };
12143
+ });
12144
+ app.post("/v1/operator/cancel", { schema: {
12145
+ operationId: "cancelAgentServerOperatorApproval",
12146
+ tags: ["operator"],
12147
+ security: [{ agentServerToken: [] }],
12148
+ response: { 200: {
12149
+ type: "object",
12150
+ properties: { state: {
12151
+ type: "string",
12152
+ const: "cancelled"
12153
+ } },
12154
+ required: ["state"]
12155
+ } }
12156
+ } }, async (request) => {
12157
+ if (await requireAuthorizedOrigin(request) !== "moltnet-agent-desktop://native" || !oauth) throw new AgentServerHttpError(403, "native_required", "Native administration required");
12158
+ oauth.cancel();
12159
+ return { state: "cancelled" };
12160
+ });
12161
+ app.delete("/v1/operator", { schema: {
12162
+ operationId: "removeAgentServerOperator",
12163
+ tags: ["operator"],
12164
+ security: [{ agentServerToken: [] }],
12165
+ response: { 200: {
12166
+ type: "object",
12167
+ properties: { state: {
12168
+ type: "string",
12169
+ const: "removed"
12170
+ } },
12171
+ required: ["state"]
12172
+ } }
12173
+ } }, async (request) => {
12174
+ if (await requireAuthorizedOrigin(request) !== "moltnet-agent-desktop://native" || !oauth) throw new AgentServerHttpError(403, "native_required", "Native administration required");
12175
+ oauth.removeOperator();
12176
+ return { state: "removed" };
12177
+ });
12178
+ registerStatusRoute(app, options, requireAuthorizedOrigin);
12179
+ registerAgentRoutes(app, options, requireAuthorizedOrigin);
12180
+ registerProviderRoutes(app, options, requireAuthorizedOrigin);
12181
+ registerSubscriptionRoutes(app, options, requireAuthorizedOrigin);
12182
+ registerRunRoutes(app, options, requireAuthorizedOrigin);
12183
+ registerCatalogueRoute(app, options, requireAuthorizedOrigin);
12184
+ });
12185
+ app.addHook("preClose", async () => {
12186
+ options.operatorOAuth?.cancel();
11053
12187
  });
11054
12188
  app.addHook("onClose", () => {
11055
12189
  options.subscriptions.close();
@@ -11073,41 +12207,65 @@ function buildAgentServer(options) {
11073
12207
  });
11074
12208
  return app;
11075
12209
  }
11076
- function registerPairingRoutes(app, pairing) {
11077
- app.post("/v1/pairings", { schema: AgentServerRouteSchemas.startPairing }, async (request, reply) => {
11078
- const origin = requireOriginHeader(request.headers);
11079
- return reply.code(201).send(pairing.start(origin));
11080
- });
11081
- app.get("/pairings/:pairingId", { schema: { hide: true } }, async (request, reply) => {
11082
- assertNavigationRequest(request.headers);
11083
- const { pairingId } = request.params;
11084
- const approval = pairing.approval(pairingId);
11085
- return reply.type("text/html; charset=utf-8").send(renderPairingApprovalPage({
11086
- pairingId,
11087
- origin: approval.origin,
11088
- confirmToken: approval.confirmToken
11089
- }));
11090
- });
11091
- app.post("/pairings/:pairingId/confirm", { schema: { hide: true } }, async (request, reply) => {
11092
- rejectExplicitCrossSite(request.headers);
11093
- const { pairingId } = request.params;
11094
- if (!(request.body instanceof URLSearchParams)) throw new AgentServerHttpError(400, "invalid_body", "Confirmation form is invalid");
11095
- const { origin } = pairing.confirm(pairingId, request.body.get("confirmToken") ?? "");
11096
- return reply.type("text/html; charset=utf-8").send(renderPairingResultPage({
11097
- title: "Connection approved",
11098
- message: `${origin} can now manage local MoltNet agents on this machine.`
11099
- }));
11100
- });
11101
- app.post("/v1/pairings/:pairingId/claim", { schema: AgentServerRouteSchemas.claimPairing }, async (request) => {
11102
- const origin = requireOriginHeader(request.headers);
11103
- const { pairingId } = request.params;
11104
- return pairing.claim(pairingId, origin);
12210
+ function registerCatalogueRoute(app, options, requireAuthorizedOrigin) {
12211
+ app.get("/v1/catalogue", {
12212
+ schema: AgentServerRouteSchemas.catalogue,
12213
+ attachValidation: true
12214
+ }, async (request) => {
12215
+ await requireAuthorizedOrigin(request);
12216
+ const { identity } = request.query ?? {};
12217
+ if (!identity || identity.trim().length === 0) throw new AgentServerHttpError(400, "invalid_query", "\"identity\" is required");
12218
+ const alias = identity.trim();
12219
+ requireActivation(options.store, alias);
12220
+ return buildCatalogue({
12221
+ agent: await (options.catalogueAgentFor ? options.catalogueAgentFor(alias) : defaultCatalogueAgent(options, alias)),
12222
+ machine: machineCapabilities(options),
12223
+ identityDefault: readIdentityDefaultBinding(options.store.identityDir(alias))
12224
+ });
11105
12225
  });
11106
12226
  }
11107
- function registerStatusRoute(app, options, requirePairedOrigin) {
12227
+ /** Resolve and verify each indexed team independently with its exact key. */
12228
+ async function defaultCatalogueAgent(options, alias) {
12229
+ const { config } = await loadAgentActivation(options.store, alias);
12230
+ return {
12231
+ teamIds: Object.keys(config.agent_key_refs ?? {}),
12232
+ lastVerified: (teamId) => requireActivation(options.store, alias).credentialHealth?.[teamId],
12233
+ readTeam: async (teamId) => {
12234
+ const { client, metadata } = requireCredentialSnapshot(await verifyTeamActivation(options.store, alias, options.secretProviders, options.externalSecretProviders, void 0, options.shutdownSignal, teamId));
12235
+ const [team, diaries, profiles] = await Promise.all([
12236
+ client.teams.get(teamId),
12237
+ client.diaries.list(),
12238
+ client.runtimeProfiles.list({ teamId })
12239
+ ]);
12240
+ return {
12241
+ team,
12242
+ diaries: diaries.items,
12243
+ profiles: profiles.items,
12244
+ credential: metadata
12245
+ };
12246
+ }
12247
+ };
12248
+ }
12249
+ /** What this machine can execute right now: provider keys and runtime kinds. */
12250
+ function machineCapabilities(options) {
12251
+ const providerEnv = /* @__PURE__ */ new Map();
12252
+ for (const provider of Object.values(options.providers.list())) {
12253
+ const configured = providerEnv.get(provider.envName) === true;
12254
+ providerEnv.set(provider.envName, configured || provider.hasApiKey);
12255
+ }
12256
+ const runtimeKinds = new Set([BUILT_IN_RUNTIME_KIND]);
12257
+ for (const entry of options.runtimeRegistry?.list() ?? []) try {
12258
+ if (options.runtimeRegistry?.resolve(entry.kind, { forDisplay: true })) runtimeKinds.add(entry.kind);
12259
+ } catch {}
12260
+ return {
12261
+ providerEnv,
12262
+ runtimeKinds
12263
+ };
12264
+ }
12265
+ function registerStatusRoute(app, options, requireAuthorizedOrigin) {
11108
12266
  const { store, runs } = options;
11109
12267
  app.get("/v1/status", { schema: AgentServerRouteSchemas.status }, async (request) => {
11110
- requirePairedOrigin(request);
12268
+ await requireAuthorizedOrigin(request);
11111
12269
  const selected = selectedIdentity(store, options.activeIdentity);
11112
12270
  return {
11113
12271
  version: options.version,
@@ -11117,22 +12275,22 @@ function registerStatusRoute(app, options, requirePairedOrigin) {
11117
12275
  identities: identityViews(store),
11118
12276
  ...selected ? { selectedIdentity: selected } : {},
11119
12277
  providers: options.providers.list(),
11120
- runs: runViews(runs),
12278
+ runs: await runViews(runs),
11121
12279
  runtimeSettings: options.runtimeSettings ?? DEFAULT_LOCAL_OPERATIONAL_SETTINGS
11122
12280
  };
11123
12281
  });
11124
12282
  }
11125
- function registerAgentRoutes(app, options, requirePairedOrigin) {
12283
+ function registerAgentRoutes(app, options, requireAuthorizedOrigin) {
11126
12284
  const { store } = options;
11127
12285
  app.get("/v1/agents", { schema: AgentServerRouteSchemas.listAgents }, async (request) => {
11128
- requirePairedOrigin(request);
12286
+ await requireAuthorizedOrigin(request);
11129
12287
  return store.listActivations().map((activation) => publicAgentView(store, activation));
11130
12288
  });
11131
12289
  app.post("/v1/agents", {
11132
12290
  schema: AgentServerRouteSchemas.createAgent,
11133
12291
  attachValidation: true
11134
12292
  }, async (request, reply) => {
11135
- requirePairedOrigin(request);
12293
+ await requireAuthorizedOrigin(request);
11136
12294
  const body = requireBody(request);
11137
12295
  const signal = requestOperationSignal(request, options.shutdownSignal);
11138
12296
  const kind = requireString(body, "kind");
@@ -11151,17 +12309,33 @@ function registerAgentRoutes(app, options, requirePairedOrigin) {
11151
12309
  const entry = await attachExternalAgent(store, options.externalSecretProviders, {
11152
12310
  name: identityAlias,
11153
12311
  configDir: store.identityDir(identityAlias),
12312
+ ...typeof body["teamId"] === "string" ? { teamId: body["teamId"] } : {},
11154
12313
  signal
11155
12314
  });
11156
12315
  return reply.code(201).send(publicAgentView(store, entry.activation));
11157
12316
  }
11158
12317
  throw new AgentServerHttpError(400, "invalid_body", "\"kind\" must be \"managed\" or \"external\"");
11159
12318
  });
12319
+ app.post("/v1/agents/:agentName/teams", { schema: AgentServerRouteSchemas.enrollTeam }, async (request) => {
12320
+ await requireAuthorizedOrigin(request);
12321
+ const { agentName } = request.params;
12322
+ if (request.headers.origin !== "moltnet-agent-desktop://native" || !options.operatorOAuth || !options.operatorApiUrl) throw new AgentServerHttpError(403, "native_required", "Native OAuth enrollment required");
12323
+ return enrollIdentityTeam({
12324
+ oauth: options.operatorOAuth,
12325
+ apiUrl: options.operatorApiUrl,
12326
+ store,
12327
+ alias: agentName,
12328
+ managed: options.secretProviders,
12329
+ external: options.externalSecretProviders,
12330
+ input: request.body,
12331
+ signal: requestOperationSignal(request, options.shutdownSignal)
12332
+ });
12333
+ });
11160
12334
  app.post("/v1/agents/:agentName/reconcile", {
11161
12335
  schema: AgentServerRouteSchemas.reconcileAgent,
11162
12336
  attachValidation: true
11163
12337
  }, async (request) => {
11164
- requirePairedOrigin(request);
12338
+ await requireAuthorizedOrigin(request);
11165
12339
  const { agentName } = request.params;
11166
12340
  const action = requireString(requireBody(request), "action");
11167
12341
  if (action !== "resume" && action !== "abandon") throw new AgentServerHttpError(400, "invalid_body", "\"action\" must be \"resume\" or \"abandon\"");
@@ -11182,16 +12356,16 @@ function identityViews(store) {
11182
12356
  return store.listIdentityAliases().map((alias) => ({
11183
12357
  alias,
11184
12358
  activated: activated.has(alias),
11185
- hasAgentKey: Boolean(store.readAgentConfig(alias)?.agent_key_ref)
12359
+ hasAgentKey: hasAgentKeyConfiguration(store.readAgentConfig(alias) ?? {})
11186
12360
  }));
11187
12361
  }
11188
- function registerProviderRoutes(app, options, requirePairedOrigin) {
12362
+ function registerProviderRoutes(app, options, requireAuthorizedOrigin) {
11189
12363
  app.get("/v1/providers", { schema: AgentServerRouteSchemas.listProviders }, async (request) => {
11190
- requirePairedOrigin(request);
12364
+ await requireAuthorizedOrigin(request);
11191
12365
  return options.providers.list();
11192
12366
  });
11193
12367
  app.post("/v1/providers/:providerId/discover-models", { schema: AgentServerRouteSchemas.discoverModels }, async (request) => {
11194
- requirePairedOrigin(request);
12368
+ await requireAuthorizedOrigin(request);
11195
12369
  const { providerId } = request.params;
11196
12370
  return options.providers.discover(providerId, { signal: requestOperationSignal(request, options.shutdownSignal) });
11197
12371
  });
@@ -11199,7 +12373,7 @@ function registerProviderRoutes(app, options, requirePairedOrigin) {
11199
12373
  schema: AgentServerRouteSchemas.putProvider,
11200
12374
  attachValidation: true
11201
12375
  }, async (request, reply) => {
11202
- requirePairedOrigin(request);
12376
+ await requireAuthorizedOrigin(request);
11203
12377
  const { providerId } = request.params;
11204
12378
  const body = requireBody(request);
11205
12379
  const entry = await options.providers.set(providerId, {
@@ -11215,7 +12389,7 @@ function registerProviderRoutes(app, options, requirePairedOrigin) {
11215
12389
  schema: AgentServerRouteSchemas.deleteProvider,
11216
12390
  attachValidation: true
11217
12391
  }, async (request, reply) => {
11218
- requirePairedOrigin(request);
12392
+ await requireAuthorizedOrigin(request);
11219
12393
  const { providerId } = request.params;
11220
12394
  try {
11221
12395
  await options.providers.remove(providerId);
@@ -11226,45 +12400,45 @@ function registerProviderRoutes(app, options, requirePairedOrigin) {
11226
12400
  return reply.code(204).send(null);
11227
12401
  });
11228
12402
  }
11229
- function runViews(runs) {
11230
- return runs.list(RUN_HISTORY_LIMIT).map((record) => ({
12403
+ async function runViews(runs) {
12404
+ return (await runs.listAsync(RUN_HISTORY_LIMIT)).map((record) => ({
11231
12405
  ...record,
11232
12406
  active: runs.isActive(record.id)
11233
12407
  }));
11234
12408
  }
11235
- function registerSubscriptionRoutes(app, options, requirePairedOrigin) {
12409
+ function registerSubscriptionRoutes(app, options, requireAuthorizedOrigin) {
11236
12410
  app.get("/v1/subscriptions", { schema: AgentServerRouteSchemas.listSubscriptions }, async (request) => {
11237
- requirePairedOrigin(request);
12411
+ await requireAuthorizedOrigin(request);
11238
12412
  return options.subscriptions.list();
11239
12413
  });
11240
12414
  app.post("/v1/subscriptions/:providerId/login", { schema: AgentServerRouteSchemas.startSubscriptionLogin }, async (request, reply) => {
11241
- requirePairedOrigin(request);
12415
+ await requireAuthorizedOrigin(request);
11242
12416
  const { providerId } = request.params;
11243
12417
  const login = await options.subscriptions.start(providerId);
11244
12418
  return reply.code(201).send(login);
11245
12419
  });
11246
12420
  app.get("/v1/subscriptions/:providerId/login", { schema: AgentServerRouteSchemas.getSubscriptionLogin }, async (request) => {
11247
- requirePairedOrigin(request);
12421
+ await requireAuthorizedOrigin(request);
11248
12422
  const { providerId } = request.params;
11249
12423
  return options.subscriptions.status(providerId);
11250
12424
  });
11251
12425
  app.delete("/v1/subscriptions/:providerId/login", { schema: AgentServerRouteSchemas.cancelSubscriptionLogin }, async (request) => {
11252
- requirePairedOrigin(request);
12426
+ await requireAuthorizedOrigin(request);
11253
12427
  const { providerId } = request.params;
11254
12428
  return options.subscriptions.cancel(providerId);
11255
12429
  });
11256
12430
  }
11257
- function registerRunRoutes(app, options, requirePairedOrigin) {
12431
+ function registerRunRoutes(app, options, requireAuthorizedOrigin) {
11258
12432
  const { runs } = options;
11259
12433
  app.get("/v1/runs", { schema: AgentServerRouteSchemas.listRuns }, async (request) => {
11260
- requirePairedOrigin(request);
12434
+ await requireAuthorizedOrigin(request);
11261
12435
  return runViews(runs);
11262
12436
  });
11263
12437
  app.post("/v1/runs", {
11264
12438
  schema: AgentServerRouteSchemas.startRun,
11265
12439
  attachValidation: true
11266
12440
  }, async (request, reply) => {
11267
- requirePairedOrigin(request);
12441
+ await requireAuthorizedOrigin(request);
11268
12442
  const body = requireBody(request);
11269
12443
  const diaryId = optionalString(body, "diaryId");
11270
12444
  const record = await runs.start({
@@ -11281,17 +12455,57 @@ function registerRunRoutes(app, options, requirePairedOrigin) {
11281
12455
  });
11282
12456
  });
11283
12457
  app.delete("/v1/runs/:runId", { schema: AgentServerRouteSchemas.stopRun }, async (request) => {
11284
- requirePairedOrigin(request);
12458
+ await requireAuthorizedOrigin(request);
11285
12459
  const { runId } = request.params;
11286
12460
  return runs.stop(runId);
11287
12461
  });
11288
- registerRunLogRoute(app, options, requirePairedOrigin);
12462
+ registerRunLogRoute(app, options, requireAuthorizedOrigin);
11289
12463
  }
11290
- function registerRunLogRoute(app, options, requirePairedOrigin) {
12464
+ function registerRunLogRoute(app, options, requireAuthorizedOrigin) {
11291
12465
  const { runs, store } = options;
12466
+ app.get("/v1/runs/:runId/logs/snapshot", { schema: {
12467
+ operationId: "getAgentServerRunLogSnapshot",
12468
+ tags: ["runs"],
12469
+ security: [{ agentServerToken: [] }],
12470
+ params: {
12471
+ type: "object",
12472
+ required: ["runId"],
12473
+ properties: { runId: {
12474
+ type: "string",
12475
+ minLength: 1
12476
+ } }
12477
+ },
12478
+ response: { 200: {
12479
+ type: "object",
12480
+ required: ["lines"],
12481
+ properties: { lines: {
12482
+ type: "array",
12483
+ items: { type: "string" }
12484
+ } }
12485
+ } }
12486
+ } }, async (request) => {
12487
+ await requireAuthorizedOrigin(request);
12488
+ const { runId } = request.params;
12489
+ const record = runs.status(runId);
12490
+ const handle = await open(store.resolveRunLogPath(record.id), constants.O_RDONLY | constants.O_NOFOLLOW);
12491
+ try {
12492
+ const state = {
12493
+ offset: 0,
12494
+ fragment: ""
12495
+ };
12496
+ const { lines, omitted } = await readAgentServerLogDelta(handle, state);
12497
+ return { lines: [
12498
+ ...omitted ? ["[older log output omitted]"] : [],
12499
+ ...lines,
12500
+ ...state.fragment ? [state.fragment] : []
12501
+ ] };
12502
+ } finally {
12503
+ await handle.close();
12504
+ }
12505
+ });
11292
12506
  let openStreams = 0;
11293
12507
  app.get("/v1/runs/:runId/logs", { schema: AgentServerRouteSchemas.streamRunLogs }, async (request, reply) => {
11294
- requirePairedOrigin(request);
12508
+ await requireAuthorizedOrigin(request);
11295
12509
  const { runId } = request.params;
11296
12510
  const record = runs.status(runId);
11297
12511
  store.resolveRunLogPath(record.id);
@@ -11334,6 +12548,7 @@ function registerRunLogRoute(app, options, requirePairedOrigin) {
11334
12548
  });
11335
12549
  };
11336
12550
  const push = async () => {
12551
+ if (request.headers.origin !== "moltnet-agent-desktop://native") await requireAuthorizedOrigin(request);
11337
12552
  const handle = await open(store.resolveRunLogPath(record.id), constants.O_RDONLY | constants.O_NOFOLLOW);
11338
12553
  try {
11339
12554
  const { lines, omitted } = await readAgentServerLogDelta(handle, readState);
@@ -11382,9 +12597,14 @@ function corsHeadersFor(request, options) {
11382
12597
  return {};
11383
12598
  }
11384
12599
  function isConfiguredOrigin(origin, options) {
11385
- return typeof origin === "string" && (options.allowedOrigins.includes(origin) || origin === options.selfOrigin);
12600
+ return typeof origin === "string" && (origin === "moltnet-agent-desktop://native" || options.allowedOrigins.includes(origin) || origin === options.selfOrigin);
11386
12601
  }
11387
12602
  function normalizeAgentServerError(error) {
12603
+ if (error instanceof TeamCredentialError) return {
12604
+ statusCode: 400,
12605
+ code: error.blocker.code,
12606
+ message: error.blocker.message
12607
+ };
11388
12608
  if (error instanceof AgentServerHttpError) return {
11389
12609
  statusCode: error.statusCode,
11390
12610
  code: error.code,
@@ -11395,8 +12615,8 @@ function normalizeAgentServerError(error) {
11395
12615
  code: error.kind,
11396
12616
  message: error.message
11397
12617
  };
11398
- if (error instanceof AgentServerPairingError) return {
11399
- statusCode: error.code === "pairing_not_found" ? 404 : error.code === "pairing_token_invalid" || error.code === "pairing_not_approved" ? 401 : 403,
12618
+ if (error instanceof NativeGrantError) return {
12619
+ statusCode: 401,
11400
12620
  code: error.code,
11401
12621
  message: error.message
11402
12622
  };
@@ -11455,28 +12675,6 @@ function pemPrivateKey(key) {
11455
12675
  type: "pkcs8"
11456
12676
  }).toString());
11457
12677
  }
11458
- async function importCaKeyPair(pem) {
11459
- const privateKey = createPrivateKey(pem);
11460
- const privateDer = privateKey.export({
11461
- format: "der",
11462
- type: "pkcs8"
11463
- });
11464
- const publicDer = createPublicKey(privateKey).export({
11465
- format: "der",
11466
- type: "spki"
11467
- });
11468
- const [privateCryptoKey, publicCryptoKey] = await Promise.all([webcrypto.subtle.importKey("pkcs8", privateDer, {
11469
- name: "ECDSA",
11470
- namedCurve: "P-256"
11471
- }, false, ["sign"]), webcrypto.subtle.importKey("spki", publicDer, {
11472
- name: "ECDSA",
11473
- namedCurve: "P-256"
11474
- }, false, ["verify"])]);
11475
- return {
11476
- privateKey: privateCryptoKey,
11477
- publicKey: publicCryptoKey
11478
- };
11479
- }
11480
12678
  async function localTlsMaterialFromDirectory(dir) {
11481
12679
  try {
11482
12680
  const [key, cert, ca] = await Promise.all([
@@ -11485,7 +12683,8 @@ async function localTlsMaterialFromDirectory(dir) {
11485
12683
  readFile(join(dir, "local-ca.pem"), "utf8")
11486
12684
  ]);
11487
12685
  const parsed = new X509Certificate(cert);
11488
- if (Date.parse(parsed.validTo) - Date.now() > RENEW_BEFORE_MS) return {
12686
+ const authority = new X509Certificate(ca);
12687
+ if (Date.parse(parsed.validTo) - Date.now() > RENEW_BEFORE_MS && Date.parse(authority.validTo) - Date.now() > RENEW_BEFORE_MS && authority.keyUsage?.includes("1.3.6.1.5.5.7.3.1") && parsed.checkIP("127.0.0.1") === "127.0.0.1" && parsed.verify(authority.publicKey) && parsed.publicKey.equals(createPublicKey(key))) return {
11489
12688
  key,
11490
12689
  cert,
11491
12690
  ca,
@@ -11494,6 +12693,17 @@ async function localTlsMaterialFromDirectory(dir) {
11494
12693
  } catch {}
11495
12694
  return null;
11496
12695
  }
12696
+ /** Read-only inspection for native status polling. */
12697
+ async function inspectLocalTlsMaterial(root) {
12698
+ if (await hasStoredSigningKey(join(root, "tls"))) return null;
12699
+ return localTlsMaterialFromDirectory(join(root, "tls"));
12700
+ }
12701
+ async function hasStoredSigningKey(dir) {
12702
+ return stat(join(dir, "local-ca-key.pem")).then(() => true, (error) => {
12703
+ if (error.code === "ENOENT") return false;
12704
+ throw error;
12705
+ });
12706
+ }
11497
12707
  /** Creates a per-user CA and loopback-only leaf certificate under a 0700 directory. */
11498
12708
  async function ensureLocalTlsMaterial(root) {
11499
12709
  const dir = join(root, "tls");
@@ -11501,27 +12711,25 @@ async function ensureLocalTlsMaterial(root) {
11501
12711
  recursive: true,
11502
12712
  mode: 448
11503
12713
  });
12714
+ const previousSigningKey = join(dir, "local-ca-key.pem");
12715
+ const hasPreviousSigningKey = await hasStoredSigningKey(dir);
11504
12716
  const existing = await localTlsMaterialFromDirectory(dir);
11505
- if (existing) return existing;
11506
- let ca;
11507
- let caKeys;
11508
- let caKey;
11509
- try {
11510
- ca = await readFile(join(dir, "local-ca.pem"), "utf8");
11511
- caKeys = await importCaKeyPair(await readFile(join(dir, "local-ca-key.pem"), "utf8"));
11512
- } catch {
11513
- caKeys = await webcrypto.subtle.generateKey({
11514
- name: "ECDSA",
11515
- namedCurve: "P-256"
11516
- }, true, ["sign", "verify"]);
11517
- ca = (await X509CertificateGenerator.createSelfSigned({
11518
- name: `CN=${CA_COMMON_NAME}`,
11519
- keys: caKeys,
11520
- notAfter: new Date(Date.now() + 10 * 365 * 24 * 60 * 60 * 1e3),
11521
- extensions: [new BasicConstraintsExtension(true, void 0, true), new KeyUsagesExtension(KeyUsageFlags.keyCertSign | KeyUsageFlags.cRLSign, true)]
11522
- })).toString("pem");
11523
- caKey = await pemPrivateKey(caKeys.privateKey);
11524
- }
12717
+ if (existing && !hasPreviousSigningKey) return existing;
12718
+ if (isMacos() && await isLocalCaTrusted(root)) await removeLocalCa(root);
12719
+ const caKeys = await webcrypto.subtle.generateKey({
12720
+ name: "ECDSA",
12721
+ namedCurve: "P-256"
12722
+ }, false, ["sign", "verify"]);
12723
+ const ca = (await X509CertificateGenerator.createSelfSigned({
12724
+ name: `CN=${CA_COMMON_NAME}`,
12725
+ keys: caKeys,
12726
+ notAfter: new Date(Date.now() + 366 * 24 * 60 * 60 * 1e3),
12727
+ extensions: [
12728
+ new BasicConstraintsExtension(true, 0, true),
12729
+ new KeyUsagesExtension(KeyUsageFlags.keyCertSign, true),
12730
+ new ExtendedKeyUsageExtension([ExtendedKeyUsage.serverAuth], true)
12731
+ ]
12732
+ })).toString("pem");
11525
12733
  const leafKeys = await webcrypto.subtle.generateKey({
11526
12734
  name: "ECDSA",
11527
12735
  namedCurve: "P-256"
@@ -11545,8 +12753,9 @@ async function ensureLocalTlsMaterial(root) {
11545
12753
  const key = await pemPrivateKey(leafKeys.privateKey);
11546
12754
  const cert = leafCert.toString("pem");
11547
12755
  const writes = [writeFile(join(dir, "loopback-key.pem"), key, { mode: 384 }), writeFile(join(dir, "loopback-cert.pem"), cert, { mode: 384 })];
11548
- if (caKey) writes.push(writeFile(join(dir, "local-ca.pem"), ca, { mode: 384 }), writeFile(join(dir, "local-ca-key.pem"), caKey, { mode: 384 }));
12756
+ writes.push(writeFile(join(dir, "local-ca.pem"), ca, { mode: 384 }));
11549
12757
  await Promise.all(writes);
12758
+ await rm(previousSigningKey, { force: true });
11550
12759
  return {
11551
12760
  key,
11552
12761
  cert,
@@ -11556,20 +12765,23 @@ async function ensureLocalTlsMaterial(root) {
11556
12765
  }
11557
12766
  async function trustLocalCa(root) {
11558
12767
  const caPath = join(root, "tls", "local-ca.pem");
11559
- await execFileAsync("security", [
12768
+ await execFileAsync("/usr/bin/security", [
11560
12769
  "add-trusted-cert",
11561
- "-d",
11562
12770
  "-r",
11563
12771
  "trustRoot",
12772
+ "-p",
12773
+ "ssl",
12774
+ "-s",
12775
+ "127.0.0.1",
11564
12776
  "-k",
11565
12777
  loginKeychainPath(),
11566
12778
  caPath
11567
12779
  ]);
11568
12780
  }
11569
12781
  async function isLocalCaTrusted(root) {
11570
- const ca = await readFile(join(root, "tls", "local-ca.pem"), "utf8");
11571
12782
  try {
11572
- const { stdout } = await execFileAsync("security", [
12783
+ const ca = await readFile(join(root, "tls", "local-ca.pem"), "utf8");
12784
+ const { stdout } = await execFileAsync("/usr/bin/security", [
11573
12785
  "find-certificate",
11574
12786
  "-a",
11575
12787
  "-p",
@@ -11578,32 +12790,51 @@ async function isLocalCaTrusted(root) {
11578
12790
  loginKeychainPath()
11579
12791
  ]);
11580
12792
  return stdout.includes(ca.trim());
11581
- } catch {
11582
- return false;
12793
+ } catch (error) {
12794
+ if (error && typeof error === "object" && "code" in error && error.code === "ENOENT") return false;
12795
+ const stderr = error && typeof error === "object" && "stderr" in error ? error.stderr : void 0;
12796
+ if (typeof stderr === "string" && stderr.trim() === "security: SecKeychainSearchCopyNext: The specified item could not be found in the keychain.") return false;
12797
+ throw error;
11583
12798
  }
11584
12799
  }
11585
12800
  async function removeLocalCa(root) {
11586
- await execFileAsync("security", [
12801
+ const fingerprint = new X509Certificate(await readFile(join(root, "tls", "local-ca.pem"), "utf8")).fingerprint256.replaceAll(":", "");
12802
+ const caPath = join(root, "tls", "local-ca.pem");
12803
+ await removeTrustSettings([
12804
+ "remove-trusted-cert",
12805
+ "-d",
12806
+ caPath
12807
+ ]);
12808
+ await removeTrustSettings(["remove-trusted-cert", caPath]);
12809
+ await execFileAsync("/usr/bin/security", [
11587
12810
  "delete-certificate",
11588
12811
  "-Z",
11589
- new X509Certificate(await readFile(join(root, "tls", "local-ca.pem"), "utf8")).fingerprint256.replaceAll(":", ""),
12812
+ fingerprint,
11590
12813
  loginKeychainPath()
11591
12814
  ]);
11592
12815
  }
11593
12816
  function isMacos() {
11594
12817
  return process.platform === "darwin";
11595
12818
  }
12819
+ async function removeTrustSettings(args) {
12820
+ try {
12821
+ await execFileAsync("/usr/bin/security", args, { env: { LC_ALL: "C" } });
12822
+ } catch (error) {
12823
+ const stderr = error && typeof error === "object" && "stderr" in error ? error.stderr : void 0;
12824
+ if (typeof stderr === "string" && ["SecTrustSettingsRemoveTrustSettings: No Trust Settings were found.", "SecTrustSettingsRemoveTrustSettings: The specified item could not be found in the keychain."].includes(stderr.trim())) return;
12825
+ throw error;
12826
+ }
12827
+ }
11596
12828
  //#endregion
11597
12829
  //#region src/cli/server.ts
11598
12830
  /**
11599
12831
  * `moltnet-agent server` — per-user loopback supervisor (#2061).
11600
12832
  *
11601
- * Starts nothing on its own: it binds 127.0.0.1 and waits for a paired
12833
+ * Starts nothing on its own: it binds 127.0.0.1 and waits for an authorized
11602
12834
  * Console origin to configure agents/providers and start/stop runs.
11603
12835
  */
11604
- var DEFAULT_PORT = 17374;
12836
+ var DEFAULT_PORT = OPERATOR_OAUTH.serverPort;
11605
12837
  var DEFAULT_ALLOWED_ORIGINS = "https://console.themolt.net";
11606
- var DEFAULT_API_URL = "https://api.themolt.net";
11607
12838
  var SHUTDOWN_TIMEOUT_MS = 15e3;
11608
12839
  async function runAgentServer(argv) {
11609
12840
  if (isHelpFlag(argv)) {
@@ -11632,8 +12863,14 @@ async function runAgentServer(argv) {
11632
12863
  return 1;
11633
12864
  }
11634
12865
  const allowedOrigins = parseAllowedOrigins(values["allowed-origins"] ?? (envConfig.allowedOrigins || DEFAULT_ALLOWED_ORIGINS));
11635
- const root = values.root ?? resolveAgentServerRoot({ root: envConfig.root });
11636
- const defaultApiUrl = values["api-url"] ?? (envConfig.apiUrl || DEFAULT_API_URL);
12866
+ const settingsRoot = values.root ?? resolveAgentServerRoot({ root: envConfig.root });
12867
+ const connectionSettings = new ConnectionSettingsStore(settingsRoot, {
12868
+ ...envConfig.operatorOAuth,
12869
+ ...values["api-url"] || envConfig.apiUrl ? { apiUrl: values["api-url"] || envConfig.apiUrl } : {}
12870
+ });
12871
+ const connection = connectionSettings.view().effective;
12872
+ const root = connectionSettings.stateRoot(connection);
12873
+ const defaultApiUrl = connection.apiUrl;
11637
12874
  const runtimeSettings = parseLocalOperationalSettings(values);
11638
12875
  const store = new AgentServerStore(root).ensure();
11639
12876
  const { logger, shutdown: shutdownLogger } = createRootLogger({
@@ -11649,7 +12886,15 @@ async function runAgentServer(argv) {
11649
12886
  });
11650
12887
  const secretProviders = createNodeSecretProviderRegistry().register(secrets);
11651
12888
  const externalSecretProviders = createNodeSecretProviderRegistry();
11652
- const pairing = new PairingService();
12889
+ const nativeGrant = new NativeGrantService();
12890
+ const nativeClient = applyNativeClientGrant({
12891
+ nativeGrant,
12892
+ env: processEnvSnapshot()
12893
+ });
12894
+ if (Boolean(values.supervised) && !nativeClient) {
12895
+ console.error(`A supervised Agent Server requires ${NATIVE_TOKEN_ENV}. Start it from MoltNet Agent, or omit --supervised to run it to reconnect Console to an operator already established by Desktop.`);
12896
+ return 1;
12897
+ }
11653
12898
  const shutdownController = new AbortController();
11654
12899
  const subscriptions = await ProviderLoginService.create({
11655
12900
  authPath: store.piAuthJsonPath,
@@ -11661,27 +12906,41 @@ async function runAgentServer(argv) {
11661
12906
  secretProviders,
11662
12907
  logger
11663
12908
  });
12909
+ const runtimeRegistry = new RuntimeRegistry(store.root);
11664
12910
  const runs = new RunManager({
11665
12911
  store,
11666
12912
  secretProviders,
11667
12913
  externalSecretProviders,
11668
12914
  baseEnv: processEnvSnapshot(),
11669
12915
  logger,
11670
- runtimeRegistry: new RuntimeRegistry(store.root),
12916
+ runtimeRegistry,
11671
12917
  runtimeSettings
11672
12918
  });
11673
- const tls = isMacos() ? await ensureTrustedLocalTls(root) : void 0;
12919
+ const tls = isMacos() ? await ensureTrustedLocalTls(settingsRoot) : void 0;
12920
+ const selfOrigin = `${tls ? "https" : "http"}://127.0.0.1:${port}`;
11674
12921
  const app = buildAgentServer({
12922
+ operatorOAuth: new OperatorOAuth({
12923
+ issuer: connection.issuer,
12924
+ authorizationUrl: new URL("/oauth2/auth", connection.publicUrl).href,
12925
+ tokenUrl: new URL("/oauth2/token", connection.publicUrl).href,
12926
+ jwksUrl: new URL("/.well-known/jwks.json", connection.publicUrl).href,
12927
+ nativeClientId: connection.nativeClientId,
12928
+ consoleClientId: connection.consoleClientId,
12929
+ callbackPort: OPERATOR_OAUTH.callbackPort
12930
+ }, root),
12931
+ connectionSettings,
12932
+ operatorApiUrl: connection.apiUrl,
11675
12933
  store,
11676
12934
  secrets,
11677
12935
  secretProviders,
11678
12936
  externalSecretProviders,
11679
- pairing,
12937
+ nativeGrant,
11680
12938
  runs,
11681
12939
  subscriptions,
11682
12940
  providers,
12941
+ runtimeRegistry,
11683
12942
  allowedOrigins,
11684
- selfOrigin: `${tls ? "https" : "http"}://127.0.0.1:${port}`,
12943
+ selfOrigin,
11685
12944
  ...tls ? { tls: {
11686
12945
  key: tls.key,
11687
12946
  cert: tls.cert
@@ -11701,7 +12960,8 @@ async function runAgentServer(argv) {
11701
12960
  console.error(`moltnet-agent server listening on ${address}`);
11702
12961
  console.error(`config root: ${root}`);
11703
12962
  console.error(`allowed origins: ${allowedOrigins.join(", ")}`);
11704
- console.error("Pair from the Console \"Local runtime\" page; approve the one-click prompt this server opens.");
12963
+ if (nativeClient) console.error("native desktop client: authorized");
12964
+ console.error("Sign in through Desktop, then connect from the Console \"Local runtime\" page.");
11705
12965
  return await waitForAgentServerShutdown(runs, app, shutdownController, Boolean(values.supervised));
11706
12966
  } catch (cause) {
11707
12967
  await app.close().catch(() => void 0);
@@ -11756,15 +13016,15 @@ async function runTrustCommand(argv, defaultRoot) {
11756
13016
  console.error("Local HTTPS trust setup is currently supported on macOS only.");
11757
13017
  return 1;
11758
13018
  }
11759
- const material = await ensureLocalTlsMaterial(root);
11760
13019
  if (statusRequested) {
11761
- const trusted = await isLocalCaTrusted(root);
13020
+ const material = await inspectLocalTlsMaterial(root);
13021
+ const trusted = material !== null && await isLocalCaTrusted(root);
11762
13022
  if (json) printTrustStatus({
11763
13023
  supported: true,
11764
13024
  trusted,
11765
- fingerprint: material.fingerprint
13025
+ fingerprint: material?.fingerprint ?? null
11766
13026
  });
11767
- else console.log(trusted ? `MoltNet local CA ${material.fingerprint} is trusted.` : `MoltNet local CA ${material.fingerprint} is not trusted.`);
13027
+ else console.log(trusted ? `MoltNet local CA ${material?.fingerprint ?? "(not prepared)"} is trusted.` : `MoltNet local CA ${material?.fingerprint ?? "(not prepared)"} is not trusted.`);
11768
13028
  return 0;
11769
13029
  }
11770
13030
  if (json && !yes) {
@@ -11776,11 +13036,12 @@ async function runTrustCommand(argv, defaultRoot) {
11776
13036
  if (json) printTrustStatus({
11777
13037
  supported: true,
11778
13038
  trusted: false,
11779
- fingerprint: material.fingerprint
13039
+ fingerprint: null
11780
13040
  });
11781
13041
  else console.log("Removed the MoltNet local CA from your login keychain.");
11782
13042
  return 0;
11783
13043
  }
13044
+ const material = await ensureLocalTlsMaterial(root);
11784
13045
  if (yes) await trustLocalCa(root);
11785
13046
  else await ensureTrustedLocalTls(root);
11786
13047
  if (json) printTrustStatus({
@@ -11989,6 +13250,7 @@ async function runSyncSessions(argv) {
11989
13250
  args: argv,
11990
13251
  options: {
11991
13252
  ...identityOptionDefs(),
13253
+ ...projectRunOptionDefs(),
11992
13254
  team: { type: "string" },
11993
13255
  "runtime-profile-id": { type: "string" },
11994
13256
  state: { type: "string" },
@@ -11996,11 +13258,6 @@ async function runSyncSessions(argv) {
11996
13258
  "dry-run": { type: "boolean" }
11997
13259
  }
11998
13260
  });
11999
- if (!values.team) {
12000
- console.error("Missing required flag: --team\n");
12001
- console.error(SYNC_SESSIONS_HELP);
12002
- return 1;
12003
- }
12004
13261
  let identity;
12005
13262
  try {
12006
13263
  identity = parseIdentityProcessOptions(values);
@@ -12017,16 +13274,38 @@ async function runSyncSessions(argv) {
12017
13274
  const agentRootDir = resolve(process.cwd(), values["agent-root"] ?? process.cwd());
12018
13275
  const explicitAgentRootDir = values["agent-root"] ? resolve(process.cwd(), values["agent-root"]) : void 0;
12019
13276
  const cfg = loadConfig();
13277
+ let selection;
13278
+ try {
13279
+ const apiUrl = await resolveSelectionApiUrl(identity.agent, {
13280
+ agentRootDir: explicitAgentRootDir,
13281
+ credentialSource: cfg.credentialSource,
13282
+ envApiUrl: cfg.apiUrl
13283
+ });
13284
+ selection = await resolveRunProjectSelection({
13285
+ ...values,
13286
+ agent: identity.agent,
13287
+ cwd: process.cwd(),
13288
+ apiUrl
13289
+ });
13290
+ if (!selection.teamId) throw new Error("Select --team or a binding with a team");
13291
+ } catch (error) {
13292
+ console.error(error instanceof Error ? error.message : String(error));
13293
+ console.error(SYNC_SESSIONS_HELP);
13294
+ return 1;
13295
+ }
12020
13296
  const ctx = await resolveAgentContext(identity.agent, {
12021
13297
  credentialSource: cfg.credentialSource,
12022
13298
  envApiUrl: cfg.apiUrl,
13299
+ projectApiUrl: selection.binding?.apiUrl,
13300
+ teamId: selection.teamId,
12023
13301
  agentRootDir: explicitAgentRootDir
12024
13302
  });
12025
13303
  await validateStartupBinding({
12026
13304
  agent: ctx.agent,
12027
- teamId: values.team
13305
+ teamId: selection.teamId,
13306
+ credentialTeamId: ctx.credentialTeamId
12028
13307
  });
12029
- const stateDirs = ensureDaemonStateDirs(agentRootDir);
13308
+ const stateDirs = ensureDaemonStateDirs(selection.stateRootDir ?? (selection.binding || values.source ? selection.source ?? agentRootDir : agentRootDir));
12030
13309
  const result = await syncRuntimeSessions({
12031
13310
  runtimeSessionStore: createApiRuntimeSessionStore({ agent: ctx.agent }),
12032
13311
  runtimeSlotStore: createApiRuntimeSlotStore({ agent: ctx.agent }),
@@ -12038,7 +13317,7 @@ async function runSyncSessions(argv) {
12038
13317
  runtimeProfileId: values["runtime-profile-id"],
12039
13318
  sessionRootDir: stateDirs.piSessionsDir,
12040
13319
  state,
12041
- teamId: values.team
13320
+ teamId: selection.teamId
12042
13321
  });
12043
13322
  console.log(JSON.stringify(result, null, 2));
12044
13323
  return result.failedUpload > 0 || result.unsafeSessionPath > 0 ? 1 : 0;
@@ -12209,7 +13488,7 @@ async function writeCache(cache) {
12209
13488
  }
12210
13489
  //#endregion
12211
13490
  //#region src/version.ts
12212
- var DAEMON_VERSION = "0.59.0";
13491
+ var DAEMON_VERSION = "0.61.0";
12213
13492
  //#endregion
12214
13493
  //#region src/cli.ts
12215
13494
  async function runAgentDaemonCli(options) {