@themoltnet/agent-daemon 0.60.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 +74 -3
  2. package/dist/cli.js +2011 -766
  3. package/dist/pi.js +16 -1
  4. package/package.json +10 -9
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, hasAgentKeyConfiguration, isCanonicalConfig, parseSecretReferenceString, readConfig, requireSecureCredentialApiUrl, resolveAgentKey, resolveEnvSecretReference, resolveIdentitySeed, selectAgentKeyReference } 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";
@@ -857,6 +859,23 @@ Object.freeze([...[
857
859
  "offline_access"
858
860
  ], ...MCP_CLIENT_SCOPES]);
859
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
860
879
  //#region ../../libs/models/src/preview-sign.ts
861
880
  function schemaRef$2(schema, id) {
862
881
  return Type.Unsafe(Type.Ref(id));
@@ -1208,7 +1227,8 @@ Type.Object({
1208
1227
  });
1209
1228
  Type.Object({
1210
1229
  code: Type.String({ minLength: 1 }),
1211
- issueAgentKey: Type.Optional(Type.Literal(true))
1230
+ issueAgentKey: Type.Optional(Type.Literal(true)),
1231
+ expectedTeamId: Type.Optional(UuidSchema)
1212
1232
  });
1213
1233
  Type.Object({ role: Type.Union([
1214
1234
  Type.Literal("manager"),
@@ -1449,6 +1469,7 @@ var ProblemCodeSchema = Type.Union([
1449
1469
  Type.Literal("FORBIDDEN"),
1450
1470
  Type.Literal("NOT_FOUND"),
1451
1471
  Type.Literal("CONFLICT"),
1472
+ Type.Literal("PROJECT_MISMATCH"),
1452
1473
  Type.Literal("UNSUPPORTED_MEDIA_TYPE"),
1453
1474
  Type.Literal("VALIDATION_FAILED"),
1454
1475
  Type.Literal("INVALID_CHALLENGE"),
@@ -1529,6 +1550,34 @@ Type.Intersect([ConflictProblemDetailsSchema, Type.Object({ flagged: Type.Option
1529
1550
  threats: Type.Array(Type.Ref("InjectionThreat"))
1530
1551
  }, { additionalProperties: false }))) })], { $id: "InjectionConflictProblemDetails" });
1531
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
+ });
1532
1581
  Type.Union([
1533
1582
  Type.Literal("pack"),
1534
1583
  Type.Literal("entry"),
@@ -1967,7 +2016,7 @@ var RuntimeProfileMaxBashTimeouts = Type.Integer({
1967
2016
  minimum: 0,
1968
2017
  maximum: 1e3
1969
2018
  });
1970
- Type.Object({
2019
+ var RuntimeProfile = Type.Object({
1971
2020
  id: Type.String({ format: "uuid" }),
1972
2021
  teamId: Type.String({ format: "uuid" }),
1973
2022
  name: RuntimeProfileName,
@@ -3353,6 +3402,7 @@ Type.Object({
3353
3402
  title: Type.Union([Type.String(), Type.Null()]),
3354
3403
  tags: Type.Array(Type.String()),
3355
3404
  teamId: Uuid,
3405
+ projectId: Type.Union([Uuid, Type.Null()]),
3356
3406
  diaryId: Type.Union([Uuid, Type.Null()]),
3357
3407
  outputKind: OutputKind,
3358
3408
  input: Type.Record(Type.String(), Type.Unknown()),
@@ -3461,279 +3511,28 @@ Type.Object({
3461
3511
  additionalProperties: false
3462
3512
  });
3463
3513
  //#endregion
3464
- //#region src/lib/help.ts
3465
- var COMMON_REQUIRED_FLAGS = `\
3466
- -a, --agent <name> MoltNet agent identity. Agent-key auth is
3467
- configless; OAuth2 reads moltnet.json.
3468
- --profile <uuid|name> Remote runtime profile. Repeat for poll/drain
3469
- to declare priority order. Provider, model,
3470
- sandbox policy, prerequisites, and runtime
3471
- execution policy come from the selected profile.`;
3472
- var COMMON_OPTIONAL_FLAGS = `\
3473
- --sandbox <path> Deprecated. Remote runtime profiles define
3474
- sandbox policy.
3475
- --agent-root <path> Directory that owns .moltnet/<agent>. Default:
3476
- CWD, with git root fallback when available.
3477
- --git-author <"Name <email>">
3478
- Non-secret git identity projected into the
3479
- guest for host-brokered commit signing. Default:
3480
- host git config. Configless agent-key runs must
3481
- provide this flag or MOLTNET_GIT_AUTHOR.
3482
- Env: MOLTNET_GIT_AUTHOR.
3483
- --heartbeat-interval-ms <n> Reporter heartbeat cadence. Default: 60000.
3484
- --warm-retention-sec <n> Resumability window for runtime slots
3485
- (Pi sessions + reusable worktrees) after use.
3486
- Default: 1800.
3487
- --debug Verbose logging: also log successful list/claim
3488
- outcomes (candidate counts, claim attempts).`;
3489
- var REGISTERED_TASK_TYPES = Object.keys(BUILT_IN_TASK_TYPES).sort();
3490
- function knownTaskTypesList() {
3491
- 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 };
3492
3523
  }
3493
- var ROOT_USAGE = `\
3494
- agent-daemon long-running task worker for MoltNet.
3495
-
3496
- Usage: agent-daemon [--runtime <module>] <command> [...flags]
3497
-
3498
- Runtime:
3499
- --runtime <module> Trusted local file or installed package whose
3500
- default export is a DaemonRuntimeAdapter.
3501
- Omit to use the built-in gondolin_pi runtime.
3502
-
3503
- Commands:
3504
- poll Long-running worker. Polls the task queue and claims tasks
3505
- matching the configured filter until SIGINT/SIGTERM.
3506
- once Claim and execute one specific queued task by id, then exit.
3507
- drain Poll until the queue has nothing claimable, then exit.
3508
- Useful for batch eval runs and demos.
3509
- server Loopback supervisor for console-managed runs: pairing,
3510
- agent/provider config store, and start/stop of poll/drain
3511
- child processes. Binds 127.0.0.1 only.
3512
- server trust
3513
- Install the per-user macOS local-HTTPS CA after explicit consent.
3514
- providers Manage configured endpoints and Pi OAuth subscriptions without
3515
- starting the Agent Server. See \`agent-daemon providers --help\`.
3516
- sync-sessions
3517
- Repair durable runtime-session checkpoints from local slot files.
3518
- update check
3519
- Check the stable MoltNet agent release without reading credentials.
3520
-
3521
- Run \`agent-daemon <command> --help\` for command-specific flags.
3522
-
3523
- Prerequisites:
3524
- - configless: MOLTNET_AGENT_KEY (or MOLTNET_AGENT_KEY_REF) and
3525
- MOLTNET_PRIVATE_KEY (or MOLTNET_PRIVATE_KEY_REF); no agent files
3526
- - config-based: ~/.config/moltnet/identities/<agent>/moltnet.json
3527
- carrying agent_key_refs or agent_key_ref (OAuth2 is not accepted)
3528
- --agent-root explicitly selects a legacy .moltnet/<agent> bundle
3529
-
3530
- No key yet? Mint one with the CLI (--store writes the team slot into
3531
- moltnet.json and keeps the secret in a provider):
3532
-
3533
- moltnet teams list # find the team id
3534
- moltnet agents keys create --team-id <team-uuid> \\
3535
- --name <agent>-daemon --store
3536
-
3537
- https://docs.themolt.net/operate/agent-keys#run-the-daemon-with-an-agent-key
3538
- - --profile — remote runtime profile supplies provider/model/sandbox
3539
- policy and CWD is used as the VM mountPath.
3540
-
3541
- Registered task types: ${knownTaskTypesList()}`;
3542
- var POLL_HELP = `\
3543
- agent-daemon poll — long-running task worker.
3544
-
3545
- Usage:
3546
- agent-daemon poll --team <uuid> --agent <name> --profile <uuid|name> [...]
3547
-
3548
- Required:
3549
- --team <uuid> Team whose queue to serve. The daemon must be
3550
- a member of this team (canAccessTeam permit).
3551
- ${COMMON_REQUIRED_FLAGS}
3552
-
3553
- Optional:
3554
- --task-types <csv> Whitelist of task types to claim. Default:
3555
- accept any registered type. Known types:
3556
- ${knownTaskTypesList()}
3557
- --correlation-id <uuid> Restrict claims to one orchestration run.
3558
- --diary-ids <csv> Further client-side filter on task.diaryId.
3559
- --poll-interval-ms <n> Idle backoff floor. Default: 2000.
3560
- --max-poll-interval-ms <n> Idle backoff ceiling. Default: 30000.
3561
- --list-limit <n> Page size per list call. Default: 10.
3562
- ${COMMON_OPTIONAL_FLAGS}
3563
-
3564
- Example:
3565
- agent-daemon poll \\
3566
- --team 6743b4b1-6b93-46e2-a048-19490f04f91a \\
3567
- --task-types curate_pack,fulfill_brief \\
3568
- --agent legreffier \\
3569
- --profile github-linear \\
3570
- --profile local-fallback
3571
-
3572
- Stops cleanly on SIGINT/SIGTERM (drains the in-flight task before exit).`;
3573
- var ONCE_HELP = `\
3574
- agent-daemon once — execute one specific queued task by id, then exit.
3575
-
3576
- Usage:
3577
- agent-daemon once --task-id <uuid> --agent <name> --profile <uuid|name> [...]
3578
-
3579
- Required:
3580
- -t, --task-id <uuid> Task to claim and execute. Must already be
3581
- in 'queued' status.
3582
- ${COMMON_REQUIRED_FLAGS}
3583
-
3584
- Optional:
3585
- --team <uuid> Team scope for resolving --profile by name.
3586
- Required only when --profile is a name.
3587
- ${COMMON_OPTIONAL_FLAGS}
3588
-
3589
- Example:
3590
- agent-daemon once \\
3591
- --task-id 26004a77-bc10-43ef-a79f-c8e62faf59b1 \\
3592
- --agent legreffier \\
3593
- --profile github-linear
3594
-
3595
- Exits 0 on completed, 1 on failed/cancelled/runtime-error.`;
3596
- var DRAIN_HELP = `\
3597
- agent-daemon drain — poll until the queue is empty, then exit.
3598
-
3599
- Usage:
3600
- agent-daemon drain --team <uuid> --agent <name> --profile <uuid|name> [...]
3601
-
3602
- Same flags as \`poll\`. The only behavioural difference: \`drain\` exits
3603
- when a list call confirms no claimable tasks remain (vs \`poll\` which
3604
- sleeps and retries forever).
3605
-
3606
- Required:
3607
- --team <uuid> Team whose queue to drain.
3608
- ${COMMON_REQUIRED_FLAGS}
3609
-
3610
- Optional:
3611
- --task-types <csv> Whitelist. Known types: ${knownTaskTypesList()}
3612
- --correlation-id <uuid> Restrict claims to one orchestration run.
3613
- --wait-for-first-task-sec <n>
3614
- Wait this long for an initially empty run before
3615
- exiting. After the first claim, exit on empty.
3616
- --wait-after-task-sec <n> Require the queue to remain empty for this long
3617
- after a claim before exiting.
3618
- --diary-ids <csv> Diary filter.
3619
- --poll-interval-ms <n> Default: 2000.
3620
- --max-poll-interval-ms <n> Default: 30000.
3621
- --list-limit <n> Default: 10.
3622
- ${COMMON_OPTIONAL_FLAGS}
3623
-
3624
- Example:
3625
- agent-daemon drain \\
3626
- --team 6743b4b1-6b93-46e2-a048-19490f04f91a \\
3627
- --task-types judge_pack \\
3628
- --agent legreffier \\
3629
- --profile eval-judge`;
3630
- var SYNC_SESSIONS_HELP = `\
3631
- agent-daemon sync-sessions — repair durable runtime-session checkpoints.
3632
-
3633
- Usage:
3634
- agent-daemon sync-sessions --team <uuid> --agent <name> [...]
3635
-
3636
- Scans this daemon's team-scoped runtime slots, compares local Pi session files
3637
- with durable runtime-session metadata, and uploads missing or stale checkpoints.
3638
-
3639
- Required:
3640
- --team <uuid> Team whose runtime slots to inspect.
3641
- -a, --agent <name> MoltNet agent identity. Reads credentials
3642
- from <agent-root>/.moltnet/<name>/moltnet.json.
3643
-
3644
- Optional:
3645
- --runtime-profile-id <uuid> Limit repair to one runtime profile.
3646
- --state <active|idle> Limit scanned slots by state. Default: all.
3647
- --limit <n> Max slots to scan, 1..200. Default: 100.
3648
- --dry-run Report missing/stale sessions without uploading.
3649
- --agent-root <path> Directory that owns .moltnet/<agent>. Default:
3650
- CWD, with git root fallback when available.
3651
- --debug Accepted for consistency; no extra output yet.
3652
-
3653
- Example:
3654
- agent-daemon sync-sessions \\
3655
- --team 6743b4b1-6b93-46e2-a048-19490f04f91a \\
3656
- --agent legreffier \\
3657
- --state idle`;
3658
- function isHelpFlag(args) {
3659
- return args.includes("--help") || args.includes("-h");
3660
- }
3661
- var AGENT_SERVER_HELP = `\
3662
- agent-daemon server — loopback supervisor for console-managed runs.
3663
-
3664
- Binds 127.0.0.1 only. A paired Console origin configures agents and
3665
- providers (secret references only) and starts/stops poll/drain runs as
3666
- child processes of this supervisor.
3667
-
3668
- Options:
3669
- --port <n> Loopback port. Default: 17374.
3670
- Env: MOLTNET_AGENT_SERVER_PORT.
3671
- --allowed-origins <csv> Exact Console origins allowed to pair.
3672
- Default: https://console.themolt.net.
3673
- Env: MOLTNET_AGENT_SERVER_ALLOWED_ORIGINS.
3674
- --root <path> Config root. Default: ~/.config/moltnet
3675
- (or MOLTNET_AGENT_SERVER_ROOT).
3676
- --api-url <url> Default MoltNet API for new managed agents.
3677
- Default: https://api.themolt.net.
3678
- --heartbeat-interval-ms <n> Child reporter heartbeat cadence. Default: 60000.
3679
- --warm-retention-sec <n> Child session/workspace retention. Default: 1800.
3680
- --supervised Also stop gracefully when stdin reaches EOF.
3681
-
3682
- On macOS, the first interactive run asks to trust a per-user local CA in the
3683
- login keychain and serves HTTPS. Native supervisors use:
3684
- server trust --status --json
3685
- server trust --yes --json
3686
- server trust --remove --yes --json
3687
- Run \`agent-daemon server trust --remove\` interactively to remove that exact
3688
- CA. Linux continues to use the Chromium PNA HTTP path.
3689
- `;
3690
- var PROVIDERS_HELP = `\
3691
- moltnet-agent providers — manage local model providers.
3692
-
3693
- Usage:
3694
- moltnet-agent providers list [--json] [--root <path>]
3695
- moltnet-agent providers set <id> [--base-url <url>] [--api <pi-api-kind>]
3696
- [--model <id> ... | --clear-models]
3697
- [--model-input <id>=text,image ...]
3698
- [--api-key-stdin | --clear-api-key] [--root <path>]
3699
- moltnet-agent providers discover <id> [--save] [--json] [--root <path>]
3700
- moltnet-agent providers remove <id> [--yes] [--root <path>]
3701
- moltnet-agent providers login <id> [--auth-method <method-id>]
3702
- [--root <path>]
3703
- moltnet-agent providers logout <id> [--yes] [--root <path>]
3704
-
3705
- The default root is ~/.config/moltnet. MOLTNET_AGENT_SERVER_ROOT remains the
3706
- environment override. API keys are accepted only from redirected stdin; they
3707
- are stored separately and providers.json contains only a secret reference.
3708
-
3709
- --model declares a text-only model. --model-input declares a model together
3710
- with the input modalities it accepts, and is what makes a vision model usable:
3711
- a model with no declared modalities is text-only to Pi, which drops image
3712
- content parts before the request leaves the runtime.
3713
- `;
3714
- //#endregion
3715
- //#region src/lib/identity-pin.ts
3716
- /** Compare every pinned field without choosing a caller-specific error type. */
3717
- function assessIdentityPin(current, expected) {
3718
- for (const [field, label] of [["publicKey", "public key"], ["fingerprint", "fingerprint"]]) if (!current[field] || current[field] !== expected[field]) return {
3719
- ok: false,
3720
- field,
3721
- label
3722
- };
3723
- return { ok: true };
3724
- }
3725
- function assessAgentStartupPin(current, expected) {
3726
- for (const [field, label] of [
3727
- ["subjectId", "subject id"],
3728
- ["subjectType", "subject type"],
3729
- ["publicKey", "public key"],
3730
- ["fingerprint", "fingerprint"]
3731
- ]) if (!current[field] || current[field] !== expected[field]) return {
3732
- ok: false,
3733
- field,
3734
- label
3735
- };
3736
- return { ok: true };
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 };
3737
3536
  }
3738
3537
  /** A configured team slot must authenticate with that exact team ceiling. */
3739
3538
  function matchesCredentialTeam(current, credentialTeamId) {
@@ -3748,7 +3547,7 @@ function matchesCredentialTeam(current, credentialTeamId) {
3748
3547
  * `docs/.vitepress/config.ts`.
3749
3548
  */
3750
3549
  var AGENT_KEYS_DOC_URL = "https://docs.themolt.net/operate/agent-keys#run-the-daemon-with-an-agent-key";
3751
- /** Scopes a daemon key must carry; mirrors `DAEMON_REQUIRED_SCOPES`. */
3550
+ /** Scopes a new daemon key should be minted with; mirrors `DAEMON_RECOMMENDED_SCOPES`. */
3752
3551
  var DAEMON_KEY_SCOPES = AGENT_CREDENTIAL_SCOPES;
3753
3552
  /**
3754
3553
  * Report where `connect()` will find the key, without ever reading the secret
@@ -3827,14 +3626,24 @@ async function validateStartupBinding(options) {
3827
3626
  async function resolveAgentContext(agentName, options = {}) {
3828
3627
  assertIdentityAlias(agentName);
3829
3628
  const { agentDir, agentRootDir } = resolveIdentityLocation(agentName, options.agentRootDir, { requireConfig: options.credentialSource !== "environment" });
3830
- if (options.credentialSource === "environment") return {
3831
- agentDir,
3832
- agentRootDir,
3833
- agent: await connect({ secretProviders: createNodeSecretProviderRegistry() }),
3834
- credentialSource: "environment"
3835
- };
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
+ }
3836
3642
  const config = await readConfig(agentDir);
3837
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");
3838
3647
  const secretProviders = createNodeSecretProviderRegistry();
3839
3648
  const agentKey = await resolveAgentKey(config, secretProviders, options.teamId);
3840
3649
  if (!agentKey) throw new Error(agentKeyRequiredMessage(agentDir, agentName));
@@ -3845,7 +3654,7 @@ async function resolveAgentContext(agentName, options = {}) {
3845
3654
  configDir: agentDir,
3846
3655
  secretProviders,
3847
3656
  agentKey,
3848
- apiUrl: resolveConfigApiUrl(config, options.envApiUrl)
3657
+ apiUrl: projectApiUrl ?? configuredApiUrl
3849
3658
  }),
3850
3659
  credentialSource: "config",
3851
3660
  credentialTeamId: selectAgentKeyReference(config, options.teamId)?.teamId
@@ -3934,6 +3743,13 @@ function isTransientWhoamiError(error) {
3934
3743
  const statusCode = error.statusCode;
3935
3744
  return typeof statusCode === "number" && (statusCode === 408 || statusCode === 429 || statusCode >= 500);
3936
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
+ }
3937
3753
  //#endregion
3938
3754
  //#region src/config.ts
3939
3755
  /**
@@ -4002,7 +3818,19 @@ function activatePiCodingAgentDir(path, env = {}) {
4002
3818
  Object.assign(process.env, env);
4003
3819
  }
4004
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"];
4005
3826
  return {
3827
+ operatorOAuth: {
3828
+ ...issuer ? { issuer } : {},
3829
+ ...publicUrl ? { publicUrl } : {},
3830
+ ...nativeClientId ? { nativeClientId } : {},
3831
+ ...consoleClientId ? { consoleClientId } : {},
3832
+ ...apiUrl ? { apiUrl } : {}
3833
+ },
4006
3834
  port: process.env["MOLTNET_AGENT_SERVER_PORT"] ?? "",
4007
3835
  allowedOrigins: process.env["MOLTNET_AGENT_SERVER_ALLOWED_ORIGINS"] ?? "",
4008
3836
  root: process.env["MOLTNET_AGENT_SERVER_ROOT"] ?? "",
@@ -4022,6 +3850,378 @@ function loadUpdateEnvConfig() {
4022
3850
  };
4023
3851
  }
4024
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
4025
4225
  //#region src/lib/abort-active-attempt.ts
4026
4226
  /** Best-effort signal cleanup; lease expiry remains the final backstop. */
4027
4227
  async function abortActiveAttemptOnSignal(opts) {
@@ -4201,6 +4401,7 @@ function slugifySessionComponent(input) {
4201
4401
  }
4202
4402
  //#endregion
4203
4403
  //#region src/lib/task-execution-plan.ts
4404
+ var WorkspaceModeMismatchError = class extends Error {};
4204
4405
  function buildDaemonTaskExecutionPlan(task, stateDirs, identity, warmRetentionSec, runtimeProfileWorkspacePolicy = {}, attemptN) {
4205
4406
  const descriptor = deriveTaskSessionDescriptor(task);
4206
4407
  const workspaceMode = resolveTaskWorkspaceMode(task, descriptor.policy, runtimeProfileWorkspacePolicy);
@@ -4274,6 +4475,7 @@ function resolveTaskWorkspaceMode(task, policy, runtimeProfileWorkspacePolicy) {
4274
4475
  const requestedWorkspace = policy.acceptsInputWorkspaceOverride && typeof task.input.execution?.workspace === "string" ? task.input.execution.workspace : null;
4275
4476
  if (isRuntimeProfileWorkspaceMode(requestedWorkspace)) {
4276
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(", ")}`);
4277
4479
  }
4278
4480
  if (profileDefault && allowed.has(profileDefault)) return toDaemonWorkspaceMode(profileDefault);
4279
4481
  if (allowed.has(policy.workspaceMode)) return policy.workspaceMode;
@@ -4381,8 +4583,8 @@ function assertPlanAllowedByWorkspacePolicy(plan, policy, runtimeProfileId) {
4381
4583
  "dedicated_worktree"
4382
4584
  ]);
4383
4585
  const effectiveMode = planToRuntimeProfileWorkspaceMode(plan);
4384
- 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}")`);
4385
- 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(", ")}`);
4386
4588
  }
4387
4589
  function planToRuntimeProfileWorkspaceMode(plan) {
4388
4590
  if (plan.workspaceMode === "scratch_mount") return "none";
@@ -4569,7 +4771,8 @@ function resolveProducerWorkspaceCopySource(producer, stateDirs) {
4569
4771
  if (isDisposableScratchWorkspace(producer, stateDirs)) return null;
4570
4772
  throw new ProducerContextResolutionError(`Producer workspace path is missing on disk: ${workspacePath}`);
4571
4773
  }
4572
- 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");
4573
4776
  if (!existsSync(sharedMountRoot)) throw new ProducerContextResolutionError(`Shared producer mount root is missing on disk: ${sharedMountRoot}`);
4574
4777
  return sharedMountRoot;
4575
4778
  }
@@ -5557,7 +5760,6 @@ var AgentServerStore = class {
5557
5760
  activations: {}
5558
5761
  };
5559
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`);
5560
- 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");
5561
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");
5562
5764
  for (const [alias, activation] of Object.entries(state.activations)) validateActivation(alias, activation);
5563
5765
  for (const [alias, registration] of Object.entries(state.pendingRegistrations)) {
@@ -5661,6 +5863,17 @@ var AgentServerStore = class {
5661
5863
  if (activation.source === "managed") delete state.pendingRegistrations[alias];
5662
5864
  this.writeAgentServerState(state);
5663
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
+ }
5664
5877
  listActivations() {
5665
5878
  return Object.values(this.readAgentServerState().activations).sort((a, b) => a.alias.localeCompare(b.alias));
5666
5879
  }
@@ -5726,6 +5939,24 @@ var AgentServerStore = class {
5726
5939
  writeRun(record) {
5727
5940
  writeJsonAtomic(join(this.runDir(record.id), "run.json"), record);
5728
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
+ }
5729
5960
  listRuns(limit = Number.POSITIVE_INFINITY) {
5730
5961
  let ids;
5731
5962
  try {
@@ -5956,14 +6187,32 @@ function mergePiModels(store, repo) {
5956
6187
  //#endregion
5957
6188
  //#region src/lib/state-dir.ts
5958
6189
  function ensureDaemonStateDirs(mountPath) {
5959
- const rootDir = join(mountPath, ".moltnet", "d");
6190
+ const rootDir = canonicalStatePath(join(mountPath, ".moltnet", "d"));
5960
6191
  const piSessionsDir = join(rootDir, "pi-sessions");
5961
- mkdirSync(piSessionsDir, { recursive: true });
6192
+ mkdirSync(rootDir, {
6193
+ recursive: true,
6194
+ mode: 448
6195
+ });
6196
+ mkdirSync(piSessionsDir, {
6197
+ recursive: true,
6198
+ mode: 448
6199
+ });
5962
6200
  return {
5963
- rootDir,
5964
- piSessionsDir
6201
+ rootDir: realpathSync(rootDir),
6202
+ piSessionsDir: realpathSync(piSessionsDir)
5965
6203
  };
5966
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
+ }
5967
6216
  //#endregion
5968
6217
  //#region src/lib/prepare-runtime-profile.ts
5969
6218
  /** Validate and prepare a profile through the shared daemon execution path. */
@@ -5985,7 +6234,8 @@ async function prepareRuntimeProfile(input) {
5985
6234
  rootDir: profile.mountPath,
5986
6235
  path: profile.source
5987
6236
  };
5988
- const stateDirs = ensureDaemonStateDirs(sandbox.rootDir);
6237
+ const stateDirs = ensureDaemonStateDirs(input.stateRootDir ?? sandbox.rootDir);
6238
+ stateDirs.mountPath = profile.mountPath;
5989
6239
  const slotIdentity = {
5990
6240
  agentName: input.agentName,
5991
6241
  runtimeProfileId: profile.id,
@@ -6002,6 +6252,8 @@ async function prepareRuntimeProfile(input) {
6002
6252
  slotIdentity,
6003
6253
  warmRetentionSec: input.warmRetentionSec,
6004
6254
  workspacePolicy: {
6255
+ workspaceExplicit: input.workspaceExplicit,
6256
+ profileName: profile.name,
6005
6257
  defaultWorkspaceMode: profile.defaultWorkspaceMode,
6006
6258
  allowedWorkspaceModes: profile.allowedWorkspaceModes
6007
6259
  },
@@ -6012,6 +6264,32 @@ async function prepareRuntimeProfile(input) {
6012
6264
  };
6013
6265
  }
6014
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
6015
6293
  //#region src/lib/runtime-context.ts
6016
6294
  var storage = new AsyncLocalStorage();
6017
6295
  function runWithDaemonRuntimeContext(context, callback) {
@@ -6591,6 +6869,7 @@ async function runPolling(opts) {
6591
6869
  args: opts.argv,
6592
6870
  options: {
6593
6871
  ...runtimeCommandOptionDefs(),
6872
+ ...projectRunOptionDefs(),
6594
6873
  team: { type: "string" },
6595
6874
  "task-types": { type: "string" },
6596
6875
  "correlation-id": { type: "string" },
@@ -6607,12 +6886,12 @@ async function runPolling(opts) {
6607
6886
  }
6608
6887
  }
6609
6888
  });
6610
- if (!values.team) {
6889
+ if (!values.team && !values.binding && !values.project && !values["config-file"]) {
6611
6890
  console.error("Missing required flag: --team\n");
6612
6891
  console.error(opts.helpText);
6613
6892
  return 1;
6614
6893
  }
6615
- const teamId = values.team;
6894
+ let teamId = values.team ?? "";
6616
6895
  const profileValues = parseProfileValues(values.profile);
6617
6896
  if (profileValues.length === 0) {
6618
6897
  console.error("Missing required flag: --profile\n");
@@ -6655,6 +6934,41 @@ async function runPolling(opts) {
6655
6934
  }
6656
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).`);
6657
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
+ }
6658
6972
  const credentialSources = {
6659
6973
  profileRequirements: cfg.profileCredentialRequirements,
6660
6974
  bindings: cfg.credentialBindings
@@ -6668,6 +6982,7 @@ async function runPolling(opts) {
6668
6982
  agentRootDir: explicitAgentRootDir,
6669
6983
  credentialSource: cfg.credentialSource,
6670
6984
  envApiUrl: cfg.apiUrl,
6985
+ projectApiUrl: selection.binding?.apiUrl,
6671
6986
  teamId
6672
6987
  });
6673
6988
  gate = "authenticate_and_bind";
@@ -6722,18 +7037,25 @@ async function runPolling(opts) {
6722
7037
  throw error;
6723
7038
  }
6724
7039
  })();
6725
- const daemonRootDir = explicitAgentRootDir ?? process.cwd();
6726
- const resolvedProfiles = await resolveRuntimeProfiles({
7040
+ const daemonRootDir = selection.binding || values.source ? selection.source ?? process.cwd() : explicitAgentRootDir ?? process.cwd();
7041
+ const resolvedProfiles = (await resolveRuntimeProfiles({
6727
7042
  agent: ctx.agent,
6728
7043
  profiles: profileValues,
6729
7044
  teamId,
6730
7045
  cwd: daemonRootDir
6731
- });
7046
+ })).map((profile) => applyProjectWorkspacePolicy(profile, selection));
6732
7047
  const { logger, shutdown: shutdownLogger } = createRootLogger({
6733
7048
  name: `agent-daemon.${opts.modeLabel}`,
6734
7049
  level: cfg.logLevel || (identity.debug ? "debug" : "info")
6735
7050
  });
6736
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,
6737
7059
  mode: opts.modeLabel,
6738
7060
  agent: identity.agent,
6739
7061
  teamId,
@@ -6756,6 +7078,8 @@ async function runPolling(opts) {
6756
7078
  agent: ctx.agent,
6757
7079
  agentName: identity.agent,
6758
7080
  profile,
7081
+ stateRootDir: selection.stateRootDir,
7082
+ workspaceExplicit: selection.workspaceExplicit,
6759
7083
  prerequisiteEnv: cfg.profilePrerequisiteEnv,
6760
7084
  runtimeAdapter,
6761
7085
  runtimeInstanceId,
@@ -6787,6 +7111,9 @@ async function runPolling(opts) {
6787
7111
  agent: ctx.agent,
6788
7112
  endpoint: cfg.otelEndpoint,
6789
7113
  resourceAttributes: {
7114
+ "moltnet.project.id": selection.projectId ?? "general",
7115
+ "moltnet.project.selection": selection.selectedBy,
7116
+ "moltnet.workspace.strategy": selection.strategy,
6790
7117
  "moltnet.team.id": teamId,
6791
7118
  "moltnet.agent.name": identity.agent,
6792
7119
  "moltnet.credential.source": ctx.credentialSource,
@@ -6905,7 +7232,7 @@ async function runPolling(opts) {
6905
7232
  try {
6906
7233
  runtime = new AgentRuntime({
6907
7234
  logger: rootLogger,
6908
- source: new PollingApiTaskSource({
7235
+ source: createProjectPollingSource(selection, {
6909
7236
  agent: ctx.agent,
6910
7237
  teamId,
6911
7238
  taskTypes: taskTypes.length > 0 ? taskTypes : void 0,
@@ -7027,7 +7354,7 @@ async function runPolling(opts) {
7027
7354
  error: {
7028
7355
  code: err instanceof ProducerContextResolutionError ? "producer_context_missing" : "execution_plan_failed",
7029
7356
  message,
7030
- retryable: false
7357
+ retryable: err instanceof WorkspaceModeMismatchError
7031
7358
  }
7032
7359
  };
7033
7360
  }
@@ -7227,6 +7554,7 @@ async function runOnce(argv, runtimeAdapter = defaultPiDaemonAdapter) {
7227
7554
  args: argv,
7228
7555
  options: {
7229
7556
  ...runtimeCommandOptionDefs(),
7557
+ ...projectRunOptionDefs(),
7230
7558
  "task-id": {
7231
7559
  type: "string",
7232
7560
  short: "t"
@@ -7264,6 +7592,39 @@ async function runOnce(argv, runtimeAdapter = defaultPiDaemonAdapter) {
7264
7592
  return 1;
7265
7593
  }
7266
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
+ }
7267
7628
  const credentialSources = {
7268
7629
  profileRequirements: cfg.profileCredentialRequirements,
7269
7630
  bindings: cfg.credentialBindings
@@ -7277,13 +7638,14 @@ async function runOnce(argv, runtimeAdapter = defaultPiDaemonAdapter) {
7277
7638
  agentRootDir: explicitAgentRootDir,
7278
7639
  credentialSource: cfg.credentialSource,
7279
7640
  envApiUrl: cfg.apiUrl,
7280
- teamId: values.team
7641
+ projectApiUrl: selection.binding?.apiUrl,
7642
+ teamId: selection.teamId
7281
7643
  });
7282
7644
  gate = "authenticate_and_bind";
7283
7645
  const whoami = await validateStartupBinding({
7284
7646
  agent: resolvedContext.agent,
7285
7647
  credentialTeamId: resolvedContext.credentialTeamId,
7286
- teamId: values.team,
7648
+ teamId: selection.teamId,
7287
7649
  expectedAgent: cfg.expectedAgent
7288
7650
  });
7289
7651
  gate = "resolve_signing_material";
@@ -7330,18 +7692,25 @@ async function runOnce(argv, runtimeAdapter = defaultPiDaemonAdapter) {
7330
7692
  throw error;
7331
7693
  }
7332
7694
  })();
7333
- const daemonRootDir = explicitAgentRootDir ?? process.cwd();
7334
- const profile = await resolveRuntimeProfile({
7695
+ const daemonRootDir = selection.binding || values.source ? selection.source ?? process.cwd() : explicitAgentRootDir ?? process.cwd();
7696
+ const profile = applyProjectWorkspacePolicy(await resolveRuntimeProfile({
7335
7697
  agent: ctx.agent,
7336
7698
  profile: values.profile,
7337
- teamId: values.team,
7699
+ teamId: selection.teamId,
7338
7700
  cwd: daemonRootDir
7339
- });
7701
+ }), selection);
7340
7702
  const { logger, shutdown: shutdownLogger } = createRootLogger({
7341
7703
  name: "agent-daemon.once",
7342
7704
  level: cfg.logLevel || (identity.debug ? "debug" : "info")
7343
7705
  });
7344
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,
7345
7714
  mode: "once",
7346
7715
  agent: identity.agent,
7347
7716
  provider: profile.provider,
@@ -7365,6 +7734,8 @@ async function runOnce(argv, runtimeAdapter = defaultPiDaemonAdapter) {
7365
7734
  agent: ctx.agent,
7366
7735
  agentName: identity.agent,
7367
7736
  profile,
7737
+ stateRootDir: selection.stateRootDir,
7738
+ workspaceExplicit: selection.workspaceExplicit,
7368
7739
  prerequisiteEnv: cfg.profilePrerequisiteEnv,
7369
7740
  runtimeAdapter,
7370
7741
  runtimeInstanceId,
@@ -7382,6 +7753,9 @@ async function runOnce(argv, runtimeAdapter = defaultPiDaemonAdapter) {
7382
7753
  agent: ctx.agent,
7383
7754
  endpoint: cfg.otelEndpoint,
7384
7755
  resourceAttributes: {
7756
+ "moltnet.project.id": selection.projectId ?? "general",
7757
+ "moltnet.project.selection": selection.selectedBy,
7758
+ "moltnet.workspace.strategy": selection.strategy,
7385
7759
  "moltnet.task.id": taskId,
7386
7760
  "moltnet.agent.name": identity.agent,
7387
7761
  "moltnet.credential.source": ctx.credentialSource,
@@ -7576,7 +7950,7 @@ async function runOnce(argv, runtimeAdapter = defaultPiDaemonAdapter) {
7576
7950
  });
7577
7951
  runtime = new AgentRuntime({
7578
7952
  logger: rootLogger,
7579
- source: new ApiTaskSource({
7953
+ source: createProjectOnceSource(selection, {
7580
7954
  agent: ctx.agent,
7581
7955
  taskId,
7582
7956
  teamId: profile.teamId,
@@ -7669,6 +8043,18 @@ function runPoll(argv, runtimeAdapter) {
7669
8043
  //#region src/lib/provider-lock.ts
7670
8044
  var DEFAULT_LOCK_TIMEOUT_MS = 3e4;
7671
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
+ }
7672
8058
  var ProviderLockError = class extends Error {
7673
8059
  name = "ProviderLockError";
7674
8060
  constructor(code, message, options) {
@@ -7697,6 +8083,7 @@ async function withNamedProviderLock(root, name, work, options) {
7697
8083
  let compromised;
7698
8084
  const startedAt = Date.now();
7699
8085
  const timeoutMs = options.timeoutMs ?? DEFAULT_LOCK_TIMEOUT_MS;
8086
+ const warnAfterMs = warningThresholdMs(timeoutMs);
7700
8087
  const lockfilePath = join(locksDir, `${name}.lock`);
7701
8088
  let warned = false;
7702
8089
  let release;
@@ -7704,7 +8091,7 @@ async function withNamedProviderLock(root, name, work, options) {
7704
8091
  if (options.signal?.aborted) throw new ProviderLockError("lock_aborted", `provider lock acquisition was cancelled for "${name}"`, { cause: options.signal.reason });
7705
8092
  const elapsedMs = Date.now() - startedAt;
7706
8093
  if (elapsedMs >= timeoutMs) throw new ProviderLockError("lock_timeout", `timed out waiting for provider lock "${name}"`);
7707
- if (!warned && elapsedMs >= LOCK_WAIT_WARNING_MS) {
8094
+ if (!warned && elapsedMs >= warnAfterMs) {
7708
8095
  warned = true;
7709
8096
  options.logger?.warn({
7710
8097
  code: "provider_lock_contended",
@@ -8887,29 +9274,97 @@ function registerLoopbackSecurity(app, options) {
8887
9274
  });
8888
9275
  }
8889
9276
  //#endregion
8890
- //#region ../../libs/loopback-companion/src/fetch-metadata.ts
8891
- function headerValue(headers, name) {
8892
- const value = headers[name];
8893
- return typeof value === "string" ? value : void 0;
8894
- }
8895
- /**
8896
- * Require that a request is a top-level browser navigation (used by local
8897
- * approval pages that must be opened as a document, never fetched).
8898
- */
8899
- function assertNavigationRequest(headers) {
8900
- const site = headerValue(headers, "sec-fetch-site");
8901
- const mode = headerValue(headers, "sec-fetch-mode");
8902
- const destination = headerValue(headers, "sec-fetch-dest");
8903
- 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;
8904
9301
  }
8905
- /**
8906
- * Reject an explicit cross-site Fetch-Metadata signal. Only the explicit
8907
- * `cross-site` value is rejected: Safari may omit Fetch Metadata on
8908
- * same-origin form submissions, so the absence of the header is not treated
8909
- * as a violation — callers keep their one-time token as the primary control.
8910
- */
8911
- function rejectExplicitCrossSite(headers) {
8912
- 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"));
8913
9368
  }
8914
9369
  //#endregion
8915
9370
  //#region src/lib/agent-server/lock.ts
@@ -8968,162 +9423,249 @@ async function withAgentServerLock(root, work, options) {
8968
9423
  }
8969
9424
  }
8970
9425
  //#endregion
8971
- //#region src/lib/agent-server/pairing.ts
9426
+ //#region src/lib/agent-server/native-grant.ts
8972
9427
  /**
8973
- * One-click pairing ceremony for the agent server, mirroring the signer's
8974
- * session/ceremony pattern (#2062 design):
9428
+ * Environment variable the supervising desktop app uses to hand this server
9429
+ * process its control token.
8975
9430
  *
8976
- * 1. Console (allowed origin) POSTs `/v1/pairings` pending pairing bound
8977
- * to that origin, with a one-time confirmation token.
8978
- * 2. Console opens `http://127.0.0.1:<port>/pairings/<id>` in a new tab
8979
- * a navigation-gated local approval page naming the origin.
8980
- * 3. One click POSTs the confirmation form (explicit cross-site rejected;
8981
- * the one-time token is the primary CSRF control).
8982
- * 4. Console claims `/v1/pairings/<id>/claim` from the same origin and
8983
- * receives the bearer token exactly once; only its SHA-256 remains in
8984
- * this supervisor process.
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;
9438
+ /**
9439
+ * Consume the supervisor's native token from the environment and grant it.
8985
9440
  *
8986
- * The token exists for shared-machine cross-user protection and to bind
8987
- * "this console session is the operator" browser-vs-browser isolation is
8988
- * already covered by the loopback-companion origin checks. Grants are
8989
- * deliberately process-scoped: after the listening socket changes owners, a
8990
- * token disclosed to an impostor on that port cannot authenticate to a later
8991
- * 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.
9445
+ *
9446
+ * @returns whether a native grant was issued.
8992
9447
  */
8993
- var PENDING_TTL_MS = 600 * 1e3;
8994
- var AgentServerPairingError = class extends Error {
8995
- name = "AgentServerPairingError";
8996
- constructor(code, message) {
8997
- super(message);
8998
- this.code = code;
8999
- }
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";
9000
9459
  };
9001
- function sha256Hex(value) {
9002
- return createHash("sha256").update(value, "utf8").digest("hex");
9003
- }
9004
- function safeEqual(a, b) {
9005
- const left = Buffer.from(a, "utf8");
9006
- const right = Buffer.from(b, "utf8");
9007
- return left.length === right.length && timingSafeEqual(left, right);
9008
- }
9009
- var PairingService = class {
9010
- pending = /* @__PURE__ */ new Map();
9011
- paired = /* @__PURE__ */ new Map();
9012
- constructor(options = {}) {
9013
- 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();
9014
9465
  }
9015
- now() {
9016
- 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");
9017
9469
  }
9018
- token() {
9019
- return this.options.randomToken?.() ?? randomBytes(32).toString("base64url");
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
+ }
9020
9512
  }
9021
- sweep() {
9022
- const now = this.now();
9023
- for (const [id, pairing] of this.pending) if (pairing.expiresAt <= now) this.pending.delete(id);
9513
+ cancel() {
9514
+ this.pending?.abort();
9024
9515
  }
9025
- start(origin) {
9026
- this.sweep();
9027
- const pairingId = randomBytes(12).toString("hex");
9028
- this.pending.set(pairingId, {
9029
- origin,
9030
- confirmToken: this.token(),
9031
- expiresAt: this.now() + PENDING_TTL_MS,
9032
- approved: false,
9033
- bearerToken: null
9034
- });
9516
+ removeOperator() {
9517
+ this.cancel();
9518
+ rmSync(join(this.root, "operator.json"), { force: true });
9519
+ this.operator = null;
9520
+ }
9521
+ metadata() {
9035
9522
  return {
9036
- pairingId,
9037
- 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
9038
9530
  };
9039
9531
  }
9040
- /** Data for the local approval page. */
9041
- approval(pairingId) {
9042
- const pairing = this.require(pairingId);
9043
- 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");
9044
9547
  return {
9045
- origin: pairing.origin,
9046
- confirmToken: pairing.confirmToken
9548
+ issuer: payload.iss,
9549
+ subject: payload.sub,
9550
+ provisioning: claims["moltnet:provisioning"]
9047
9551
  };
9048
9552
  }
9049
- confirm(pairingId, confirmToken) {
9050
- const pairing = this.require(pairingId);
9051
- if (pairing.approved || !safeEqual(pairing.confirmToken, confirmToken)) throw new AgentServerPairingError("pairing_invalid", "Confirmation token is not valid");
9052
- pairing.approved = true;
9053
- pairing.bearerToken = this.token();
9054
- return { origin: pairing.origin };
9055
- }
9056
- claim(pairingId, origin) {
9057
- const pairing = this.require(pairingId);
9058
- if (pairing.origin !== origin) throw new AgentServerPairingError("pairing_origin_mismatch", "Pairing belongs to a different origin");
9059
- if (!pairing.approved || !pairing.bearerToken) throw new AgentServerPairingError("pairing_not_approved", "Pairing has not been approved yet");
9060
- const token = pairing.bearerToken;
9061
- this.pending.delete(pairingId);
9062
- this.paired.set(origin, sha256Hex(token));
9063
- return { token };
9064
- }
9065
- verify(origin, token) {
9066
- const tokenHash = this.paired.get(origin);
9067
- if (!tokenHash || !safeEqual(tokenHash, sha256Hex(token))) throw new AgentServerPairingError("pairing_token_invalid", "Pairing token is not valid for this origin");
9068
- }
9069
- require(pairingId) {
9070
- this.sweep();
9071
- const pairing = this.pending.get(pairingId);
9072
- if (!pairing) throw new AgentServerPairingError("pairing_not_found", "Pairing was not found or has expired");
9073
- 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
+ }
9074
9667
  }
9075
9668
  };
9076
- function escapeHtml(value) {
9077
- return value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll("\"", "&quot;");
9078
- }
9079
- /** Minimal, dependency-free local approval page. */
9080
- function renderPairingApprovalPage(input) {
9081
- return `<!doctype html>
9082
- <html lang="en">
9083
- <head>
9084
- <meta charset="utf-8" />
9085
- <meta name="viewport" content="width=device-width, initial-scale=1" />
9086
- <title>MoltNet Agent — approve connection</title>
9087
- <style>
9088
- :root { color-scheme: light dark; }
9089
- body { margin: 0; font: 16px/1.5 system-ui, sans-serif; display: grid; place-items: center; min-height: 100vh; background: Canvas; color: CanvasText; }
9090
- main { max-width: 26rem; padding: 2rem; border: 1px solid color-mix(in srgb, CanvasText 20%, transparent); border-radius: 12px; }
9091
- h1 { font-size: 1.2rem; margin: 0 0 0.5rem; }
9092
- code { font-size: 0.95em; word-break: break-all; }
9093
- 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; }
9094
- p.small { font-size: 0.85rem; opacity: 0.75; }
9095
- </style>
9096
- </head>
9097
- <body>
9098
- <script>
9099
- // The Console must remain this popup's opener until it finishes navigating
9100
- // from about:blank. Safari rejects that cross-origin navigation otherwise.
9101
- // Once this trusted local approval document has loaded, it needs no opener.
9102
- window.opener = null;
9103
- <\/script>
9104
- <main>
9105
- <h1>Allow this site to manage local MoltNet agents?</h1>
9106
- <p><code>${escapeHtml(input.origin)}</code> asks to configure agents and start or stop local daemon runs on this machine.</p>
9107
- <p class="small">Approve only if you opened that page yourself. This grant lasts until the local supervisor stops.</p>
9108
- <form method="post" action="/pairings/${escapeHtml(input.pairingId)}/confirm">
9109
- <input type="hidden" name="confirmToken" value="${escapeHtml(input.confirmToken)}" />
9110
- <button type="submit">Approve</button>
9111
- </form>
9112
- </main>
9113
- </body>
9114
- </html>
9115
- `;
9116
- }
9117
- function renderPairingResultPage(input) {
9118
- return `<!doctype html>
9119
- <html lang="en">
9120
- <head><meta charset="utf-8" /><meta name="viewport" content="width=device-width, initial-scale=1" /><title>${escapeHtml(input.title)}</title>
9121
- <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>
9122
- </head>
9123
- <body><main role="status"><h1>${escapeHtml(input.title)}</h1><p>${escapeHtml(input.message)}</p><p>You can close this tab.</p></main></body>
9124
- </html>
9125
- `;
9126
- }
9127
9669
  //#endregion
9128
9670
  //#region src/lib/agent-server/provider-login.ts
9129
9671
  /**
@@ -9669,53 +10211,6 @@ async function attachExternalAgent(store, secretProviders, input, connectAgent =
9669
10211
  releaseAlias();
9670
10212
  }
9671
10213
  }
9672
- /** Load and authenticate the current config, then refresh its derived pin. */
9673
- async function verifyAgentActivation(store, alias, managedSecretProviders, externalSecretProviders, connectAgent = connect, signal, teamId) {
9674
- const activation = requireActivation(store, alias);
9675
- teamId ??= activation.boundTeamId;
9676
- const verified = activation.source === "managed" ? await verifyManagedActivation(store, activation, managedSecretProviders, connectAgent, signal, teamId) : await verifyExternalActivation(store, activation, externalSecretProviders, connectAgent, signal, teamId);
9677
- assertSubjectMatches(verified.whoami, verified.config, "authenticated whoami", `agent "${activation.alias}" config`);
9678
- if (verified.whoami.subjectId !== activation.subjectId) throw new AgentServerIdentityError("verification_failed", `authenticated whoami subject does not match agent "${activation.alias}" pinned activation`);
9679
- const identity = identityFromConfig(verified.config);
9680
- assertIdentityMatches(verified.whoami, identity, "authenticated whoami", `agent "${activation.alias}" config`);
9681
- const boundTeamId = boundTeamIdFromWhoami(verified.whoami);
9682
- if (!selectAgentKeyReference(verified.config, teamId)?.teamId && activation.boundTeamId !== boundTeamId) throw new AgentServerIdentityError("verification_failed", `authenticated whoami team binding does not match agent "${activation.alias}" pinned activation`);
9683
- const refreshed = {
9684
- ...activation,
9685
- ...identity
9686
- };
9687
- if (activation.publicKey !== refreshed.publicKey || activation.fingerprint !== refreshed.fingerprint) {
9688
- store.writeActivation(refreshed);
9689
- process.stderr.write(`agent-server: refreshed the authenticated signing identity for ${JSON.stringify(activation.alias)}\n`);
9690
- }
9691
- return {
9692
- activation: refreshed,
9693
- config: verified.config,
9694
- ...boundTeamId ? { boundTeamId } : {}
9695
- };
9696
- }
9697
- async function verifyManagedActivation(store, activation, secretProviders, connectAgent, signal, teamId) {
9698
- const configPath = store.agentPath(activation.alias);
9699
- const config = await readCurrentConfig(configPath);
9700
- assertActivatedConfig(config, activation, configPath, requireConfigApiUrl(config, configPath), activation.apiUrl);
9701
- return {
9702
- config,
9703
- whoami: await authenticateConfig(config, configPath, activation.apiUrl, secretProviders, connectAgent, signal, teamId)
9704
- };
9705
- }
9706
- async function verifyExternalActivation(store, activation, secretProviders, connectAgent, signal, teamId) {
9707
- const central = activation.configPath === store.agentPath(activation.alias);
9708
- if (!central) externalAgentLocation(activation.configPath);
9709
- assertTrustedConfigApiUrl(activation.configApiUrl);
9710
- const config = await readCurrentConfig(activation.configPath);
9711
- if (central && !hasAgentKeyConfiguration(config)) throw new AgentServerIdentityError("unsupported_credential", `central identity "${activation.alias}" needs a stored agent key before the Agent Server can run it`);
9712
- assertActivatedConfig(config, activation, activation.configPath, requireTrustedConfigApiUrl(config, activation.configPath), activation.configApiUrl);
9713
- const effectiveApiUrl = requireTrustedApiOverride(activation.apiUrl, activation.configApiUrl, activation.configPath);
9714
- return {
9715
- config,
9716
- whoami: await authenticateConfig(config, activation.configPath, effectiveApiUrl, secretProviders, connectAgent, signal, teamId)
9717
- };
9718
- }
9719
10214
  /** Never let request or activation metadata redirect persisted credentials. */
9720
10215
  function requireTrustedApiOverride(override, configApiUrl, configPath) {
9721
10216
  if (!override) return configApiUrl;
@@ -9849,10 +10344,133 @@ function publicAgentView(store, activation) {
9849
10344
  function boundTeamIdFromWhoami(whoami) {
9850
10345
  return whoami.credentialBinding?.bindingScope === "team" ? whoami.credentialBinding.boundTeamId ?? void 0 : void 0;
9851
10346
  }
9852
- function requireActivation(store, alias) {
9853
- const activation = store.readActivation(alias);
9854
- if (!activation) throw new AgentServerStoreError("not_found", `agent "${alias}" is not configured`);
9855
- return activation;
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
+ });
9856
10474
  }
9857
10475
  //#endregion
9858
10476
  //#region src/lib/agent-server/runs.ts
@@ -9893,6 +10511,19 @@ var INHERITED_MOLTNET_ENV_NAMES = new Set([
9893
10511
  "MOLTNET_SIGNER_URL",
9894
10512
  "MOLTNET_TRACE_IDLE_POLLING"
9895
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
+ }
9896
10527
  var AgentServerRunError = class extends Error {
9897
10528
  name = "AgentServerRunError";
9898
10529
  constructor(code, message) {
@@ -9975,23 +10606,16 @@ var RunManager = class {
9975
10606
  String(runtimeSettings.warmRetentionSec),
9976
10607
  ...target.extraArgs
9977
10608
  ];
10609
+ env["MOLTNET_AGENT_KEY"] = requireCredentialSnapshot(agent).agentKey;
10610
+ env["MOLTNET_API_URL"] = activation.apiUrl ?? (activation.source === "external" ? activation.configApiUrl : "");
9978
10611
  if (activation.source === "managed") {
9979
- const reference = selectAgentKeyReference(config, spec.teamId)?.reference;
9980
- if (!reference || !config.keys.private_key_ref) throw new AgentServerRunError("invalid_spec", `managed config for "${activation.alias}" is missing canonical secret references`);
9981
- env["MOLTNET_API_URL"] = activation.apiUrl;
9982
- env["MOLTNET_AGENT_KEY_REF"] = formatSecretReferenceString(reference);
10612
+ if (!config.keys.private_key_ref) throw new AgentServerRunError("invalid_spec", "The managed signing key reference is missing");
9983
10613
  env["MOLTNET_PRIVATE_KEY_REF"] = formatSecretReferenceString(config.keys.private_key_ref);
9984
10614
  env["MOLTNET_SECRET_ROOT"] = this.store.secretsDir;
9985
- } else {
9986
- env["MOLTNET_API_URL"] = activation.apiUrl ?? activation.configApiUrl;
9987
- try {
9988
- const agentKey = await resolveAgentKey(config, this.options.externalSecretProviders, spec.teamId);
9989
- if (!agentKey) throw new Error("external daemon config has no agent key");
9990
- env["MOLTNET_AGENT_KEY"] = agentKey;
9991
- env["MOLTNET_PRIVATE_KEY"] = await resolveIdentitySeed(config, this.options.externalSecretProviders);
9992
- } catch {
9993
- throw new AgentServerRunError("invalid_spec", `external credentials for "${activation.alias}" could not be projected`);
9994
- }
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");
9995
10619
  }
9996
10620
  env["MOLTNET_EXPECTED_SUBJECT_ID"] = activation.subjectId;
9997
10621
  env["MOLTNET_EXPECTED_SUBJECT_TYPE"] = "agent";
@@ -10025,9 +10649,9 @@ var RunManager = class {
10025
10649
  }
10026
10650
  async startReserved(spec, signal) {
10027
10651
  this.assertStartOpen(signal);
10028
- const agent = await (this.options.verifyActivationImpl ?? verifyAgentActivation)(this.store, spec.agent, this.options.secretProviders, this.options.externalSecretProviders, void 0, signal, spec.teamId).catch((cause) => {
10029
- if (cause instanceof AgentServerIdentityError && cause.code === "verification_failed") throw new AgentServerIdentityError(cause.code, `Cannot start agent "${spec.agent}" for team "${spec.teamId}": credential verification failed. Check the selected team key and activation.`, { cause });
10030
- throw cause;
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.`);
10031
10655
  });
10032
10656
  this.assertStartOpen(signal);
10033
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}`);
@@ -10104,7 +10728,8 @@ var RunManager = class {
10104
10728
  id,
10105
10729
  status: "running",
10106
10730
  pid: child.pid,
10107
- startedAt: (this.options.now?.() ?? /* @__PURE__ */ new Date()).toISOString()
10731
+ startedAt: (this.options.now?.() ?? /* @__PURE__ */ new Date()).toISOString(),
10732
+ credential: requireCredentialSnapshot(agent).metadata
10108
10733
  };
10109
10734
  child.once("exit", (code, signal) => {
10110
10735
  const activeRun = this.active.get(id);
@@ -10119,7 +10744,8 @@ var RunManager = class {
10119
10744
  });
10120
10745
  this.persistRunCompletion(id, spec.agent, {
10121
10746
  status,
10122
- exitCode: code
10747
+ exitCode: code,
10748
+ ...status === "failed" ? { lastError: describeFailure(code, signal) } : {}
10123
10749
  });
10124
10750
  });
10125
10751
  child.once("error", (error) => {
@@ -10130,7 +10756,13 @@ var RunManager = class {
10130
10756
  transition: "spawn_failed",
10131
10757
  ...safeRunError(error)
10132
10758
  });
10133
- 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
+ });
10134
10766
  });
10135
10767
  this.store.writeRun(record);
10136
10768
  this.log("info", "agent server run started", {
@@ -10157,7 +10789,7 @@ var RunManager = class {
10157
10789
  }
10158
10790
  async resolveRuntimeModule(spec, activated, cwd) {
10159
10791
  const profiles = await resolveRuntimeProfiles({
10160
- agent: await this.connectActivatedAgent(activated, spec.teamId),
10792
+ agent: await this.connectAgent(activated, spec.teamId),
10161
10793
  profiles: spec.profiles,
10162
10794
  teamId: spec.teamId,
10163
10795
  cwd
@@ -10179,14 +10811,9 @@ var RunManager = class {
10179
10811
  if (kind === "gondolin_pi") return void 0;
10180
10812
  throw new AgentServerRunError("invalid_spec", `No local runtime is registered for profile kind "${kind}".`);
10181
10813
  }
10182
- async connectActivatedAgent(activated, teamId) {
10183
- const { activation, config } = activated;
10184
- const agentKey = await resolveAgentKey(config, activation.source === "managed" ? this.options.secretProviders : this.options.externalSecretProviders, teamId);
10185
- if (!agentKey) throw new AgentServerRunError("invalid_spec", `agent "${activation.alias}" has no agent key`);
10186
- return connect({
10187
- agentKey,
10188
- apiUrl: activation.source === "managed" ? activation.apiUrl : activation.apiUrl ?? activation.configApiUrl
10189
- });
10814
+ connectAgent(activated, teamId) {
10815
+ if (activated.boundTeamId !== teamId) throw new AgentServerRunError("invalid_spec", "Snapshot team mismatch");
10816
+ return Promise.resolve(requireCredentialSnapshot(activated).client);
10190
10817
  }
10191
10818
  stop(id) {
10192
10819
  const record = this.store.readRun(id);
@@ -10211,6 +10838,22 @@ var RunManager = class {
10211
10838
  if (!record) throw new AgentServerStoreError("not_found", `run "${id}" was not found`);
10212
10839
  return record;
10213
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
+ }
10214
10857
  list(limit = Number.POSITIVE_INFINITY) {
10215
10858
  if (!Number.isFinite(limit)) return this.store.listRuns();
10216
10859
  const active = [...this.active.keys()].map((id) => this.store.readRun(id)).filter((record) => record !== null).sort((a, b) => b.startedAt.localeCompare(a.startedAt));
@@ -10467,6 +11110,21 @@ var LOCKFILE_NAMES = [
10467
11110
  ];
10468
11111
  /** Local, operator-owned allowlist for executable daemon runtime modules. */
10469
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
+ }
10470
11128
  constructor(root) {
10471
11129
  this.root = root;
10472
11130
  }
@@ -10489,7 +11147,7 @@ var RuntimeRegistry = class {
10489
11147
  const adapter = await loadDaemonRuntimeAdapter(specifier, { cwd });
10490
11148
  if (adapter.runtimeKind !== kind) throw new Error(`Runtime module provides "${adapter.runtimeKind}", not registered kind "${kind}".`);
10491
11149
  if (!moduleUrl.startsWith("file:")) throw new Error("Runtime registration must resolve to a local file URL.");
10492
- const entryHash = hashFile(new URL(moduleUrl));
11150
+ const entryHash = hashPath(new URL(moduleUrl));
10493
11151
  const lockfilePath = isPackageSpecifier(specifier) ? findLockfile(cwd) : void 0;
10494
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.");
10495
11153
  const entry = {
@@ -10515,18 +11173,16 @@ var RuntimeRegistry = class {
10515
11173
  writeRegistry(this.path, next);
10516
11174
  return true;
10517
11175
  }
10518
- resolve(kind) {
11176
+ resolve(kind, options = {}) {
10519
11177
  kind = assertStoreName("runtime kind", kind);
10520
11178
  const entry = this.list().find((candidate) => candidate.kind === kind);
10521
11179
  if (!entry) return void 0;
10522
- if (hashFile(new URL(entry.moduleUrl)) !== entry.entryHash) throw new Error(`Registered runtime "${kind}" has changed; re-register it before starting a run.`);
10523
- 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.`);
10524
11183
  return entry;
10525
11184
  }
10526
11185
  };
10527
- function hashFile(url) {
10528
- return createHash("sha256").update(readFileSync(url)).digest("hex");
10529
- }
10530
11186
  function hashPath(path) {
10531
11187
  return createHash("sha256").update(readFileSync(path)).digest("hex");
10532
11188
  }
@@ -10556,6 +11212,229 @@ function writeRegistry(path, entries) {
10556
11212
  renameSync(temp, path);
10557
11213
  }
10558
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
10559
11438
  //#region src/lib/agent-server/protocol.ts
10560
11439
  var DateTime = Type.String({ format: "date-time" });
10561
11440
  var StringList = Type.Array(Type.String());
@@ -10601,6 +11480,63 @@ var AgentServerProviderSchema = Type.Object({
10601
11480
  models: ProviderModelList,
10602
11481
  hasApiKey: Type.Boolean()
10603
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 }) });
10604
11540
  var AgentServerRunRecordSchema = Type.Object({
10605
11541
  id: Type.String(),
10606
11542
  agent: Type.String(),
@@ -10617,6 +11553,11 @@ var AgentServerRunRecordSchema = Type.Object({
10617
11553
  ]),
10618
11554
  pid: Type.Optional(Type.Number()),
10619
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
+ })),
10620
11561
  startedAt: DateTime,
10621
11562
  endedAt: Type.Optional(DateTime)
10622
11563
  }, { $id: "AgentServerRunRecord" });
@@ -10653,12 +11594,6 @@ var AgentServerStatusSchema = Type.Object({
10653
11594
  warmRetentionSec: Type.Integer({ minimum: 0 })
10654
11595
  })
10655
11596
  }, { $id: "AgentServerStatus" });
10656
- var PairingStartedSchema = Type.Object({
10657
- pairingId: Type.String(),
10658
- approvalPath: Type.String()
10659
- }, { $id: "PairingStarted" });
10660
- var PairingClaimedSchema = Type.Object({ token: Type.String() }, { $id: "PairingClaimed" });
10661
- var PairingParamsSchema = Type.Object({ pairingId: Type.String() });
10662
11597
  var ProviderParamsSchema = Type.Object({ providerId: Type.String() });
10663
11598
  var AgentParamsSchema = Type.Object({ agentName: Type.String() });
10664
11599
  var RunParamsSchema = Type.Object({ runId: Type.String() });
@@ -10704,19 +11639,20 @@ var AGENT_SERVER_SCHEMAS = [
10704
11639
  AgentServerIdentitySchema,
10705
11640
  AgentServerTaskTypeSchema,
10706
11641
  AgentServerProviderSchema,
11642
+ AgentServerCatalogueTeamSchema,
11643
+ AgentServerCatalogueProfileSchema,
11644
+ AgentServerCatalogueSchema,
10707
11645
  AgentServerRunRecordSchema,
10708
11646
  AgentServerRunSchema,
10709
11647
  AgentServerSubscriptionSchema,
10710
11648
  AgentServerSubscriptionLoginSchema,
10711
11649
  AgentServerStatusSchema,
10712
- PairingStartedSchema,
10713
- PairingClaimedSchema,
10714
11650
  ReconcileAgentResultSchema,
10715
11651
  DiscoverModelsSchema,
10716
11652
  CancelledSubscriptionSchema,
10717
11653
  LogStreamSchema
10718
11654
  ];
10719
- var pairedSecurity = [{ agentServerToken: [] }];
11655
+ var localControlSecurity = [{ agentServerToken: [] }];
10720
11656
  var problemResponse = { default: schemaRef(AgentServerProblemSchema) };
10721
11657
  var AgentServerRouteSchemas = {
10722
11658
  health: {
@@ -10724,27 +11660,10 @@ var AgentServerRouteSchemas = {
10724
11660
  tags: ["system"],
10725
11661
  response: { 200: schemaRef(AgentServerHealthSchema) }
10726
11662
  },
10727
- startPairing: {
10728
- operationId: "startAgentServerPairing",
10729
- tags: ["pairing"],
10730
- response: {
10731
- 201: schemaRef(PairingStartedSchema),
10732
- ...problemResponse
10733
- }
10734
- },
10735
- claimPairing: {
10736
- operationId: "claimAgentServerPairing",
10737
- tags: ["pairing"],
10738
- params: PairingParamsSchema,
10739
- response: {
10740
- 200: schemaRef(PairingClaimedSchema),
10741
- ...problemResponse
10742
- }
10743
- },
10744
11663
  status: {
10745
11664
  operationId: "getAgentServerStatus",
10746
11665
  tags: ["system"],
10747
- security: pairedSecurity,
11666
+ security: localControlSecurity,
10748
11667
  response: {
10749
11668
  200: schemaRef(AgentServerStatusSchema),
10750
11669
  ...problemResponse
@@ -10753,7 +11672,7 @@ var AgentServerRouteSchemas = {
10753
11672
  listAgents: {
10754
11673
  operationId: "listAgentServerAgents",
10755
11674
  tags: ["agents"],
10756
- security: pairedSecurity,
11675
+ security: localControlSecurity,
10757
11676
  response: {
10758
11677
  200: Type.Array(schemaRef(AgentServerAgentSchema)),
10759
11678
  ...problemResponse
@@ -10762,17 +11681,44 @@ var AgentServerRouteSchemas = {
10762
11681
  createAgent: {
10763
11682
  operationId: "createAgentServerAgent",
10764
11683
  tags: ["agents"],
10765
- security: pairedSecurity,
11684
+ security: localControlSecurity,
10766
11685
  body: CreateAgentSchema,
10767
11686
  response: {
10768
11687
  201: schemaRef(AgentServerAgentSchema),
10769
11688
  ...problemResponse
10770
11689
  }
10771
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
+ },
10772
11718
  reconcileAgent: {
10773
11719
  operationId: "reconcileAgentServerAgent",
10774
11720
  tags: ["agents"],
10775
- security: pairedSecurity,
11721
+ security: localControlSecurity,
10776
11722
  params: AgentParamsSchema,
10777
11723
  body: ReconcileAgentSchema,
10778
11724
  response: {
@@ -10783,7 +11729,7 @@ var AgentServerRouteSchemas = {
10783
11729
  listProviders: {
10784
11730
  operationId: "listAgentServerProviders",
10785
11731
  tags: ["providers"],
10786
- security: pairedSecurity,
11732
+ security: localControlSecurity,
10787
11733
  response: {
10788
11734
  200: Type.Record(Type.String(), schemaRef(AgentServerProviderSchema)),
10789
11735
  ...problemResponse
@@ -10792,7 +11738,7 @@ var AgentServerRouteSchemas = {
10792
11738
  discoverModels: {
10793
11739
  operationId: "discoverAgentServerProviderModels",
10794
11740
  tags: ["providers"],
10795
- security: pairedSecurity,
11741
+ security: localControlSecurity,
10796
11742
  params: ProviderParamsSchema,
10797
11743
  response: {
10798
11744
  200: schemaRef(DiscoverModelsSchema),
@@ -10802,7 +11748,7 @@ var AgentServerRouteSchemas = {
10802
11748
  putProvider: {
10803
11749
  operationId: "putAgentServerProvider",
10804
11750
  tags: ["providers"],
10805
- security: pairedSecurity,
11751
+ security: localControlSecurity,
10806
11752
  params: ProviderParamsSchema,
10807
11753
  body: PutProviderSchema,
10808
11754
  response: {
@@ -10813,7 +11759,7 @@ var AgentServerRouteSchemas = {
10813
11759
  deleteProvider: {
10814
11760
  operationId: "deleteAgentServerProvider",
10815
11761
  tags: ["providers"],
10816
- security: pairedSecurity,
11762
+ security: localControlSecurity,
10817
11763
  params: ProviderParamsSchema,
10818
11764
  response: {
10819
11765
  204: Type.Any(),
@@ -10823,7 +11769,7 @@ var AgentServerRouteSchemas = {
10823
11769
  listSubscriptions: {
10824
11770
  operationId: "listAgentServerSubscriptions",
10825
11771
  tags: ["subscriptions"],
10826
- security: pairedSecurity,
11772
+ security: localControlSecurity,
10827
11773
  response: {
10828
11774
  200: Type.Array(schemaRef(AgentServerSubscriptionSchema)),
10829
11775
  ...problemResponse
@@ -10832,7 +11778,7 @@ var AgentServerRouteSchemas = {
10832
11778
  startSubscriptionLogin: {
10833
11779
  operationId: "startAgentServerSubscriptionLogin",
10834
11780
  tags: ["subscriptions"],
10835
- security: pairedSecurity,
11781
+ security: localControlSecurity,
10836
11782
  params: ProviderParamsSchema,
10837
11783
  response: {
10838
11784
  201: schemaRef(AgentServerSubscriptionLoginSchema),
@@ -10842,7 +11788,7 @@ var AgentServerRouteSchemas = {
10842
11788
  getSubscriptionLogin: {
10843
11789
  operationId: "getAgentServerSubscriptionLogin",
10844
11790
  tags: ["subscriptions"],
10845
- security: pairedSecurity,
11791
+ security: localControlSecurity,
10846
11792
  params: ProviderParamsSchema,
10847
11793
  response: {
10848
11794
  200: schemaRef(AgentServerSubscriptionLoginSchema),
@@ -10852,17 +11798,27 @@ var AgentServerRouteSchemas = {
10852
11798
  cancelSubscriptionLogin: {
10853
11799
  operationId: "cancelAgentServerSubscriptionLogin",
10854
11800
  tags: ["subscriptions"],
10855
- security: pairedSecurity,
11801
+ security: localControlSecurity,
10856
11802
  params: ProviderParamsSchema,
10857
11803
  response: {
10858
11804
  200: schemaRef(CancelledSubscriptionSchema),
10859
11805
  ...problemResponse
10860
11806
  }
10861
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
+ },
10862
11818
  listRuns: {
10863
11819
  operationId: "listAgentServerRuns",
10864
11820
  tags: ["runs"],
10865
- security: pairedSecurity,
11821
+ security: localControlSecurity,
10866
11822
  response: {
10867
11823
  200: Type.Array(schemaRef(AgentServerRunSchema)),
10868
11824
  ...problemResponse
@@ -10871,7 +11827,7 @@ var AgentServerRouteSchemas = {
10871
11827
  startRun: {
10872
11828
  operationId: "startAgentServerRun",
10873
11829
  tags: ["runs"],
10874
- security: pairedSecurity,
11830
+ security: localControlSecurity,
10875
11831
  body: StartRunSchema,
10876
11832
  response: {
10877
11833
  201: schemaRef(AgentServerRunSchema),
@@ -10881,7 +11837,7 @@ var AgentServerRouteSchemas = {
10881
11837
  stopRun: {
10882
11838
  operationId: "stopAgentServerRun",
10883
11839
  tags: ["runs"],
10884
- security: pairedSecurity,
11840
+ security: localControlSecurity,
10885
11841
  params: RunParamsSchema,
10886
11842
  response: {
10887
11843
  200: schemaRef(AgentServerRunRecordSchema),
@@ -10891,7 +11847,7 @@ var AgentServerRouteSchemas = {
10891
11847
  streamRunLogs: {
10892
11848
  operationId: "streamAgentServerRunLogs",
10893
11849
  tags: ["runs"],
10894
- security: pairedSecurity,
11850
+ security: localControlSecurity,
10895
11851
  params: RunParamsSchema,
10896
11852
  response: {
10897
11853
  200: schemaRef(LogStreamSchema),
@@ -10906,9 +11862,8 @@ var AgentServerRouteSchemas = {
10906
11862
  * loopback-companion security profile (#2066): loopback Host enforcement,
10907
11863
  * exact-origin CORS, Fetch-Metadata guards, strict JSON parsing.
10908
11864
  *
10909
- * Everything under `/v1` except the pairing bootstrap requires a paired
10910
- * origin: the `x-moltnet-agent-server-token` header must verify against the
10911
- * 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.
10912
11867
  */
10913
11868
  var AGENT_SERVER_TOKEN_HEADER = "x-moltnet-agent-server-token";
10914
11869
  var BODY_LIMIT = 64 * 1024;
@@ -11024,7 +11979,9 @@ function requestOperationSignal(request, shutdownSignal) {
11024
11979
  return shutdownSignal ? AbortSignal.any([disconnected.signal, shutdownSignal]) : disconnected.signal;
11025
11980
  }
11026
11981
  function buildAgentServer(options) {
11027
- const { pairing } = options;
11982
+ const { nativeGrant } = options;
11983
+ const oauth = options.operatorOAuth;
11984
+ let restartRequired = false;
11028
11985
  const fastifyOptions = {
11029
11986
  bodyLimit: BODY_LIMIT,
11030
11987
  ...options.tls ? { https: options.tls } : {}
@@ -11035,8 +11992,9 @@ function buildAgentServer(options) {
11035
11992
  }) : Fastify(fastifyOptions);
11036
11993
  options.registerOpenApi?.(app);
11037
11994
  for (const schema of AGENT_SERVER_SCHEMAS) app.addSchema(schema);
11995
+ const browserOrigins = new OriginAllowlist(options.allowedOrigins);
11038
11996
  registerLoopbackSecurity(app, {
11039
- allowedOrigins: options.allowedOrigins,
11997
+ isOriginAllowed: (origin) => origin === "moltnet-agent-desktop://native" || browserOrigins.has(origin),
11040
11998
  ...options.selfOrigin ? { selfOrigins: [options.selfOrigin] } : {},
11041
11999
  allowedHeaders: [AGENT_SERVER_TOKEN_HEADER],
11042
12000
  methods: [
@@ -11047,40 +12005,185 @@ function buildAgentServer(options) {
11047
12005
  "OPTIONS"
11048
12006
  ]
11049
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
+ }
11050
12024
  app.register(rateLimit, {
11051
- global: false,
12025
+ global: true,
11052
12026
  max: options.rateLimitMax ?? RATE_LIMIT_MAX,
11053
12027
  timeWindow: RATE_LIMIT_WINDOW_MS,
11054
12028
  errorResponseBuilder: () => new AgentServerHttpError(429, "rate_limited", "Too many requests"),
11055
- keyGenerator: (request) => {
12029
+ keyGenerator: async (request) => {
11056
12030
  const origin = request.headers.origin;
11057
- return isConfiguredOrigin(origin, options) ? `origin:${origin}` : `ip:${request.ip}`;
11058
- }
11059
- });
11060
- app.addContentTypeParser("application/x-www-form-urlencoded", { parseAs: "buffer" }, (_request, body, done) => {
11061
- try {
11062
- const form = new TextDecoder("utf-8", { fatal: true }).decode(typeof body === "string" ? Buffer.from(body) : body);
11063
- done(null, new URLSearchParams(form));
11064
- } catch {
11065
- 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}`;
11066
12046
  }
11067
12047
  });
11068
- 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");
11069
12050
  const origin = requireOriginHeader(request.headers);
11070
12051
  const token = request.headers[AGENT_SERVER_TOKEN_HEADER];
11071
- if (typeof token !== "string" || token.length === 0) throw new AgentServerHttpError(401, "pairing_required", "Pairing token is required");
11072
- 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
+ }
11073
12079
  return origin;
11074
12080
  };
11075
12081
  app.after(() => {
11076
- app.addHook("onRequest", app.rateLimit());
11077
12082
  app.get("/health", { schema: AgentServerRouteSchemas.health }, async () => ({ status: "ok" }));
11078
- registerPairingRoutes(app, pairing);
11079
- registerStatusRoute(app, options, requirePairedOrigin);
11080
- registerAgentRoutes(app, options, requirePairedOrigin);
11081
- registerProviderRoutes(app, options, requirePairedOrigin);
11082
- registerSubscriptionRoutes(app, options, requirePairedOrigin);
11083
- 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();
11084
12187
  });
11085
12188
  app.addHook("onClose", () => {
11086
12189
  options.subscriptions.close();
@@ -11104,41 +12207,65 @@ function buildAgentServer(options) {
11104
12207
  });
11105
12208
  return app;
11106
12209
  }
11107
- function registerPairingRoutes(app, pairing) {
11108
- app.post("/v1/pairings", { schema: AgentServerRouteSchemas.startPairing }, async (request, reply) => {
11109
- const origin = requireOriginHeader(request.headers);
11110
- return reply.code(201).send(pairing.start(origin));
11111
- });
11112
- app.get("/pairings/:pairingId", { schema: { hide: true } }, async (request, reply) => {
11113
- assertNavigationRequest(request.headers);
11114
- const { pairingId } = request.params;
11115
- const approval = pairing.approval(pairingId);
11116
- return reply.type("text/html; charset=utf-8").send(renderPairingApprovalPage({
11117
- pairingId,
11118
- origin: approval.origin,
11119
- confirmToken: approval.confirmToken
11120
- }));
11121
- });
11122
- app.post("/pairings/:pairingId/confirm", { schema: { hide: true } }, async (request, reply) => {
11123
- rejectExplicitCrossSite(request.headers);
11124
- const { pairingId } = request.params;
11125
- if (!(request.body instanceof URLSearchParams)) throw new AgentServerHttpError(400, "invalid_body", "Confirmation form is invalid");
11126
- const { origin } = pairing.confirm(pairingId, request.body.get("confirmToken") ?? "");
11127
- return reply.type("text/html; charset=utf-8").send(renderPairingResultPage({
11128
- title: "Connection approved",
11129
- message: `${origin} can now manage local MoltNet agents on this machine.`
11130
- }));
11131
- });
11132
- app.post("/v1/pairings/:pairingId/claim", { schema: AgentServerRouteSchemas.claimPairing }, async (request) => {
11133
- const origin = requireOriginHeader(request.headers);
11134
- const { pairingId } = request.params;
11135
- 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
+ });
11136
12225
  });
11137
12226
  }
11138
- 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) {
11139
12266
  const { store, runs } = options;
11140
12267
  app.get("/v1/status", { schema: AgentServerRouteSchemas.status }, async (request) => {
11141
- requirePairedOrigin(request);
12268
+ await requireAuthorizedOrigin(request);
11142
12269
  const selected = selectedIdentity(store, options.activeIdentity);
11143
12270
  return {
11144
12271
  version: options.version,
@@ -11148,22 +12275,22 @@ function registerStatusRoute(app, options, requirePairedOrigin) {
11148
12275
  identities: identityViews(store),
11149
12276
  ...selected ? { selectedIdentity: selected } : {},
11150
12277
  providers: options.providers.list(),
11151
- runs: runViews(runs),
12278
+ runs: await runViews(runs),
11152
12279
  runtimeSettings: options.runtimeSettings ?? DEFAULT_LOCAL_OPERATIONAL_SETTINGS
11153
12280
  };
11154
12281
  });
11155
12282
  }
11156
- function registerAgentRoutes(app, options, requirePairedOrigin) {
12283
+ function registerAgentRoutes(app, options, requireAuthorizedOrigin) {
11157
12284
  const { store } = options;
11158
12285
  app.get("/v1/agents", { schema: AgentServerRouteSchemas.listAgents }, async (request) => {
11159
- requirePairedOrigin(request);
12286
+ await requireAuthorizedOrigin(request);
11160
12287
  return store.listActivations().map((activation) => publicAgentView(store, activation));
11161
12288
  });
11162
12289
  app.post("/v1/agents", {
11163
12290
  schema: AgentServerRouteSchemas.createAgent,
11164
12291
  attachValidation: true
11165
12292
  }, async (request, reply) => {
11166
- requirePairedOrigin(request);
12293
+ await requireAuthorizedOrigin(request);
11167
12294
  const body = requireBody(request);
11168
12295
  const signal = requestOperationSignal(request, options.shutdownSignal);
11169
12296
  const kind = requireString(body, "kind");
@@ -11189,11 +12316,26 @@ function registerAgentRoutes(app, options, requirePairedOrigin) {
11189
12316
  }
11190
12317
  throw new AgentServerHttpError(400, "invalid_body", "\"kind\" must be \"managed\" or \"external\"");
11191
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
+ });
11192
12334
  app.post("/v1/agents/:agentName/reconcile", {
11193
12335
  schema: AgentServerRouteSchemas.reconcileAgent,
11194
12336
  attachValidation: true
11195
12337
  }, async (request) => {
11196
- requirePairedOrigin(request);
12338
+ await requireAuthorizedOrigin(request);
11197
12339
  const { agentName } = request.params;
11198
12340
  const action = requireString(requireBody(request), "action");
11199
12341
  if (action !== "resume" && action !== "abandon") throw new AgentServerHttpError(400, "invalid_body", "\"action\" must be \"resume\" or \"abandon\"");
@@ -11217,13 +12359,13 @@ function identityViews(store) {
11217
12359
  hasAgentKey: hasAgentKeyConfiguration(store.readAgentConfig(alias) ?? {})
11218
12360
  }));
11219
12361
  }
11220
- function registerProviderRoutes(app, options, requirePairedOrigin) {
12362
+ function registerProviderRoutes(app, options, requireAuthorizedOrigin) {
11221
12363
  app.get("/v1/providers", { schema: AgentServerRouteSchemas.listProviders }, async (request) => {
11222
- requirePairedOrigin(request);
12364
+ await requireAuthorizedOrigin(request);
11223
12365
  return options.providers.list();
11224
12366
  });
11225
12367
  app.post("/v1/providers/:providerId/discover-models", { schema: AgentServerRouteSchemas.discoverModels }, async (request) => {
11226
- requirePairedOrigin(request);
12368
+ await requireAuthorizedOrigin(request);
11227
12369
  const { providerId } = request.params;
11228
12370
  return options.providers.discover(providerId, { signal: requestOperationSignal(request, options.shutdownSignal) });
11229
12371
  });
@@ -11231,7 +12373,7 @@ function registerProviderRoutes(app, options, requirePairedOrigin) {
11231
12373
  schema: AgentServerRouteSchemas.putProvider,
11232
12374
  attachValidation: true
11233
12375
  }, async (request, reply) => {
11234
- requirePairedOrigin(request);
12376
+ await requireAuthorizedOrigin(request);
11235
12377
  const { providerId } = request.params;
11236
12378
  const body = requireBody(request);
11237
12379
  const entry = await options.providers.set(providerId, {
@@ -11247,7 +12389,7 @@ function registerProviderRoutes(app, options, requirePairedOrigin) {
11247
12389
  schema: AgentServerRouteSchemas.deleteProvider,
11248
12390
  attachValidation: true
11249
12391
  }, async (request, reply) => {
11250
- requirePairedOrigin(request);
12392
+ await requireAuthorizedOrigin(request);
11251
12393
  const { providerId } = request.params;
11252
12394
  try {
11253
12395
  await options.providers.remove(providerId);
@@ -11258,45 +12400,45 @@ function registerProviderRoutes(app, options, requirePairedOrigin) {
11258
12400
  return reply.code(204).send(null);
11259
12401
  });
11260
12402
  }
11261
- function runViews(runs) {
11262
- return runs.list(RUN_HISTORY_LIMIT).map((record) => ({
12403
+ async function runViews(runs) {
12404
+ return (await runs.listAsync(RUN_HISTORY_LIMIT)).map((record) => ({
11263
12405
  ...record,
11264
12406
  active: runs.isActive(record.id)
11265
12407
  }));
11266
12408
  }
11267
- function registerSubscriptionRoutes(app, options, requirePairedOrigin) {
12409
+ function registerSubscriptionRoutes(app, options, requireAuthorizedOrigin) {
11268
12410
  app.get("/v1/subscriptions", { schema: AgentServerRouteSchemas.listSubscriptions }, async (request) => {
11269
- requirePairedOrigin(request);
12411
+ await requireAuthorizedOrigin(request);
11270
12412
  return options.subscriptions.list();
11271
12413
  });
11272
12414
  app.post("/v1/subscriptions/:providerId/login", { schema: AgentServerRouteSchemas.startSubscriptionLogin }, async (request, reply) => {
11273
- requirePairedOrigin(request);
12415
+ await requireAuthorizedOrigin(request);
11274
12416
  const { providerId } = request.params;
11275
12417
  const login = await options.subscriptions.start(providerId);
11276
12418
  return reply.code(201).send(login);
11277
12419
  });
11278
12420
  app.get("/v1/subscriptions/:providerId/login", { schema: AgentServerRouteSchemas.getSubscriptionLogin }, async (request) => {
11279
- requirePairedOrigin(request);
12421
+ await requireAuthorizedOrigin(request);
11280
12422
  const { providerId } = request.params;
11281
12423
  return options.subscriptions.status(providerId);
11282
12424
  });
11283
12425
  app.delete("/v1/subscriptions/:providerId/login", { schema: AgentServerRouteSchemas.cancelSubscriptionLogin }, async (request) => {
11284
- requirePairedOrigin(request);
12426
+ await requireAuthorizedOrigin(request);
11285
12427
  const { providerId } = request.params;
11286
12428
  return options.subscriptions.cancel(providerId);
11287
12429
  });
11288
12430
  }
11289
- function registerRunRoutes(app, options, requirePairedOrigin) {
12431
+ function registerRunRoutes(app, options, requireAuthorizedOrigin) {
11290
12432
  const { runs } = options;
11291
12433
  app.get("/v1/runs", { schema: AgentServerRouteSchemas.listRuns }, async (request) => {
11292
- requirePairedOrigin(request);
12434
+ await requireAuthorizedOrigin(request);
11293
12435
  return runViews(runs);
11294
12436
  });
11295
12437
  app.post("/v1/runs", {
11296
12438
  schema: AgentServerRouteSchemas.startRun,
11297
12439
  attachValidation: true
11298
12440
  }, async (request, reply) => {
11299
- requirePairedOrigin(request);
12441
+ await requireAuthorizedOrigin(request);
11300
12442
  const body = requireBody(request);
11301
12443
  const diaryId = optionalString(body, "diaryId");
11302
12444
  const record = await runs.start({
@@ -11313,17 +12455,57 @@ function registerRunRoutes(app, options, requirePairedOrigin) {
11313
12455
  });
11314
12456
  });
11315
12457
  app.delete("/v1/runs/:runId", { schema: AgentServerRouteSchemas.stopRun }, async (request) => {
11316
- requirePairedOrigin(request);
12458
+ await requireAuthorizedOrigin(request);
11317
12459
  const { runId } = request.params;
11318
12460
  return runs.stop(runId);
11319
12461
  });
11320
- registerRunLogRoute(app, options, requirePairedOrigin);
12462
+ registerRunLogRoute(app, options, requireAuthorizedOrigin);
11321
12463
  }
11322
- function registerRunLogRoute(app, options, requirePairedOrigin) {
12464
+ function registerRunLogRoute(app, options, requireAuthorizedOrigin) {
11323
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
+ });
11324
12506
  let openStreams = 0;
11325
12507
  app.get("/v1/runs/:runId/logs", { schema: AgentServerRouteSchemas.streamRunLogs }, async (request, reply) => {
11326
- requirePairedOrigin(request);
12508
+ await requireAuthorizedOrigin(request);
11327
12509
  const { runId } = request.params;
11328
12510
  const record = runs.status(runId);
11329
12511
  store.resolveRunLogPath(record.id);
@@ -11366,6 +12548,7 @@ function registerRunLogRoute(app, options, requirePairedOrigin) {
11366
12548
  });
11367
12549
  };
11368
12550
  const push = async () => {
12551
+ if (request.headers.origin !== "moltnet-agent-desktop://native") await requireAuthorizedOrigin(request);
11369
12552
  const handle = await open(store.resolveRunLogPath(record.id), constants.O_RDONLY | constants.O_NOFOLLOW);
11370
12553
  try {
11371
12554
  const { lines, omitted } = await readAgentServerLogDelta(handle, readState);
@@ -11414,9 +12597,14 @@ function corsHeadersFor(request, options) {
11414
12597
  return {};
11415
12598
  }
11416
12599
  function isConfiguredOrigin(origin, options) {
11417
- 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);
11418
12601
  }
11419
12602
  function normalizeAgentServerError(error) {
12603
+ if (error instanceof TeamCredentialError) return {
12604
+ statusCode: 400,
12605
+ code: error.blocker.code,
12606
+ message: error.blocker.message
12607
+ };
11420
12608
  if (error instanceof AgentServerHttpError) return {
11421
12609
  statusCode: error.statusCode,
11422
12610
  code: error.code,
@@ -11427,8 +12615,8 @@ function normalizeAgentServerError(error) {
11427
12615
  code: error.kind,
11428
12616
  message: error.message
11429
12617
  };
11430
- if (error instanceof AgentServerPairingError) return {
11431
- 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,
11432
12620
  code: error.code,
11433
12621
  message: error.message
11434
12622
  };
@@ -11487,28 +12675,6 @@ function pemPrivateKey(key) {
11487
12675
  type: "pkcs8"
11488
12676
  }).toString());
11489
12677
  }
11490
- async function importCaKeyPair(pem) {
11491
- const privateKey = createPrivateKey(pem);
11492
- const privateDer = privateKey.export({
11493
- format: "der",
11494
- type: "pkcs8"
11495
- });
11496
- const publicDer = createPublicKey(privateKey).export({
11497
- format: "der",
11498
- type: "spki"
11499
- });
11500
- const [privateCryptoKey, publicCryptoKey] = await Promise.all([webcrypto.subtle.importKey("pkcs8", privateDer, {
11501
- name: "ECDSA",
11502
- namedCurve: "P-256"
11503
- }, false, ["sign"]), webcrypto.subtle.importKey("spki", publicDer, {
11504
- name: "ECDSA",
11505
- namedCurve: "P-256"
11506
- }, false, ["verify"])]);
11507
- return {
11508
- privateKey: privateCryptoKey,
11509
- publicKey: publicCryptoKey
11510
- };
11511
- }
11512
12678
  async function localTlsMaterialFromDirectory(dir) {
11513
12679
  try {
11514
12680
  const [key, cert, ca] = await Promise.all([
@@ -11517,7 +12683,8 @@ async function localTlsMaterialFromDirectory(dir) {
11517
12683
  readFile(join(dir, "local-ca.pem"), "utf8")
11518
12684
  ]);
11519
12685
  const parsed = new X509Certificate(cert);
11520
- 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 {
11521
12688
  key,
11522
12689
  cert,
11523
12690
  ca,
@@ -11526,6 +12693,17 @@ async function localTlsMaterialFromDirectory(dir) {
11526
12693
  } catch {}
11527
12694
  return null;
11528
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
+ }
11529
12707
  /** Creates a per-user CA and loopback-only leaf certificate under a 0700 directory. */
11530
12708
  async function ensureLocalTlsMaterial(root) {
11531
12709
  const dir = join(root, "tls");
@@ -11533,27 +12711,25 @@ async function ensureLocalTlsMaterial(root) {
11533
12711
  recursive: true,
11534
12712
  mode: 448
11535
12713
  });
12714
+ const previousSigningKey = join(dir, "local-ca-key.pem");
12715
+ const hasPreviousSigningKey = await hasStoredSigningKey(dir);
11536
12716
  const existing = await localTlsMaterialFromDirectory(dir);
11537
- if (existing) return existing;
11538
- let ca;
11539
- let caKeys;
11540
- let caKey;
11541
- try {
11542
- ca = await readFile(join(dir, "local-ca.pem"), "utf8");
11543
- caKeys = await importCaKeyPair(await readFile(join(dir, "local-ca-key.pem"), "utf8"));
11544
- } catch {
11545
- caKeys = await webcrypto.subtle.generateKey({
11546
- name: "ECDSA",
11547
- namedCurve: "P-256"
11548
- }, true, ["sign", "verify"]);
11549
- ca = (await X509CertificateGenerator.createSelfSigned({
11550
- name: `CN=${CA_COMMON_NAME}`,
11551
- keys: caKeys,
11552
- notAfter: new Date(Date.now() + 10 * 365 * 24 * 60 * 60 * 1e3),
11553
- extensions: [new BasicConstraintsExtension(true, void 0, true), new KeyUsagesExtension(KeyUsageFlags.keyCertSign | KeyUsageFlags.cRLSign, true)]
11554
- })).toString("pem");
11555
- caKey = await pemPrivateKey(caKeys.privateKey);
11556
- }
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");
11557
12733
  const leafKeys = await webcrypto.subtle.generateKey({
11558
12734
  name: "ECDSA",
11559
12735
  namedCurve: "P-256"
@@ -11577,8 +12753,9 @@ async function ensureLocalTlsMaterial(root) {
11577
12753
  const key = await pemPrivateKey(leafKeys.privateKey);
11578
12754
  const cert = leafCert.toString("pem");
11579
12755
  const writes = [writeFile(join(dir, "loopback-key.pem"), key, { mode: 384 }), writeFile(join(dir, "loopback-cert.pem"), cert, { mode: 384 })];
11580
- 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 }));
11581
12757
  await Promise.all(writes);
12758
+ await rm(previousSigningKey, { force: true });
11582
12759
  return {
11583
12760
  key,
11584
12761
  cert,
@@ -11588,20 +12765,23 @@ async function ensureLocalTlsMaterial(root) {
11588
12765
  }
11589
12766
  async function trustLocalCa(root) {
11590
12767
  const caPath = join(root, "tls", "local-ca.pem");
11591
- await execFileAsync("security", [
12768
+ await execFileAsync("/usr/bin/security", [
11592
12769
  "add-trusted-cert",
11593
- "-d",
11594
12770
  "-r",
11595
12771
  "trustRoot",
12772
+ "-p",
12773
+ "ssl",
12774
+ "-s",
12775
+ "127.0.0.1",
11596
12776
  "-k",
11597
12777
  loginKeychainPath(),
11598
12778
  caPath
11599
12779
  ]);
11600
12780
  }
11601
12781
  async function isLocalCaTrusted(root) {
11602
- const ca = await readFile(join(root, "tls", "local-ca.pem"), "utf8");
11603
12782
  try {
11604
- 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", [
11605
12785
  "find-certificate",
11606
12786
  "-a",
11607
12787
  "-p",
@@ -11610,32 +12790,51 @@ async function isLocalCaTrusted(root) {
11610
12790
  loginKeychainPath()
11611
12791
  ]);
11612
12792
  return stdout.includes(ca.trim());
11613
- } catch {
11614
- 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;
11615
12798
  }
11616
12799
  }
11617
12800
  async function removeLocalCa(root) {
11618
- 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", [
11619
12810
  "delete-certificate",
11620
12811
  "-Z",
11621
- new X509Certificate(await readFile(join(root, "tls", "local-ca.pem"), "utf8")).fingerprint256.replaceAll(":", ""),
12812
+ fingerprint,
11622
12813
  loginKeychainPath()
11623
12814
  ]);
11624
12815
  }
11625
12816
  function isMacos() {
11626
12817
  return process.platform === "darwin";
11627
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
+ }
11628
12828
  //#endregion
11629
12829
  //#region src/cli/server.ts
11630
12830
  /**
11631
12831
  * `moltnet-agent server` — per-user loopback supervisor (#2061).
11632
12832
  *
11633
- * 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
11634
12834
  * Console origin to configure agents/providers and start/stop runs.
11635
12835
  */
11636
- var DEFAULT_PORT = 17374;
12836
+ var DEFAULT_PORT = OPERATOR_OAUTH.serverPort;
11637
12837
  var DEFAULT_ALLOWED_ORIGINS = "https://console.themolt.net";
11638
- var DEFAULT_API_URL = "https://api.themolt.net";
11639
12838
  var SHUTDOWN_TIMEOUT_MS = 15e3;
11640
12839
  async function runAgentServer(argv) {
11641
12840
  if (isHelpFlag(argv)) {
@@ -11664,8 +12863,14 @@ async function runAgentServer(argv) {
11664
12863
  return 1;
11665
12864
  }
11666
12865
  const allowedOrigins = parseAllowedOrigins(values["allowed-origins"] ?? (envConfig.allowedOrigins || DEFAULT_ALLOWED_ORIGINS));
11667
- const root = values.root ?? resolveAgentServerRoot({ root: envConfig.root });
11668
- 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;
11669
12874
  const runtimeSettings = parseLocalOperationalSettings(values);
11670
12875
  const store = new AgentServerStore(root).ensure();
11671
12876
  const { logger, shutdown: shutdownLogger } = createRootLogger({
@@ -11681,7 +12886,15 @@ async function runAgentServer(argv) {
11681
12886
  });
11682
12887
  const secretProviders = createNodeSecretProviderRegistry().register(secrets);
11683
12888
  const externalSecretProviders = createNodeSecretProviderRegistry();
11684
- 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
+ }
11685
12898
  const shutdownController = new AbortController();
11686
12899
  const subscriptions = await ProviderLoginService.create({
11687
12900
  authPath: store.piAuthJsonPath,
@@ -11693,27 +12906,41 @@ async function runAgentServer(argv) {
11693
12906
  secretProviders,
11694
12907
  logger
11695
12908
  });
12909
+ const runtimeRegistry = new RuntimeRegistry(store.root);
11696
12910
  const runs = new RunManager({
11697
12911
  store,
11698
12912
  secretProviders,
11699
12913
  externalSecretProviders,
11700
12914
  baseEnv: processEnvSnapshot(),
11701
12915
  logger,
11702
- runtimeRegistry: new RuntimeRegistry(store.root),
12916
+ runtimeRegistry,
11703
12917
  runtimeSettings
11704
12918
  });
11705
- 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}`;
11706
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,
11707
12933
  store,
11708
12934
  secrets,
11709
12935
  secretProviders,
11710
12936
  externalSecretProviders,
11711
- pairing,
12937
+ nativeGrant,
11712
12938
  runs,
11713
12939
  subscriptions,
11714
12940
  providers,
12941
+ runtimeRegistry,
11715
12942
  allowedOrigins,
11716
- selfOrigin: `${tls ? "https" : "http"}://127.0.0.1:${port}`,
12943
+ selfOrigin,
11717
12944
  ...tls ? { tls: {
11718
12945
  key: tls.key,
11719
12946
  cert: tls.cert
@@ -11733,7 +12960,8 @@ async function runAgentServer(argv) {
11733
12960
  console.error(`moltnet-agent server listening on ${address}`);
11734
12961
  console.error(`config root: ${root}`);
11735
12962
  console.error(`allowed origins: ${allowedOrigins.join(", ")}`);
11736
- 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.");
11737
12965
  return await waitForAgentServerShutdown(runs, app, shutdownController, Boolean(values.supervised));
11738
12966
  } catch (cause) {
11739
12967
  await app.close().catch(() => void 0);
@@ -11788,15 +13016,15 @@ async function runTrustCommand(argv, defaultRoot) {
11788
13016
  console.error("Local HTTPS trust setup is currently supported on macOS only.");
11789
13017
  return 1;
11790
13018
  }
11791
- const material = await ensureLocalTlsMaterial(root);
11792
13019
  if (statusRequested) {
11793
- const trusted = await isLocalCaTrusted(root);
13020
+ const material = await inspectLocalTlsMaterial(root);
13021
+ const trusted = material !== null && await isLocalCaTrusted(root);
11794
13022
  if (json) printTrustStatus({
11795
13023
  supported: true,
11796
13024
  trusted,
11797
- fingerprint: material.fingerprint
13025
+ fingerprint: material?.fingerprint ?? null
11798
13026
  });
11799
- 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.`);
11800
13028
  return 0;
11801
13029
  }
11802
13030
  if (json && !yes) {
@@ -11808,11 +13036,12 @@ async function runTrustCommand(argv, defaultRoot) {
11808
13036
  if (json) printTrustStatus({
11809
13037
  supported: true,
11810
13038
  trusted: false,
11811
- fingerprint: material.fingerprint
13039
+ fingerprint: null
11812
13040
  });
11813
13041
  else console.log("Removed the MoltNet local CA from your login keychain.");
11814
13042
  return 0;
11815
13043
  }
13044
+ const material = await ensureLocalTlsMaterial(root);
11816
13045
  if (yes) await trustLocalCa(root);
11817
13046
  else await ensureTrustedLocalTls(root);
11818
13047
  if (json) printTrustStatus({
@@ -12021,6 +13250,7 @@ async function runSyncSessions(argv) {
12021
13250
  args: argv,
12022
13251
  options: {
12023
13252
  ...identityOptionDefs(),
13253
+ ...projectRunOptionDefs(),
12024
13254
  team: { type: "string" },
12025
13255
  "runtime-profile-id": { type: "string" },
12026
13256
  state: { type: "string" },
@@ -12028,11 +13258,6 @@ async function runSyncSessions(argv) {
12028
13258
  "dry-run": { type: "boolean" }
12029
13259
  }
12030
13260
  });
12031
- if (!values.team) {
12032
- console.error("Missing required flag: --team\n");
12033
- console.error(SYNC_SESSIONS_HELP);
12034
- return 1;
12035
- }
12036
13261
  let identity;
12037
13262
  try {
12038
13263
  identity = parseIdentityProcessOptions(values);
@@ -12049,18 +13274,38 @@ async function runSyncSessions(argv) {
12049
13274
  const agentRootDir = resolve(process.cwd(), values["agent-root"] ?? process.cwd());
12050
13275
  const explicitAgentRootDir = values["agent-root"] ? resolve(process.cwd(), values["agent-root"]) : void 0;
12051
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
+ }
12052
13296
  const ctx = await resolveAgentContext(identity.agent, {
12053
13297
  credentialSource: cfg.credentialSource,
12054
13298
  envApiUrl: cfg.apiUrl,
12055
- teamId: values.team,
13299
+ projectApiUrl: selection.binding?.apiUrl,
13300
+ teamId: selection.teamId,
12056
13301
  agentRootDir: explicitAgentRootDir
12057
13302
  });
12058
13303
  await validateStartupBinding({
12059
13304
  agent: ctx.agent,
12060
- teamId: values.team,
13305
+ teamId: selection.teamId,
12061
13306
  credentialTeamId: ctx.credentialTeamId
12062
13307
  });
12063
- const stateDirs = ensureDaemonStateDirs(agentRootDir);
13308
+ const stateDirs = ensureDaemonStateDirs(selection.stateRootDir ?? (selection.binding || values.source ? selection.source ?? agentRootDir : agentRootDir));
12064
13309
  const result = await syncRuntimeSessions({
12065
13310
  runtimeSessionStore: createApiRuntimeSessionStore({ agent: ctx.agent }),
12066
13311
  runtimeSlotStore: createApiRuntimeSlotStore({ agent: ctx.agent }),
@@ -12072,7 +13317,7 @@ async function runSyncSessions(argv) {
12072
13317
  runtimeProfileId: values["runtime-profile-id"],
12073
13318
  sessionRootDir: stateDirs.piSessionsDir,
12074
13319
  state,
12075
- teamId: values.team
13320
+ teamId: selection.teamId
12076
13321
  });
12077
13322
  console.log(JSON.stringify(result, null, 2));
12078
13323
  return result.failedUpload > 0 || result.unsafeSessionPath > 0 ? 1 : 0;
@@ -12243,7 +13488,7 @@ async function writeCache(cache) {
12243
13488
  }
12244
13489
  //#endregion
12245
13490
  //#region src/version.ts
12246
- var DAEMON_VERSION = "0.60.0";
13491
+ var DAEMON_VERSION = "0.61.0";
12247
13492
  //#endregion
12248
13493
  //#region src/cli.ts
12249
13494
  async function runAgentDaemonCli(options) {