@themoltnet/agent-daemon 0.60.0 → 0.62.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 +81 -9
  2. package/dist/cli.js +2013 -959
  3. package/dist/pi.js +16 -1
  4. package/package.json +9 -10
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,15 +8,15 @@ 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
- import { X509Certificate, createHash, createPrivateKey, createPublicKey, randomBytes, randomUUID, timingSafeEqual, webcrypto } from "node:crypto";
19
+ import { createHash, randomBytes, randomUUID, timingSafeEqual } from "node:crypto";
20
20
  import { once } from "node:events";
21
21
  import { metrics } from "@opentelemetry/api";
22
22
  import { OTLPMetricExporter } from "@opentelemetry/exporter-metrics-otlp-proto";
@@ -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 { chmod, lstat, mkdir, open, readFile, readdir, realpath, 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,13 +38,13 @@ 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";
44
46
  import rateLimit from "@fastify/rate-limit";
45
47
  import Fastify from "fastify";
46
- import "reflect-metadata";
47
- import { BasicConstraintsExtension, ExtendedKeyUsage, ExtendedKeyUsageExtension, IP, KeyUsageFlags, KeyUsagesExtension, SubjectAlternativeNameExtension, X509CertificateGenerator } from "@peculiar/x509";
48
48
  import { createGzip } from "node:zlib";
49
49
  //#region ../../libs/tasks/src/rubric.ts
50
50
  /**
@@ -857,6 +857,23 @@ Object.freeze([...[
857
857
  "offline_access"
858
858
  ], ...MCP_CLIENT_SCOPES]);
859
859
  //#endregion
860
+ //#region ../../libs/models/src/operator-oauth.ts
861
+ /** Public protocol constants shared by consent, native control, and Console. */
862
+ var OPERATOR_OAUTH = Object.freeze({
863
+ protocolVersion: 2,
864
+ provisioningScope: "moltnet:provision",
865
+ localControlScope: "moltnet:local-control",
866
+ provisioningAudience: "moltnet:provisioning",
867
+ localControlAudience: "moltnet:agent-server",
868
+ nativeClientId: "moltnet-native",
869
+ consoleClientId: "moltnet-console",
870
+ approvalTransportGraceSeconds: 30,
871
+ callbackPort: 17375,
872
+ consoleLifetimeSeconds: 900,
873
+ nativeLifetimeSeconds: 300,
874
+ serverPort: 17374
875
+ });
876
+ //#endregion
860
877
  //#region ../../libs/models/src/preview-sign.ts
861
878
  function schemaRef$2(schema, id) {
862
879
  return Type.Unsafe(Type.Ref(id));
@@ -1208,7 +1225,8 @@ Type.Object({
1208
1225
  });
1209
1226
  Type.Object({
1210
1227
  code: Type.String({ minLength: 1 }),
1211
- issueAgentKey: Type.Optional(Type.Literal(true))
1228
+ issueAgentKey: Type.Optional(Type.Literal(true)),
1229
+ expectedTeamId: Type.Optional(UuidSchema)
1212
1230
  });
1213
1231
  Type.Object({ role: Type.Union([
1214
1232
  Type.Literal("manager"),
@@ -1449,6 +1467,7 @@ var ProblemCodeSchema = Type.Union([
1449
1467
  Type.Literal("FORBIDDEN"),
1450
1468
  Type.Literal("NOT_FOUND"),
1451
1469
  Type.Literal("CONFLICT"),
1470
+ Type.Literal("PROJECT_MISMATCH"),
1452
1471
  Type.Literal("UNSUPPORTED_MEDIA_TYPE"),
1453
1472
  Type.Literal("VALIDATION_FAILED"),
1454
1473
  Type.Literal("INVALID_CHALLENGE"),
@@ -1529,6 +1548,34 @@ Type.Intersect([ConflictProblemDetailsSchema, Type.Object({ flagged: Type.Option
1529
1548
  threats: Type.Array(Type.Ref("InjectionThreat"))
1530
1549
  }, { additionalProperties: false }))) })], { $id: "InjectionConflictProblemDetails" });
1531
1550
  Type.Intersect([ProblemDetailsSchema, Type.Object({ errors: Type.Array(Type.Ref("ValidationError")) })], { $id: "ValidationProblemDetails" });
1551
+ Type.Object({
1552
+ id: Type.String({ format: "uuid" }),
1553
+ teamId: Type.String({ format: "uuid" }),
1554
+ creatorAgentId: Type.Union([Type.String({ format: "uuid" }), Type.Null()]),
1555
+ creatorHumanId: Type.Union([Type.String({ format: "uuid" }), Type.Null()]),
1556
+ name: Type.String(),
1557
+ description: Type.Union([Type.String(), Type.Null()]),
1558
+ defaultDiaryId: Type.Union([Type.String({ format: "uuid" }), Type.Null()]),
1559
+ archived: Type.Boolean(),
1560
+ createdAt: Type.String({ format: "date-time" }),
1561
+ updatedAt: Type.String({ format: "date-time" })
1562
+ });
1563
+ var CreateProjectSchema = Type.Object({
1564
+ name: Type.String({
1565
+ minLength: 1,
1566
+ maxLength: 255,
1567
+ pattern: "\\S"
1568
+ }),
1569
+ description: Type.Optional(Type.Union([Type.String({ maxLength: 1e4 }), Type.Null()])),
1570
+ defaultDiaryId: Type.Optional(Type.Union([Type.String({ format: "uuid" }), Type.Null()]))
1571
+ }, { additionalProperties: false });
1572
+ Type.Object({
1573
+ ...Type.Partial(CreateProjectSchema).properties,
1574
+ archived: Type.Optional(Type.Boolean())
1575
+ }, {
1576
+ additionalProperties: false,
1577
+ minProperties: 1
1578
+ });
1532
1579
  Type.Union([
1533
1580
  Type.Literal("pack"),
1534
1581
  Type.Literal("entry"),
@@ -1967,7 +2014,7 @@ var RuntimeProfileMaxBashTimeouts = Type.Integer({
1967
2014
  minimum: 0,
1968
2015
  maximum: 1e3
1969
2016
  });
1970
- Type.Object({
2017
+ var RuntimeProfile = Type.Object({
1971
2018
  id: Type.String({ format: "uuid" }),
1972
2019
  teamId: Type.String({ format: "uuid" }),
1973
2020
  name: RuntimeProfileName,
@@ -3353,6 +3400,7 @@ Type.Object({
3353
3400
  title: Type.Union([Type.String(), Type.Null()]),
3354
3401
  tags: Type.Array(Type.String()),
3355
3402
  teamId: Uuid,
3403
+ projectId: Type.Union([Uuid, Type.Null()]),
3356
3404
  diaryId: Type.Union([Uuid, Type.Null()]),
3357
3405
  outputKind: OutputKind,
3358
3406
  input: Type.Record(Type.String(), Type.Unknown()),
@@ -3461,257 +3509,6 @@ Type.Object({
3461
3509
  additionalProperties: false
3462
3510
  });
3463
3511
  //#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(", ");
3492
- }
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
3512
  //#region src/lib/identity-pin.ts
3716
3513
  /** Compare every pinned field without choosing a caller-specific error type. */
3717
3514
  function assessIdentityPin(current, expected) {
@@ -3748,7 +3545,7 @@ function matchesCredentialTeam(current, credentialTeamId) {
3748
3545
  * `docs/.vitepress/config.ts`.
3749
3546
  */
3750
3547
  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`. */
3548
+ /** Scopes a new daemon key should be minted with; mirrors `DAEMON_RECOMMENDED_SCOPES`. */
3752
3549
  var DAEMON_KEY_SCOPES = AGENT_CREDENTIAL_SCOPES;
3753
3550
  /**
3754
3551
  * Report where `connect()` will find the key, without ever reading the secret
@@ -3827,14 +3624,24 @@ async function validateStartupBinding(options) {
3827
3624
  async function resolveAgentContext(agentName, options = {}) {
3828
3625
  assertIdentityAlias(agentName);
3829
3626
  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
- };
3627
+ const projectApiUrl = options.projectApiUrl;
3628
+ if (options.credentialSource === "environment") {
3629
+ if (projectApiUrl) assertTrustedConfigApiUrl(projectApiUrl, options.envApiUrl?.trim() || "https://api.themolt.net");
3630
+ return {
3631
+ agentDir,
3632
+ agentRootDir,
3633
+ agent: await connect({
3634
+ ...projectApiUrl ? { apiUrl: projectApiUrl } : {},
3635
+ secretProviders: createNodeSecretProviderRegistry()
3636
+ }),
3637
+ credentialSource: "environment"
3638
+ };
3639
+ }
3836
3640
  const config = await readConfig(agentDir);
3837
3641
  if (!config || !hasAgentKeyConfiguration(config)) throw new Error(agentKeyRequiredMessage(agentDir, agentName));
3642
+ const configuredApiUrl = resolveConfigApiUrl(config, options.envApiUrl);
3643
+ if (options.envApiUrl?.trim()) requireSecureCredentialApiUrl(options.envApiUrl);
3644
+ if (projectApiUrl) assertTrustedConfigApiUrl(projectApiUrl, options.envApiUrl?.trim() || configuredApiUrl || "https://api.themolt.net");
3838
3645
  const secretProviders = createNodeSecretProviderRegistry();
3839
3646
  const agentKey = await resolveAgentKey(config, secretProviders, options.teamId);
3840
3647
  if (!agentKey) throw new Error(agentKeyRequiredMessage(agentDir, agentName));
@@ -3845,7 +3652,7 @@ async function resolveAgentContext(agentName, options = {}) {
3845
3652
  configDir: agentDir,
3846
3653
  secretProviders,
3847
3654
  agentKey,
3848
- apiUrl: resolveConfigApiUrl(config, options.envApiUrl)
3655
+ apiUrl: projectApiUrl ?? configuredApiUrl
3849
3656
  }),
3850
3657
  credentialSource: "config",
3851
3658
  credentialTeamId: selectAgentKeyReference(config, options.teamId)?.teamId
@@ -3934,6 +3741,13 @@ function isTransientWhoamiError(error) {
3934
3741
  const statusCode = error.statusCode;
3935
3742
  return typeof statusCode === "number" && (statusCode === 408 || statusCode === 429 || statusCode >= 500);
3936
3743
  }
3744
+ /** Read only non-secret endpoint metadata before selecting a team credential. */
3745
+ async function resolveSelectionApiUrl(agentName, options) {
3746
+ if (options.envApiUrl?.trim()) return options.envApiUrl.trim();
3747
+ if (options.credentialSource === "environment") throw new Error("Set MOLTNET_API_URL for an environment-key worker");
3748
+ const { agentDir } = resolveIdentityLocation(agentName, options.agentRootDir, { requireConfig: true });
3749
+ return resolveConfigApiUrl(await readConfig(agentDir) ?? {}) ?? "https://api.themolt.net";
3750
+ }
3937
3751
  //#endregion
3938
3752
  //#region src/config.ts
3939
3753
  /**
@@ -4002,7 +3816,19 @@ function activatePiCodingAgentDir(path, env = {}) {
4002
3816
  Object.assign(process.env, env);
4003
3817
  }
4004
3818
  function loadAgentServerEnvConfig() {
3819
+ const issuer = process.env["MOLTNET_OPERATOR_OAUTH_ISSUER"];
3820
+ const publicUrl = process.env["MOLTNET_OPERATOR_OAUTH_PUBLIC_URL"] ?? issuer;
3821
+ const nativeClientId = process.env["MOLTNET_NATIVE_OAUTH_CLIENT_ID"];
3822
+ const consoleClientId = process.env["MOLTNET_CONSOLE_OAUTH_CLIENT_ID"];
3823
+ const apiUrl = process.env["MOLTNET_OPERATOR_API_URL"];
4005
3824
  return {
3825
+ operatorOAuth: {
3826
+ ...issuer ? { issuer } : {},
3827
+ ...publicUrl ? { publicUrl } : {},
3828
+ ...nativeClientId ? { nativeClientId } : {},
3829
+ ...consoleClientId ? { consoleClientId } : {},
3830
+ ...apiUrl ? { apiUrl } : {}
3831
+ },
4006
3832
  port: process.env["MOLTNET_AGENT_SERVER_PORT"] ?? "",
4007
3833
  allowedOrigins: process.env["MOLTNET_AGENT_SERVER_ALLOWED_ORIGINS"] ?? "",
4008
3834
  root: process.env["MOLTNET_AGENT_SERVER_ROOT"] ?? "",
@@ -4022,6 +3848,376 @@ function loadUpdateEnvConfig() {
4022
3848
  };
4023
3849
  }
4024
3850
  //#endregion
3851
+ //#region src/lib/run-project-selection.ts
3852
+ var execFileAsync$1 = promisify(execFile);
3853
+ var GIT_TIMEOUT_MS = 1e4;
3854
+ var GIT_MAX_OUTPUT_BYTES = 64 * 1024;
3855
+ async function validateGitSource(source) {
3856
+ const inherited = processEnvSnapshot();
3857
+ try {
3858
+ const { stdout } = await execFileAsync$1("git", [
3859
+ "rev-parse",
3860
+ "--show-toplevel",
3861
+ "--verify",
3862
+ "HEAD^{commit}"
3863
+ ], {
3864
+ cwd: source,
3865
+ env: {
3866
+ PATH: inherited.PATH,
3867
+ HOME: inherited.HOME,
3868
+ GIT_CONFIG_NOSYSTEM: "1",
3869
+ GIT_CONFIG_GLOBAL: "/dev/null"
3870
+ },
3871
+ timeout: GIT_TIMEOUT_MS,
3872
+ killSignal: "SIGKILL",
3873
+ maxBuffer: GIT_MAX_OUTPUT_BYTES
3874
+ });
3875
+ const top = stdout.trim().split("\n")[0];
3876
+ if (await canonicalDirectory(top) === source) return;
3877
+ } catch (cause) {
3878
+ 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 });
3879
+ }
3880
+ throw new ProjectConfigError("selection", `git-worktree source ${source} must be the Git repository root, not a subdirectory`);
3881
+ }
3882
+ function projectRunOptionDefs() {
3883
+ return {
3884
+ project: { type: "string" },
3885
+ binding: { type: "string" },
3886
+ general: { type: "boolean" },
3887
+ "config-file": { type: "string" },
3888
+ "state-dir": { type: "string" },
3889
+ source: { type: "string" },
3890
+ "workspace-strategy": { type: "string" }
3891
+ };
3892
+ }
3893
+ function strategy(value) {
3894
+ if (value === void 0) return void 0;
3895
+ if (WORKSPACE_STRATEGIES.includes(value)) return value;
3896
+ throw new ProjectConfigError("validation", `Unknown workspace strategy ${value}; choose ${WORKSPACE_STRATEGIES.join(", ")}`);
3897
+ }
3898
+ /** Resolve once at worker startup. No credentials, remote calls, hooks or workspace creation. */
3899
+ async function resolveRunProjectSelection(args) {
3900
+ const env = processEnvSnapshot();
3901
+ const inherited = env.MOLTNET_ACTIVE_IDENTITY === args.agent && !args.general;
3902
+ const bindingName = args.binding ?? (!args["config-file"] && !args.project && inherited ? env.MOLTNET_PROJECT_BINDING : void 0);
3903
+ const projectId = args.project ?? (!args["config-file"] && !args.binding && inherited ? env.MOLTNET_PROJECT_ID : void 0);
3904
+ const configPath = resolve(args.cwd, args["config-file"] ?? (inherited ? env.MOLTNET_PROJECT_CONFIG : void 0) ?? getProjectConfigPath());
3905
+ if (args.general && (args.project || args.binding)) throw new ProjectConfigError("selection", "General work cannot also declare a project or binding");
3906
+ const overrideStrategy = strategy(args["workspace-strategy"]);
3907
+ const stateRootDir = args["state-dir"] ? resolve(args.cwd, args["state-dir"]) : void 0;
3908
+ let binding = null;
3909
+ if (!args.general) try {
3910
+ binding = await resolveProjectBinding(await readProjectConfig(configPath), {
3911
+ configPath,
3912
+ cwd: args.cwd,
3913
+ binding: bindingName,
3914
+ projectId,
3915
+ native: !bindingName && !projectId,
3916
+ teamId: args.team,
3917
+ apiUrl: args.apiUrl || void 0,
3918
+ overrides: {
3919
+ ...args.source === void 0 ? {} : { source: args.source },
3920
+ ...overrideStrategy === void 0 ? {} : { strategy: overrideStrategy }
3921
+ }
3922
+ });
3923
+ 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`);
3924
+ } catch (cause) {
3925
+ 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 });
3926
+ }
3927
+ const workspaceStrategy = binding?.strategy ?? overrideStrategy ?? "existing";
3928
+ if (workspaceStrategy === "none" && args.source !== void 0) throw new ProjectConfigError("selection", "No-workspace execution cannot specify a source");
3929
+ let source = binding?.source;
3930
+ if (workspaceStrategy !== "none" && !source) source = await canonicalDirectory(resolve(args.cwd, args.source ?? "."));
3931
+ 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`);
3932
+ if (workspaceStrategy === "git-worktree" && source) await validateGitSource(source);
3933
+ return {
3934
+ configPath,
3935
+ selectedBy: args.binding || args.project ? "explicit" : bindingName ? "activation" : binding ? "ancestor" : "general",
3936
+ projectId: binding?.projectId ?? null,
3937
+ teamId: binding?.teamId ?? args.team,
3938
+ apiUrl: binding?.apiUrl ?? (args.apiUrl || void 0),
3939
+ ...binding ? { binding } : {},
3940
+ source,
3941
+ strategy: workspaceStrategy,
3942
+ stateRootDir,
3943
+ workspaceExplicit: Boolean(binding || overrideStrategy || args.source)
3944
+ };
3945
+ }
3946
+ /** A selected location fixes this worker's strategy; saved profiles are never mutated. */
3947
+ function applyProjectWorkspacePolicy(profile, selection) {
3948
+ if (!selection.workspaceExplicit) return profile;
3949
+ if (selection.strategy === "isolated-directory") throw new Error("This runtime does not yet support isolated-directory preparation");
3950
+ if (selection.binding?.hooks?.afterCreate || selection.binding?.hooks?.beforeRun) throw new Error("This runtime does not yet support project setup hooks");
3951
+ const mode = selection.strategy === "existing" ? "shared_mount" : selection.strategy === "git-worktree" ? "dedicated_worktree" : "none";
3952
+ if (profile.allowedWorkspaceModes.length && !profile.allowedWorkspaceModes.includes(mode)) throw new Error(`Workspace strategy ${selection.strategy} is not allowed by profile ${profile.name}`);
3953
+ return {
3954
+ ...profile,
3955
+ mountPath: selection.source ?? profile.mountPath,
3956
+ defaultWorkspaceMode: mode,
3957
+ allowedWorkspaceModes: [mode]
3958
+ };
3959
+ }
3960
+ var PROJECT_RUN_FLAGS = ` --binding <name> Select a saved local project location.
3961
+ --project <uuid> Select a project and its unambiguous binding.
3962
+ --general Serve General work (projectId: null).
3963
+ --config-file <path> Explicit project bindings JSON.
3964
+ --source <path> Run-only source folder override.
3965
+ --workspace-strategy <name> existing, git-worktree, none; isolated-directory
3966
+ is reserved and currently unsupported.
3967
+ --state-dir <path> Supervisor/session state, separate from source.
3968
+ Default: profile mount root (existing state retained).`;
3969
+ //#endregion
3970
+ //#region src/lib/help.ts
3971
+ var COMMON_REQUIRED_FLAGS = `\
3972
+ -a, --agent <name> MoltNet agent identity. Agent-key auth is
3973
+ configless; OAuth2 reads moltnet.json.
3974
+ --profile <uuid|name> Remote runtime profile. Repeat for poll/drain
3975
+ to declare priority order. Provider, model,
3976
+ sandbox policy, prerequisites, and runtime
3977
+ execution policy come from the selected profile.`;
3978
+ var COMMON_OPTIONAL_FLAGS = `\
3979
+ --sandbox <path> Deprecated. Remote runtime profiles define
3980
+ sandbox policy.
3981
+ --agent-root <path> Explicit legacy identity bundle location.
3982
+ Omitted: use the central identity store.
3983
+ ${PROJECT_RUN_FLAGS}
3984
+ --git-author <"Name <email>">
3985
+ Non-secret git identity projected into the
3986
+ guest for host-brokered commit signing. Default:
3987
+ host git config. Configless agent-key runs must
3988
+ provide this flag or MOLTNET_GIT_AUTHOR.
3989
+ Env: MOLTNET_GIT_AUTHOR.
3990
+ --heartbeat-interval-ms <n> Reporter heartbeat cadence. Default: 60000.
3991
+ --warm-retention-sec <n> Resumability window for runtime slots
3992
+ (Pi sessions + reusable worktrees) after use.
3993
+ Default: 1800.
3994
+ --debug Verbose logging: also log successful list/claim
3995
+ outcomes (candidate counts, claim attempts).`;
3996
+ var REGISTERED_TASK_TYPES = Object.keys(BUILT_IN_TASK_TYPES).sort();
3997
+ function knownTaskTypesList() {
3998
+ return REGISTERED_TASK_TYPES.join(", ");
3999
+ }
4000
+ var ROOT_USAGE = `\
4001
+ agent-daemon — long-running task worker for MoltNet.
4002
+
4003
+ Usage: agent-daemon [--runtime <module>] <command> [...flags]
4004
+
4005
+ Runtime:
4006
+ --runtime <module> Trusted local file or installed package whose
4007
+ default export is a DaemonRuntimeAdapter.
4008
+ Omit to use the built-in gondolin_pi runtime.
4009
+
4010
+ Commands:
4011
+ poll Long-running worker. Polls the task queue and claims tasks
4012
+ matching the configured filter until SIGINT/SIGTERM.
4013
+ once Claim and execute one specific queued task by id, then exit.
4014
+ drain Poll until the queue has nothing claimable, then exit.
4015
+ Useful for batch eval runs and demos.
4016
+ server Supervisor for managed runs: authorized standalone local control,
4017
+ agent/provider config store, and start/stop of poll/drain child
4018
+ processes. Binds 127.0.0.1 or a private native socket.
4019
+ providers Manage configured endpoints and Pi OAuth subscriptions without
4020
+ starting the Agent Server. See \`agent-daemon providers --help\`.
4021
+ sync-sessions
4022
+ Repair durable runtime-session checkpoints from local slot files.
4023
+ update check
4024
+ Check the stable MoltNet agent release without reading credentials.
4025
+
4026
+ Run \`agent-daemon <command> --help\` for command-specific flags.
4027
+
4028
+ Prerequisites:
4029
+ - configless: MOLTNET_AGENT_KEY (or MOLTNET_AGENT_KEY_REF) and
4030
+ MOLTNET_PRIVATE_KEY (or MOLTNET_PRIVATE_KEY_REF); no agent files
4031
+ - config-based: ~/.config/moltnet/identities/<agent>/moltnet.json
4032
+ carrying agent_key_refs or agent_key_ref (OAuth2 is not accepted)
4033
+ --agent-root explicitly selects a legacy .moltnet/<agent> bundle
4034
+
4035
+ No key yet? Mint one with the CLI (--store writes the team slot into
4036
+ moltnet.json and keeps the secret in a provider):
4037
+
4038
+ moltnet teams list # find the team id
4039
+ moltnet agents keys create --team-id <team-uuid> \\
4040
+ --name <agent>-daemon --store
4041
+
4042
+ https://docs.themolt.net/operate/agent-keys#run-the-daemon-with-an-agent-key
4043
+ - --profile — remote runtime profile supplies provider/model/sandbox
4044
+ policy and CWD is used as the VM mountPath.
4045
+
4046
+ Registered task types: ${knownTaskTypesList()}`;
4047
+ var POLL_HELP = `\
4048
+ agent-daemon poll — long-running task worker.
4049
+
4050
+ Usage:
4051
+ agent-daemon poll --team <uuid> --agent <name> --profile <uuid|name> [...]
4052
+
4053
+ Required:
4054
+ --team <uuid> Team whose queue to serve. The daemon must be
4055
+ a member of this team (canAccessTeam permit).
4056
+ ${COMMON_REQUIRED_FLAGS}
4057
+
4058
+ Optional:
4059
+ --task-types <csv> Whitelist of task types to claim. Default:
4060
+ accept any registered type. Known types:
4061
+ ${knownTaskTypesList()}
4062
+ --correlation-id <uuid> Restrict claims to one orchestration run.
4063
+ --diary-ids <csv> Further client-side filter on task.diaryId.
4064
+ --poll-interval-ms <n> Idle backoff floor. Default: 2000.
4065
+ --max-poll-interval-ms <n> Idle backoff ceiling. Default: 30000.
4066
+ --list-limit <n> Page size per list call. Default: 10.
4067
+ ${COMMON_OPTIONAL_FLAGS}
4068
+
4069
+ Example:
4070
+ agent-daemon poll \\
4071
+ --team 6743b4b1-6b93-46e2-a048-19490f04f91a \\
4072
+ --task-types curate_pack,fulfill_brief \\
4073
+ --agent legreffier \\
4074
+ --profile github-linear \\
4075
+ --profile local-fallback
4076
+
4077
+ Stops cleanly on SIGINT/SIGTERM (drains the in-flight task before exit).`;
4078
+ var ONCE_HELP = `\
4079
+ agent-daemon once — execute one specific queued task by id, then exit.
4080
+
4081
+ Usage:
4082
+ agent-daemon once --task-id <uuid> --agent <name> --profile <uuid|name> [...]
4083
+
4084
+ Required:
4085
+ -t, --task-id <uuid> Task to claim and execute. Must already be
4086
+ in 'queued' status.
4087
+ ${COMMON_REQUIRED_FLAGS}
4088
+
4089
+ Optional:
4090
+ --team <uuid> Team scope for resolving --profile by name.
4091
+ Required only when --profile is a name.
4092
+ ${COMMON_OPTIONAL_FLAGS}
4093
+
4094
+ Example:
4095
+ agent-daemon once \\
4096
+ --task-id 26004a77-bc10-43ef-a79f-c8e62faf59b1 \\
4097
+ --agent legreffier \\
4098
+ --profile github-linear
4099
+
4100
+ Exits 0 on completed, 1 on failed/cancelled/runtime-error.`;
4101
+ var DRAIN_HELP = `\
4102
+ agent-daemon drain — poll until the queue is empty, then exit.
4103
+
4104
+ Usage:
4105
+ agent-daemon drain --team <uuid> --agent <name> --profile <uuid|name> [...]
4106
+
4107
+ Same flags as \`poll\`. The only behavioural difference: \`drain\` exits
4108
+ when a list call confirms no claimable tasks remain (vs \`poll\` which
4109
+ sleeps and retries forever).
4110
+
4111
+ Required:
4112
+ --team <uuid> Team whose queue to drain.
4113
+ ${COMMON_REQUIRED_FLAGS}
4114
+
4115
+ Optional:
4116
+ --task-types <csv> Whitelist. Known types: ${knownTaskTypesList()}
4117
+ --correlation-id <uuid> Restrict claims to one orchestration run.
4118
+ --wait-for-first-task-sec <n>
4119
+ Wait this long for an initially empty run before
4120
+ exiting. After the first claim, exit on empty.
4121
+ --wait-after-task-sec <n> Require the queue to remain empty for this long
4122
+ after a claim before exiting.
4123
+ --diary-ids <csv> Diary filter.
4124
+ --poll-interval-ms <n> Default: 2000.
4125
+ --max-poll-interval-ms <n> Default: 30000.
4126
+ --list-limit <n> Default: 10.
4127
+ ${COMMON_OPTIONAL_FLAGS}
4128
+
4129
+ Example:
4130
+ agent-daemon drain \\
4131
+ --team 6743b4b1-6b93-46e2-a048-19490f04f91a \\
4132
+ --task-types judge_pack \\
4133
+ --agent legreffier \\
4134
+ --profile eval-judge`;
4135
+ var SYNC_SESSIONS_HELP = `\
4136
+ agent-daemon sync-sessions — repair durable runtime-session checkpoints.
4137
+
4138
+ Usage:
4139
+ agent-daemon sync-sessions --team <uuid> --agent <name> [...]
4140
+
4141
+ Scans this daemon's team-scoped runtime slots, compares local Pi session files
4142
+ with durable runtime-session metadata, and uploads missing or stale checkpoints.
4143
+
4144
+ Required:
4145
+ --team <uuid> Team whose runtime slots to inspect.
4146
+ -a, --agent <name> MoltNet agent identity. Reads credentials
4147
+ from <agent-root>/.moltnet/<name>/moltnet.json.
4148
+
4149
+ Optional:
4150
+ --runtime-profile-id <uuid> Limit repair to one runtime profile.
4151
+ --state <active|idle> Limit scanned slots by state. Default: all.
4152
+ --limit <n> Max slots to scan, 1..200. Default: 100.
4153
+ --dry-run Report missing/stale sessions without uploading.
4154
+ --agent-root <path> Explicit legacy identity bundle location.
4155
+ Omitted: use the central identity store.
4156
+ ${PROJECT_RUN_FLAGS}
4157
+ --debug Accepted for consistency; no extra output yet.
4158
+
4159
+ Example:
4160
+ agent-daemon sync-sessions \\
4161
+ --team 6743b4b1-6b93-46e2-a048-19490f04f91a \\
4162
+ --agent legreffier \\
4163
+ --state idle`;
4164
+ function isHelpFlag(args) {
4165
+ return args.includes("--help") || args.includes("-h");
4166
+ }
4167
+ var AGENT_SERVER_HELP = `\
4168
+ agent-daemon server — local supervisor for managed runs.
4169
+
4170
+ Standalone mode binds 127.0.0.1 for authorized local-control clients. MoltNet
4171
+ Agent Desktop instead uses a private Unix socket with a process-scoped grant.
4172
+ Both modes configure agents and providers and start/stop child runs.
4173
+
4174
+ Options:
4175
+ --port <n> Loopback port. Default: 17374.
4176
+ Env: MOLTNET_AGENT_SERVER_PORT.
4177
+ --allowed-origins <csv> Exact browser-controller origins allowed
4178
+ local control.
4179
+ Default: https://console.themolt.net.
4180
+ Env: MOLTNET_AGENT_SERVER_ALLOWED_ORIGINS.
4181
+ --root <path> Config root. Default: ~/.config/moltnet
4182
+ (or MOLTNET_AGENT_SERVER_ROOT).
4183
+ --api-url <url> Default MoltNet API for new managed agents.
4184
+ Default: https://api.themolt.net.
4185
+ --heartbeat-interval-ms <n> Child reporter heartbeat cadence. Default: 60000.
4186
+ --warm-retention-sec <n> Child session/workspace retention. Default: 1800.
4187
+ --supervised Also stop gracefully when stdin reaches EOF.
4188
+ --native-socket <path> Private native-only socket (requires --supervised).
4189
+ Absolute, at most 100 bytes, with a new socket in
4190
+ a caller-owned 0700 directory. TCP flags are not
4191
+ accepted; inherited TCP env settings are ignored.
4192
+
4193
+ Standalone mode uses loopback HTTP on every platform. Desktop socket mode does
4194
+ not open a TCP listener.
4195
+ `;
4196
+ var PROVIDERS_HELP = `\
4197
+ moltnet-agent providers — manage local model providers.
4198
+
4199
+ Usage:
4200
+ moltnet-agent providers list [--json] [--root <path>]
4201
+ moltnet-agent providers set <id> [--base-url <url>] [--api <pi-api-kind>]
4202
+ [--model <id> ... | --clear-models]
4203
+ [--model-input <id>=text,image ...]
4204
+ [--api-key-stdin | --clear-api-key] [--root <path>]
4205
+ moltnet-agent providers discover <id> [--save] [--json] [--root <path>]
4206
+ moltnet-agent providers remove <id> [--yes] [--root <path>]
4207
+ moltnet-agent providers login <id> [--auth-method <method-id>]
4208
+ [--root <path>]
4209
+ moltnet-agent providers logout <id> [--yes] [--root <path>]
4210
+
4211
+ The default root is ~/.config/moltnet. MOLTNET_AGENT_SERVER_ROOT remains the
4212
+ environment override. API keys are accepted only from redirected stdin; they
4213
+ are stored separately and providers.json contains only a secret reference.
4214
+
4215
+ --model declares a text-only model. --model-input declares a model together
4216
+ with the input modalities it accepts, and is what makes a vision model usable:
4217
+ a model with no declared modalities is text-only to Pi, which drops image
4218
+ content parts before the request leaves the runtime.
4219
+ `;
4220
+ //#endregion
4025
4221
  //#region src/lib/abort-active-attempt.ts
4026
4222
  /** Best-effort signal cleanup; lease expiry remains the final backstop. */
4027
4223
  async function abortActiveAttemptOnSignal(opts) {
@@ -4058,7 +4254,7 @@ async function resolveDaemonAgentIdentity(input) {
4058
4254
  }
4059
4255
  //#endregion
4060
4256
  //#region src/lib/correlation.ts
4061
- var execFileAsync$1 = promisify(execFile);
4257
+ var execFileAsync = promisify(execFile);
4062
4258
  var CORRELATION_TRAILER_KEY = "Moltnet-Correlation-Id";
4063
4259
  var CORRELATION_MARKER_RE = /<!--\s*moltnet-correlation:\s*([\w-]+)\s*-->/i;
4064
4260
  new RegExp(`^${CORRELATION_TRAILER_KEY}:\\s*(\\S+)\\s*$`, "m");
@@ -4105,7 +4301,7 @@ function makePrBodyAnchorWriter(deps) {
4105
4301
  function createGhCliClient() {
4106
4302
  return {
4107
4303
  async get({ owner, repo, number }) {
4108
- const { stdout } = await execFileAsync$1("gh", [
4304
+ const { stdout } = await execFileAsync("gh", [
4109
4305
  "api",
4110
4306
  `repos/${owner}/${repo}/pulls/${number}`,
4111
4307
  "--jq",
@@ -4114,7 +4310,7 @@ function createGhCliClient() {
4114
4310
  return JSON.parse(stdout);
4115
4311
  },
4116
4312
  async patch({ owner, repo, number }, body) {
4117
- await execFileAsync$1("gh", [
4313
+ await execFileAsync("gh", [
4118
4314
  "api",
4119
4315
  "-X",
4120
4316
  "PATCH",
@@ -4201,6 +4397,7 @@ function slugifySessionComponent(input) {
4201
4397
  }
4202
4398
  //#endregion
4203
4399
  //#region src/lib/task-execution-plan.ts
4400
+ var WorkspaceModeMismatchError = class extends Error {};
4204
4401
  function buildDaemonTaskExecutionPlan(task, stateDirs, identity, warmRetentionSec, runtimeProfileWorkspacePolicy = {}, attemptN) {
4205
4402
  const descriptor = deriveTaskSessionDescriptor(task);
4206
4403
  const workspaceMode = resolveTaskWorkspaceMode(task, descriptor.policy, runtimeProfileWorkspacePolicy);
@@ -4274,6 +4471,7 @@ function resolveTaskWorkspaceMode(task, policy, runtimeProfileWorkspacePolicy) {
4274
4471
  const requestedWorkspace = policy.acceptsInputWorkspaceOverride && typeof task.input.execution?.workspace === "string" ? task.input.execution.workspace : null;
4275
4472
  if (isRuntimeProfileWorkspaceMode(requestedWorkspace)) {
4276
4473
  if (allowed.has(requestedWorkspace)) return toDaemonWorkspaceMode(requestedWorkspace);
4474
+ if (runtimeProfileWorkspacePolicy.workspaceExplicit) throw new WorkspaceModeMismatchError(`Requested workspace mode ${requestedWorkspace} is not allowed by profile ${runtimeProfileWorkspacePolicy.profileName ?? "(selected)"}; allowed: ${[...allowed].join(", ")}`);
4277
4475
  }
4278
4476
  if (profileDefault && allowed.has(profileDefault)) return toDaemonWorkspaceMode(profileDefault);
4279
4477
  if (allowed.has(policy.workspaceMode)) return policy.workspaceMode;
@@ -4381,8 +4579,8 @@ function assertPlanAllowedByWorkspacePolicy(plan, policy, runtimeProfileId) {
4381
4579
  "dedicated_worktree"
4382
4580
  ]);
4383
4581
  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`);
4582
+ 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}")`);
4583
+ if (!allowed.has(effectiveMode)) throw new WorkspaceModeMismatchError(`Runtime profile "${policy?.profileName ?? runtimeProfileId}" forbids final workspace mode "${effectiveMode}"; allowed: ${[...allowed].join(", ")}`);
4386
4584
  }
4387
4585
  function planToRuntimeProfileWorkspaceMode(plan) {
4388
4586
  if (plan.workspaceMode === "scratch_mount") return "none";
@@ -4569,7 +4767,8 @@ function resolveProducerWorkspaceCopySource(producer, stateDirs) {
4569
4767
  if (isDisposableScratchWorkspace(producer, stateDirs)) return null;
4570
4768
  throw new ProducerContextResolutionError(`Producer workspace path is missing on disk: ${workspacePath}`);
4571
4769
  }
4572
- const sharedMountRoot = dirname(dirname(stateDirs.rootDir));
4770
+ const sharedMountRoot = stateDirs.mountPath;
4771
+ if (!sharedMountRoot) throw new ProducerContextResolutionError("Shared producer mount root was not supplied by the runtime profile");
4573
4772
  if (!existsSync(sharedMountRoot)) throw new ProducerContextResolutionError(`Shared producer mount root is missing on disk: ${sharedMountRoot}`);
4574
4773
  return sharedMountRoot;
4575
4774
  }
@@ -5557,7 +5756,6 @@ var AgentServerStore = class {
5557
5756
  activations: {}
5558
5757
  };
5559
5758
  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
5759
  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
5760
  for (const [alias, activation] of Object.entries(state.activations)) validateActivation(alias, activation);
5563
5761
  for (const [alias, registration] of Object.entries(state.pendingRegistrations)) {
@@ -5661,7 +5859,18 @@ var AgentServerStore = class {
5661
5859
  if (activation.source === "managed") delete state.pendingRegistrations[alias];
5662
5860
  this.writeAgentServerState(state);
5663
5861
  }
5664
- listActivations() {
5862
+ writeCredentialMetadata(alias, teamId, metadata) {
5863
+ const activation = this.readActivation(alias);
5864
+ if (!activation) throw new AgentServerStoreError("not_found", "Identity is not activated");
5865
+ this.writeActivation({
5866
+ ...activation,
5867
+ credentialHealth: {
5868
+ ...activation.credentialHealth,
5869
+ [teamId]: metadata
5870
+ }
5871
+ });
5872
+ }
5873
+ listActivations() {
5665
5874
  return Object.values(this.readAgentServerState().activations).sort((a, b) => a.alias.localeCompare(b.alias));
5666
5875
  }
5667
5876
  get providersPath() {
@@ -5726,6 +5935,24 @@ var AgentServerStore = class {
5726
5935
  writeRun(record) {
5727
5936
  writeJsonAtomic(join(this.runDir(record.id), "run.json"), record);
5728
5937
  }
5938
+ /** Status polling reads history without blocking the supervisor event loop. */
5939
+ async listRunsAsync(limit, includeIds = []) {
5940
+ let ids;
5941
+ try {
5942
+ ids = await readdir(this.runsDir);
5943
+ } catch {
5944
+ return [];
5945
+ }
5946
+ const selected = ids.filter((id) => NAME_RE.test(id)).sort().reverse().slice(0, Math.max(0, limit));
5947
+ return (await Promise.all([...new Set([...includeIds, ...selected])].map(async (id) => {
5948
+ try {
5949
+ return JSON.parse(await readFile(join(this.runDir(id), "run.json"), "utf8"));
5950
+ } catch (error) {
5951
+ if (error.code === "ENOENT") return null;
5952
+ throw error;
5953
+ }
5954
+ }))).filter((record) => record !== null).sort((a, b) => b.startedAt.localeCompare(a.startedAt));
5955
+ }
5729
5956
  listRuns(limit = Number.POSITIVE_INFINITY) {
5730
5957
  let ids;
5731
5958
  try {
@@ -5956,14 +6183,32 @@ function mergePiModels(store, repo) {
5956
6183
  //#endregion
5957
6184
  //#region src/lib/state-dir.ts
5958
6185
  function ensureDaemonStateDirs(mountPath) {
5959
- const rootDir = join(mountPath, ".moltnet", "d");
6186
+ const rootDir = canonicalStatePath(join(mountPath, ".moltnet", "d"));
5960
6187
  const piSessionsDir = join(rootDir, "pi-sessions");
5961
- mkdirSync(piSessionsDir, { recursive: true });
6188
+ mkdirSync(rootDir, {
6189
+ recursive: true,
6190
+ mode: 448
6191
+ });
6192
+ mkdirSync(piSessionsDir, {
6193
+ recursive: true,
6194
+ mode: 448
6195
+ });
5962
6196
  return {
5963
- rootDir,
5964
- piSessionsDir
6197
+ rootDir: realpathSync(rootDir),
6198
+ piSessionsDir: realpathSync(piSessionsDir)
5965
6199
  };
5966
6200
  }
6201
+ function canonicalStatePath(path) {
6202
+ let parent = resolve(path);
6203
+ const missing = [];
6204
+ while (!existsSync(parent)) {
6205
+ missing.unshift(basename(parent));
6206
+ const next = dirname(parent);
6207
+ if (next === parent) throw new Error(`Cannot resolve daemon state root ${path}`);
6208
+ parent = next;
6209
+ }
6210
+ return join(realpathSync(parent), ...missing);
6211
+ }
5967
6212
  //#endregion
5968
6213
  //#region src/lib/prepare-runtime-profile.ts
5969
6214
  /** Validate and prepare a profile through the shared daemon execution path. */
@@ -5985,7 +6230,8 @@ async function prepareRuntimeProfile(input) {
5985
6230
  rootDir: profile.mountPath,
5986
6231
  path: profile.source
5987
6232
  };
5988
- const stateDirs = ensureDaemonStateDirs(sandbox.rootDir);
6233
+ const stateDirs = ensureDaemonStateDirs(input.stateRootDir ?? sandbox.rootDir);
6234
+ stateDirs.mountPath = profile.mountPath;
5989
6235
  const slotIdentity = {
5990
6236
  agentName: input.agentName,
5991
6237
  runtimeProfileId: profile.id,
@@ -6002,6 +6248,8 @@ async function prepareRuntimeProfile(input) {
6002
6248
  slotIdentity,
6003
6249
  warmRetentionSec: input.warmRetentionSec,
6004
6250
  workspacePolicy: {
6251
+ workspaceExplicit: input.workspaceExplicit,
6252
+ profileName: profile.name,
6005
6253
  defaultWorkspaceMode: profile.defaultWorkspaceMode,
6006
6254
  allowedWorkspaceModes: profile.allowedWorkspaceModes
6007
6255
  },
@@ -6012,6 +6260,32 @@ async function prepareRuntimeProfile(input) {
6012
6260
  };
6013
6261
  }
6014
6262
  //#endregion
6263
+ //#region src/lib/project-task-source.ts
6264
+ function createProjectOnceSource(selection, options) {
6265
+ return new ApiTaskSource({
6266
+ ...options,
6267
+ projectId: selection.projectId
6268
+ });
6269
+ }
6270
+ function createProjectPollingSource(selection, options) {
6271
+ return new PollingApiTaskSource({
6272
+ ...options,
6273
+ projectId: selection.projectId,
6274
+ isTaskEligible: (task) => {
6275
+ if (!selection.workspaceExplicit) return true;
6276
+ if (resolveTaskWorkspaceRevision(task.input) && selection.strategy !== "git-worktree") return false;
6277
+ if (!getTaskExecutionPolicy(task.taskType).acceptsInputWorkspaceOverride) return true;
6278
+ const requested = task.input?.execution?.workspace;
6279
+ if (!requested || ![
6280
+ "none",
6281
+ "shared_mount",
6282
+ "dedicated_worktree"
6283
+ ].includes(requested)) return true;
6284
+ return requested === (selection.strategy === "existing" ? "shared_mount" : selection.strategy === "git-worktree" ? "dedicated_worktree" : "none");
6285
+ }
6286
+ });
6287
+ }
6288
+ //#endregion
6015
6289
  //#region src/lib/runtime-context.ts
6016
6290
  var storage = new AsyncLocalStorage();
6017
6291
  function runWithDaemonRuntimeContext(context, callback) {
@@ -6591,6 +6865,7 @@ async function runPolling(opts) {
6591
6865
  args: opts.argv,
6592
6866
  options: {
6593
6867
  ...runtimeCommandOptionDefs(),
6868
+ ...projectRunOptionDefs(),
6594
6869
  team: { type: "string" },
6595
6870
  "task-types": { type: "string" },
6596
6871
  "correlation-id": { type: "string" },
@@ -6607,12 +6882,12 @@ async function runPolling(opts) {
6607
6882
  }
6608
6883
  }
6609
6884
  });
6610
- if (!values.team) {
6885
+ if (!values.team && !values.binding && !values.project && !values["config-file"]) {
6611
6886
  console.error("Missing required flag: --team\n");
6612
6887
  console.error(opts.helpText);
6613
6888
  return 1;
6614
6889
  }
6615
- const teamId = values.team;
6890
+ let teamId = values.team ?? "";
6616
6891
  const profileValues = parseProfileValues(values.profile);
6617
6892
  if (profileValues.length === 0) {
6618
6893
  console.error("Missing required flag: --profile\n");
@@ -6655,6 +6930,41 @@ async function runPolling(opts) {
6655
6930
  }
6656
6931
  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
6932
  const cfg = loadConfig();
6933
+ let selection;
6934
+ try {
6935
+ const endpoint = await resolveSelectionApiUrl(identity.agent, {
6936
+ agentRootDir: values["agent-root"] ? resolve(process.cwd(), values["agent-root"]) : void 0,
6937
+ credentialSource: cfg.credentialSource,
6938
+ envApiUrl: cfg.apiUrl
6939
+ });
6940
+ selection = await resolveRunProjectSelection({
6941
+ agent: identity.agent,
6942
+ cwd: process.cwd(),
6943
+ apiUrl: endpoint,
6944
+ binding: values.binding,
6945
+ project: values.project,
6946
+ team: values.team,
6947
+ general: values.general,
6948
+ "config-file": values["config-file"],
6949
+ "state-dir": values["state-dir"],
6950
+ source: values.source,
6951
+ "workspace-strategy": values["workspace-strategy"]
6952
+ });
6953
+ teamId = selection.teamId ?? "";
6954
+ if (!teamId) throw new Error("Select --team or a binding with a team");
6955
+ } catch (error) {
6956
+ await logDaemonStartupFailure({
6957
+ serviceName: "agent-daemon.selection",
6958
+ level: cfg.logLevel || "info",
6959
+ gate: "project_selection",
6960
+ agent: identity.agent,
6961
+ credentialSource: cfg.credentialSource,
6962
+ error
6963
+ });
6964
+ console.error(error instanceof Error ? error.message : String(error));
6965
+ console.error(opts.helpText);
6966
+ return 1;
6967
+ }
6658
6968
  const credentialSources = {
6659
6969
  profileRequirements: cfg.profileCredentialRequirements,
6660
6970
  bindings: cfg.credentialBindings
@@ -6668,6 +6978,7 @@ async function runPolling(opts) {
6668
6978
  agentRootDir: explicitAgentRootDir,
6669
6979
  credentialSource: cfg.credentialSource,
6670
6980
  envApiUrl: cfg.apiUrl,
6981
+ projectApiUrl: selection.binding?.apiUrl,
6671
6982
  teamId
6672
6983
  });
6673
6984
  gate = "authenticate_and_bind";
@@ -6722,18 +7033,25 @@ async function runPolling(opts) {
6722
7033
  throw error;
6723
7034
  }
6724
7035
  })();
6725
- const daemonRootDir = explicitAgentRootDir ?? process.cwd();
6726
- const resolvedProfiles = await resolveRuntimeProfiles({
7036
+ const daemonRootDir = selection.binding || values.source ? selection.source ?? process.cwd() : explicitAgentRootDir ?? process.cwd();
7037
+ const resolvedProfiles = (await resolveRuntimeProfiles({
6727
7038
  agent: ctx.agent,
6728
7039
  profiles: profileValues,
6729
7040
  teamId,
6730
7041
  cwd: daemonRootDir
6731
- });
7042
+ })).map((profile) => applyProjectWorkspacePolicy(profile, selection));
6732
7043
  const { logger, shutdown: shutdownLogger } = createRootLogger({
6733
7044
  name: `agent-daemon.${opts.modeLabel}`,
6734
7045
  level: cfg.logLevel || (identity.debug ? "debug" : "info")
6735
7046
  });
6736
7047
  const rootLogger = logger.child({
7048
+ projectId: selection.projectId,
7049
+ binding: selection.binding?.name,
7050
+ selectedBy: selection.selectedBy,
7051
+ source: daemonRootDir,
7052
+ strategy: selection.strategy,
7053
+ stateRootDir: selection.stateRootDir ?? daemonRootDir,
7054
+ apiUrl: selection.apiUrl,
6737
7055
  mode: opts.modeLabel,
6738
7056
  agent: identity.agent,
6739
7057
  teamId,
@@ -6756,6 +7074,8 @@ async function runPolling(opts) {
6756
7074
  agent: ctx.agent,
6757
7075
  agentName: identity.agent,
6758
7076
  profile,
7077
+ stateRootDir: selection.stateRootDir,
7078
+ workspaceExplicit: selection.workspaceExplicit,
6759
7079
  prerequisiteEnv: cfg.profilePrerequisiteEnv,
6760
7080
  runtimeAdapter,
6761
7081
  runtimeInstanceId,
@@ -6787,6 +7107,9 @@ async function runPolling(opts) {
6787
7107
  agent: ctx.agent,
6788
7108
  endpoint: cfg.otelEndpoint,
6789
7109
  resourceAttributes: {
7110
+ "moltnet.project.id": selection.projectId ?? "general",
7111
+ "moltnet.project.selection": selection.selectedBy,
7112
+ "moltnet.workspace.strategy": selection.strategy,
6790
7113
  "moltnet.team.id": teamId,
6791
7114
  "moltnet.agent.name": identity.agent,
6792
7115
  "moltnet.credential.source": ctx.credentialSource,
@@ -6905,7 +7228,7 @@ async function runPolling(opts) {
6905
7228
  try {
6906
7229
  runtime = new AgentRuntime({
6907
7230
  logger: rootLogger,
6908
- source: new PollingApiTaskSource({
7231
+ source: createProjectPollingSource(selection, {
6909
7232
  agent: ctx.agent,
6910
7233
  teamId,
6911
7234
  taskTypes: taskTypes.length > 0 ? taskTypes : void 0,
@@ -7027,7 +7350,7 @@ async function runPolling(opts) {
7027
7350
  error: {
7028
7351
  code: err instanceof ProducerContextResolutionError ? "producer_context_missing" : "execution_plan_failed",
7029
7352
  message,
7030
- retryable: false
7353
+ retryable: err instanceof WorkspaceModeMismatchError
7031
7354
  }
7032
7355
  };
7033
7356
  }
@@ -7227,6 +7550,7 @@ async function runOnce(argv, runtimeAdapter = defaultPiDaemonAdapter) {
7227
7550
  args: argv,
7228
7551
  options: {
7229
7552
  ...runtimeCommandOptionDefs(),
7553
+ ...projectRunOptionDefs(),
7230
7554
  "task-id": {
7231
7555
  type: "string",
7232
7556
  short: "t"
@@ -7264,6 +7588,39 @@ async function runOnce(argv, runtimeAdapter = defaultPiDaemonAdapter) {
7264
7588
  return 1;
7265
7589
  }
7266
7590
  const cfg = loadConfig();
7591
+ let selection;
7592
+ try {
7593
+ const endpoint = await resolveSelectionApiUrl(identity.agent, {
7594
+ agentRootDir: values["agent-root"] ? resolve(process.cwd(), values["agent-root"]) : void 0,
7595
+ credentialSource: cfg.credentialSource,
7596
+ envApiUrl: cfg.apiUrl
7597
+ });
7598
+ selection = await resolveRunProjectSelection({
7599
+ agent: identity.agent,
7600
+ cwd: process.cwd(),
7601
+ apiUrl: endpoint,
7602
+ binding: values.binding,
7603
+ project: values.project,
7604
+ team: values.team,
7605
+ general: values.general,
7606
+ "config-file": values["config-file"],
7607
+ "state-dir": values["state-dir"],
7608
+ source: values.source,
7609
+ "workspace-strategy": values["workspace-strategy"]
7610
+ });
7611
+ } catch (error) {
7612
+ await logDaemonStartupFailure({
7613
+ serviceName: "agent-daemon.selection",
7614
+ level: cfg.logLevel || "info",
7615
+ gate: "project_selection",
7616
+ agent: identity.agent,
7617
+ credentialSource: cfg.credentialSource,
7618
+ error
7619
+ });
7620
+ console.error(error instanceof Error ? error.message : String(error));
7621
+ console.error(ONCE_HELP);
7622
+ return 1;
7623
+ }
7267
7624
  const credentialSources = {
7268
7625
  profileRequirements: cfg.profileCredentialRequirements,
7269
7626
  bindings: cfg.credentialBindings
@@ -7277,13 +7634,14 @@ async function runOnce(argv, runtimeAdapter = defaultPiDaemonAdapter) {
7277
7634
  agentRootDir: explicitAgentRootDir,
7278
7635
  credentialSource: cfg.credentialSource,
7279
7636
  envApiUrl: cfg.apiUrl,
7280
- teamId: values.team
7637
+ projectApiUrl: selection.binding?.apiUrl,
7638
+ teamId: selection.teamId
7281
7639
  });
7282
7640
  gate = "authenticate_and_bind";
7283
7641
  const whoami = await validateStartupBinding({
7284
7642
  agent: resolvedContext.agent,
7285
7643
  credentialTeamId: resolvedContext.credentialTeamId,
7286
- teamId: values.team,
7644
+ teamId: selection.teamId,
7287
7645
  expectedAgent: cfg.expectedAgent
7288
7646
  });
7289
7647
  gate = "resolve_signing_material";
@@ -7330,18 +7688,25 @@ async function runOnce(argv, runtimeAdapter = defaultPiDaemonAdapter) {
7330
7688
  throw error;
7331
7689
  }
7332
7690
  })();
7333
- const daemonRootDir = explicitAgentRootDir ?? process.cwd();
7334
- const profile = await resolveRuntimeProfile({
7691
+ const daemonRootDir = selection.binding || values.source ? selection.source ?? process.cwd() : explicitAgentRootDir ?? process.cwd();
7692
+ const profile = applyProjectWorkspacePolicy(await resolveRuntimeProfile({
7335
7693
  agent: ctx.agent,
7336
7694
  profile: values.profile,
7337
- teamId: values.team,
7695
+ teamId: selection.teamId,
7338
7696
  cwd: daemonRootDir
7339
- });
7697
+ }), selection);
7340
7698
  const { logger, shutdown: shutdownLogger } = createRootLogger({
7341
7699
  name: "agent-daemon.once",
7342
7700
  level: cfg.logLevel || (identity.debug ? "debug" : "info")
7343
7701
  });
7344
7702
  const rootLogger = logger.child({
7703
+ projectId: selection.projectId,
7704
+ binding: selection.binding?.name,
7705
+ selectedBy: selection.selectedBy,
7706
+ source: daemonRootDir,
7707
+ strategy: selection.strategy,
7708
+ stateRootDir: selection.stateRootDir ?? daemonRootDir,
7709
+ apiUrl: selection.apiUrl,
7345
7710
  mode: "once",
7346
7711
  agent: identity.agent,
7347
7712
  provider: profile.provider,
@@ -7365,6 +7730,8 @@ async function runOnce(argv, runtimeAdapter = defaultPiDaemonAdapter) {
7365
7730
  agent: ctx.agent,
7366
7731
  agentName: identity.agent,
7367
7732
  profile,
7733
+ stateRootDir: selection.stateRootDir,
7734
+ workspaceExplicit: selection.workspaceExplicit,
7368
7735
  prerequisiteEnv: cfg.profilePrerequisiteEnv,
7369
7736
  runtimeAdapter,
7370
7737
  runtimeInstanceId,
@@ -7382,6 +7749,9 @@ async function runOnce(argv, runtimeAdapter = defaultPiDaemonAdapter) {
7382
7749
  agent: ctx.agent,
7383
7750
  endpoint: cfg.otelEndpoint,
7384
7751
  resourceAttributes: {
7752
+ "moltnet.project.id": selection.projectId ?? "general",
7753
+ "moltnet.project.selection": selection.selectedBy,
7754
+ "moltnet.workspace.strategy": selection.strategy,
7385
7755
  "moltnet.task.id": taskId,
7386
7756
  "moltnet.agent.name": identity.agent,
7387
7757
  "moltnet.credential.source": ctx.credentialSource,
@@ -7576,7 +7946,7 @@ async function runOnce(argv, runtimeAdapter = defaultPiDaemonAdapter) {
7576
7946
  });
7577
7947
  runtime = new AgentRuntime({
7578
7948
  logger: rootLogger,
7579
- source: new ApiTaskSource({
7949
+ source: createProjectOnceSource(selection, {
7580
7950
  agent: ctx.agent,
7581
7951
  taskId,
7582
7952
  teamId: profile.teamId,
@@ -7669,6 +8039,18 @@ function runPoll(argv, runtimeAdapter) {
7669
8039
  //#region src/lib/provider-lock.ts
7670
8040
  var DEFAULT_LOCK_TIMEOUT_MS = 3e4;
7671
8041
  var LOCK_WAIT_WARNING_MS = 1e3;
8042
+ /**
8043
+ * When to warn that we are waiting on another process.
8044
+ *
8045
+ * A fixed 1s threshold is silently useless to a caller whose whole budget is
8046
+ * shorter than that: the timeout wins the race and the operation fails with no
8047
+ * word of *why* it failed, which is the one thing the caller needs. So the
8048
+ * threshold scales down with the budget, and never sits so close to the
8049
+ * deadline that whether it fires depends on timer drift.
8050
+ */
8051
+ function warningThresholdMs(timeoutMs) {
8052
+ return Math.min(LOCK_WAIT_WARNING_MS, Math.floor(timeoutMs / 2));
8053
+ }
7672
8054
  var ProviderLockError = class extends Error {
7673
8055
  name = "ProviderLockError";
7674
8056
  constructor(code, message, options) {
@@ -7697,6 +8079,7 @@ async function withNamedProviderLock(root, name, work, options) {
7697
8079
  let compromised;
7698
8080
  const startedAt = Date.now();
7699
8081
  const timeoutMs = options.timeoutMs ?? DEFAULT_LOCK_TIMEOUT_MS;
8082
+ const warnAfterMs = warningThresholdMs(timeoutMs);
7700
8083
  const lockfilePath = join(locksDir, `${name}.lock`);
7701
8084
  let warned = false;
7702
8085
  let release;
@@ -7704,7 +8087,7 @@ async function withNamedProviderLock(root, name, work, options) {
7704
8087
  if (options.signal?.aborted) throw new ProviderLockError("lock_aborted", `provider lock acquisition was cancelled for "${name}"`, { cause: options.signal.reason });
7705
8088
  const elapsedMs = Date.now() - startedAt;
7706
8089
  if (elapsedMs >= timeoutMs) throw new ProviderLockError("lock_timeout", `timed out waiting for provider lock "${name}"`);
7707
- if (!warned && elapsedMs >= LOCK_WAIT_WARNING_MS) {
8090
+ if (!warned && elapsedMs >= warnAfterMs) {
7708
8091
  warned = true;
7709
8092
  options.logger?.warn({
7710
8093
  code: "provider_lock_contended",
@@ -8887,29 +9270,97 @@ function registerLoopbackSecurity(app, options) {
8887
9270
  });
8888
9271
  }
8889
9272
  //#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");
9273
+ //#region src/lib/agent-server/connection-settings.ts
9274
+ var RELEASE_CONNECTION = {
9275
+ apiUrl: "https://api.themolt.net",
9276
+ issuer: "https://auth.themolt.net",
9277
+ publicUrl: "https://auth.themolt.net",
9278
+ nativeClientId: "moltnet-native",
9279
+ consoleClientId: "moltnet-console"
9280
+ };
9281
+ var KEYS = Object.keys(RELEASE_CONNECTION);
9282
+ function validateConnectionOverrides(value) {
9283
+ if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("Connection settings must be an object");
9284
+ const result = {};
9285
+ for (const [key, raw] of Object.entries(value)) {
9286
+ if (!KEYS.includes(key) || typeof raw !== "string" || !raw.trim()) throw new Error(`Invalid connection setting: ${key}`);
9287
+ const text = raw.trim();
9288
+ if (key.endsWith("ClientId")) {
9289
+ if (text.length > 255 || /\s/u.test(text)) throw new Error("Client IDs must not contain whitespace");
9290
+ } else {
9291
+ const url = new URL(text);
9292
+ 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)");
9293
+ }
9294
+ result[key] = text;
9295
+ }
9296
+ return result;
8904
9297
  }
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");
9298
+ /** Local administration only. Never exposed through browser authorization. */
9299
+ var ConnectionSettingsStore = class {
9300
+ path;
9301
+ environment;
9302
+ constructor(root, environment = {}) {
9303
+ this.root = root;
9304
+ this.path = join(root, "connection-settings.json");
9305
+ this.environment = validateConnectionOverrides(environment);
9306
+ }
9307
+ view() {
9308
+ let overrides = {};
9309
+ try {
9310
+ overrides = validateConnectionOverrides(JSON.parse(readFileSync(this.path, "utf8")));
9311
+ } catch (error) {
9312
+ if (error.code !== "ENOENT") throw error;
9313
+ }
9314
+ return {
9315
+ defaults: RELEASE_CONNECTION,
9316
+ overrides,
9317
+ environment: this.environment,
9318
+ effective: {
9319
+ ...RELEASE_CONNECTION,
9320
+ ...overrides,
9321
+ ...this.environment
9322
+ }
9323
+ };
9324
+ }
9325
+ stateRoot(settings = this.view().effective) {
9326
+ if (this.environment.apiUrl && this.environment.issuer) return this.root;
9327
+ return connectionStateRoot(this.root, settings);
9328
+ }
9329
+ save(value) {
9330
+ const overrides = validateConnectionOverrides(value);
9331
+ const current = this.view();
9332
+ for (const key of KEYS) {
9333
+ if (this.environment[key] !== void 0 && overrides[key] !== current.overrides[key]) throw new Error(`${key} is managed by the launch environment`);
9334
+ if (overrides[key] === RELEASE_CONNECTION[key]) delete overrides[key];
9335
+ }
9336
+ mkdirSync(this.root, {
9337
+ recursive: true,
9338
+ mode: 448
9339
+ });
9340
+ const effective = {
9341
+ ...RELEASE_CONNECTION,
9342
+ ...overrides,
9343
+ ...this.environment
9344
+ };
9345
+ rmSync(join(this.stateRoot(effective), "operator.json"), { force: true });
9346
+ const temporary = `${this.path}.${randomUUID()}.tmp`;
9347
+ writeFileSync(temporary, JSON.stringify(overrides, null, 2) + "\n", {
9348
+ mode: 384,
9349
+ flag: "wx"
9350
+ });
9351
+ try {
9352
+ renameSync(temporary, this.path);
9353
+ } finally {
9354
+ rmSync(temporary, { force: true });
9355
+ }
9356
+ return this.view();
9357
+ }
9358
+ };
9359
+ /** A custom service gets its own identities, keys and runtime configuration. */
9360
+ function connectionStateRoot(root, settings) {
9361
+ const identity = [settings.apiUrl.replace(/\/$/u, ""), settings.issuer.replace(/\/$/u, "")];
9362
+ if (identity[0] === RELEASE_CONNECTION.apiUrl && identity[1] === RELEASE_CONNECTION.issuer) return root;
9363
+ return join(root, "environments", createHash("sha256").update(JSON.stringify(identity)).digest("hex"));
8913
9364
  }
8914
9365
  //#endregion
8915
9366
  //#region src/lib/agent-server/lock.ts
@@ -8968,169 +9419,279 @@ async function withAgentServerLock(root, work, options) {
8968
9419
  }
8969
9420
  }
8970
9421
  //#endregion
8971
- //#region src/lib/agent-server/pairing.ts
9422
+ //#region src/lib/agent-server/native-grant.ts
8972
9423
  /**
8973
- * One-click pairing ceremony for the agent server, mirroring the signer's
8974
- * session/ceremony pattern (#2062 design):
9424
+ * Environment variable the supervising desktop app uses to hand this server
9425
+ * process its control token.
8975
9426
  *
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.
9427
+ * The parent generates the token, passes it here, and keeps it in native
9428
+ * memory — it is never written to disk, never printed to stdout (the desktop
9429
+ * app surfaces server output in its WebView), and never reaches the renderer.
9430
+ */
9431
+ var NATIVE_TOKEN_ENV = "MOLTNET_AGENT_SERVER_NATIVE_TOKEN";
9432
+ /** 32 bytes of entropy, base64url-encoded, is 43 characters. */
9433
+ var MIN_TOKEN_LENGTH = 32;
9434
+ /**
9435
+ * Consume the supervisor's native token from the environment and grant it.
9436
+ *
9437
+ * Consuming matters as much as granting: spawned run children inherit this
9438
+ * process environment, so a token left in place would hand every
9439
+ * task-executing agent control of the Agent Server supervising it. The
9440
+ * variable is deleted whether or not the value turns out to be usable.
8985
9441
  *
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.
9442
+ * @returns whether a native grant was issued.
8992
9443
  */
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;
9444
+ function applyNativeClientGrant(options) {
9445
+ const { nativeGrant, env } = options;
9446
+ const token = env[NATIVE_TOKEN_ENV];
9447
+ delete env[NATIVE_TOKEN_ENV];
9448
+ if (typeof token !== "string" || token.length === 0) return false;
9449
+ if (token.length < MIN_TOKEN_LENGTH) throw new Error(`${NATIVE_TOKEN_ENV} must be at least 32 characters of unguessable entropy`);
9450
+ nativeGrant.grantNative(token);
9451
+ return true;
9452
+ }
9453
+ var NativeGrantError = class extends Error {
9454
+ code = "native_token_invalid";
9455
+ };
9456
+ var NativeGrantService = class {
9457
+ digest;
9458
+ grantNative(token) {
9459
+ if (!token) throw new NativeGrantError("Native token must not be empty");
9460
+ this.digest = createHash("sha256").update(token).digest();
9461
+ }
9462
+ verify(origin, token) {
9463
+ const digest = createHash("sha256").update(token).digest();
9464
+ if (origin !== "moltnet-agent-desktop://native" || !this.digest || !timingSafeEqual(this.digest, digest)) throw new NativeGrantError("Native token is not valid");
8999
9465
  }
9000
9466
  };
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;
9467
+ //#endregion
9468
+ //#region src/lib/agent-server/native-socket.ts
9469
+ /** The supervisor owns the private directory; the server never removes peers. */
9470
+ async function validateNativeSocket(path) {
9471
+ if (!isAbsolute(path) || Buffer.byteLength(path) > 100) throw new Error("Native socket must be an absolute path of at most 100 bytes");
9472
+ const parent = dirname(path);
9473
+ const metadata = await lstat(parent);
9474
+ if (metadata.isSymbolicLink()) throw new Error(`Native socket parent contains a symlink: ${parent}`);
9475
+ if (!metadata.isDirectory()) throw new Error(`Native socket parent is not a directory: ${parent}`);
9476
+ const expectedUid = process.getuid?.();
9477
+ if (expectedUid === void 0 || metadata.uid !== expectedUid) throw new Error(`Native socket parent has uid ${metadata.uid}; expected ${String(expectedUid)}: ${parent}`);
9478
+ const mode = metadata.mode & 511;
9479
+ if (mode !== 448) throw new Error(`Native socket parent has mode ${mode.toString(8)}; expected 700: ${parent}`);
9480
+ const canonical = await realpath(parent);
9481
+ if (canonical !== resolve(parent)) throw new Error(`Native socket parent contains a symlink: ${parent} resolves to ${canonical}`);
9482
+ try {
9483
+ await lstat(path);
9484
+ } catch (error) {
9485
+ if (error.code === "ENOENT") return;
9486
+ throw error;
9014
9487
  }
9015
- now() {
9016
- return this.options.now?.() ?? Date.now();
9488
+ throw new Error("Native socket path already exists; it was left untouched");
9489
+ }
9490
+ //#endregion
9491
+ //#region src/lib/agent-server/operator-oauth.ts
9492
+ var InvalidOperatorGrantError = class extends Error {};
9493
+ var LOCAL_SCOPE = OPERATOR_OAUTH.localControlScope;
9494
+ /** Trusted native controller owns the verifier, callback and token exchange. */
9495
+ var OperatorOAuth = class {
9496
+ instance = randomUUID();
9497
+ keys;
9498
+ active = false;
9499
+ pending;
9500
+ operator;
9501
+ constructor(config, root, openBrowser = (url) => {
9502
+ const command = process.platform === "darwin" ? "open" : process.platform === "win32" ? "explorer.exe" : "xdg-open";
9503
+ return new Promise((resolve, reject) => {
9504
+ execFile(command, [url], (error) => {
9505
+ if (error) reject(/* @__PURE__ */ new Error("Could not open Console approval"));
9506
+ else resolve();
9507
+ });
9508
+ });
9509
+ }) {
9510
+ this.config = config;
9511
+ this.root = root;
9512
+ this.openBrowser = openBrowser;
9513
+ const endpoints = [
9514
+ config.authorizationUrl,
9515
+ config.tokenUrl,
9516
+ config.jwksUrl
9517
+ ].map((value) => new URL(value));
9518
+ 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");
9519
+ this.keys = createRemoteJWKSet(new URL(config.jwksUrl));
9520
+ try {
9521
+ const value = JSON.parse(readFileSync(join(root, "operator.json"), "utf8"));
9522
+ if (!value || typeof value !== "object" || !("issuer" in value) || !("subject" in value) || typeof value.issuer !== "string" || typeof value.subject !== "string") throw new Error("Invalid operator");
9523
+ this.operator = {
9524
+ issuer: value.issuer,
9525
+ subject: value.subject
9526
+ };
9527
+ } catch (error) {
9528
+ if (error.code !== "ENOENT") throw error;
9529
+ this.operator = null;
9530
+ }
9017
9531
  }
9018
- token() {
9019
- return this.options.randomToken?.() ?? randomBytes(32).toString("base64url");
9532
+ cancel() {
9533
+ this.pending?.abort();
9020
9534
  }
9021
- sweep() {
9022
- const now = this.now();
9023
- for (const [id, pairing] of this.pending) if (pairing.expiresAt <= now) this.pending.delete(id);
9535
+ removeOperator() {
9536
+ this.cancel();
9537
+ rmSync(join(this.root, "operator.json"), { force: true });
9538
+ this.operator = null;
9024
9539
  }
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
- });
9540
+ metadata() {
9035
9541
  return {
9036
- pairingId,
9037
- approvalPath: `/pairings/${pairingId}`
9542
+ protocolVersion: OPERATOR_OAUTH.protocolVersion,
9543
+ instance: this.instance,
9544
+ issuer: this.config.issuer,
9545
+ authorizationUrl: this.config.authorizationUrl,
9546
+ tokenUrl: this.config.tokenUrl,
9547
+ clientId: this.config.consoleClientId,
9548
+ operatorConfigured: !!this.operator
9038
9549
  };
9039
9550
  }
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");
9551
+ async verify(token, scope, clientId) {
9552
+ const { payload } = await jwtVerify(token, this.keys, {
9553
+ algorithms: ["RS256"],
9554
+ issuer: this.config.issuer,
9555
+ audience: scope === LOCAL_SCOPE ? OPERATOR_OAUTH.localControlAudience : OPERATOR_OAUTH.provisioningAudience,
9556
+ requiredClaims: [
9557
+ "exp",
9558
+ "iat",
9559
+ "sub"
9560
+ ],
9561
+ maxTokenAge: clientId === this.config.nativeClientId ? OPERATOR_OAUTH.nativeLifetimeSeconds : OPERATOR_OAUTH.consoleLifetimeSeconds
9562
+ });
9563
+ const claims = payload.ext;
9564
+ const scopes = typeof payload.scope === "string" ? payload.scope.split(" ") : payload.scp;
9565
+ 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
9566
  return {
9045
- origin: pairing.origin,
9046
- confirmToken: pairing.confirmToken
9567
+ issuer: payload.iss,
9568
+ subject: payload.sub,
9569
+ provisioning: claims["moltnet:provisioning"]
9047
9570
  };
9048
9571
  }
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;
9572
+ async verifyBrowser(token) {
9573
+ const operator = await this.verify(token, LOCAL_SCOPE, this.config.consoleClientId);
9574
+ if (!this.operator || operator.issuer !== this.operator.issuer || operator.subject !== this.operator.subject) throw new InvalidOperatorGrantError("Native operator sign-in required");
9575
+ }
9576
+ async authorize(grant, signal) {
9577
+ if (this.active) throw new Error("An approval is already pending");
9578
+ this.active = true;
9579
+ const state = randomBytes(32).toString("base64url");
9580
+ const verifier = randomBytes(32).toString("base64url");
9581
+ const callback = `http://127.0.0.1:${this.config.callbackPort}/oauth/callback`;
9582
+ const scope = grant ? OPERATOR_OAUTH.provisioningScope : LOCAL_SCOPE;
9583
+ const controller = new AbortController();
9584
+ this.pending = controller;
9585
+ const timeout = setTimeout(() => controller.abort(), OPERATOR_OAUTH.nativeLifetimeSeconds * 1e3);
9586
+ const abort = () => controller.abort();
9587
+ signal?.addEventListener("abort", abort, { once: true });
9588
+ let server;
9589
+ try {
9590
+ if (signal?.aborted) throw new Error("Approval cancelled");
9591
+ const code = await new Promise((resolve, reject) => {
9592
+ let consumed = false;
9593
+ server = createServer((req, res) => {
9594
+ const url = new URL(req.url ?? "/", callback);
9595
+ res.setHeader("Cache-Control", "no-store");
9596
+ res.setHeader("Content-Security-Policy", "default-src 'none'; frame-ancestors 'none'");
9597
+ 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) {
9598
+ res.writeHead(400);
9599
+ res.end("Invalid callback");
9600
+ return;
9601
+ }
9602
+ consumed = true;
9603
+ if (url.searchParams.has("error") || url.searchParams.getAll("code").length !== 1 || !url.searchParams.get("code")) {
9604
+ res.end("Approval cancelled. Return to Desktop.");
9605
+ reject(/* @__PURE__ */ new Error("Approval cancelled"));
9606
+ return;
9607
+ }
9608
+ res.end("Approval received. Return to Desktop.");
9609
+ resolve(url.searchParams.get("code"));
9610
+ });
9611
+ server.once("error", reject);
9612
+ controller.signal.addEventListener("abort", () => reject(/* @__PURE__ */ new Error("Approval cancelled")), { once: true });
9613
+ server.listen(this.config.callbackPort, "127.0.0.1", () => {
9614
+ const url = new URL(this.config.authorizationUrl);
9615
+ for (const [key, value] of Object.entries({
9616
+ client_id: this.config.nativeClientId,
9617
+ response_type: "code",
9618
+ scope,
9619
+ audience: grant ? OPERATOR_OAUTH.provisioningAudience : OPERATOR_OAUTH.localControlAudience,
9620
+ redirect_uri: callback,
9621
+ state,
9622
+ code_challenge_method: "S256",
9623
+ code_challenge: createHash("sha256").update(verifier).digest("base64url"),
9624
+ prompt: "consent",
9625
+ instance: this.instance,
9626
+ ...grant ? { provisioning: JSON.stringify(grant) } : {}
9627
+ })) url.searchParams.set(key, value);
9628
+ try {
9629
+ Promise.resolve(this.openBrowser(url.href)).catch(() => reject(/* @__PURE__ */ new Error("Could not open Console approval")));
9630
+ } catch {
9631
+ reject(/* @__PURE__ */ new Error("Could not open Console approval"));
9632
+ }
9633
+ });
9634
+ });
9635
+ const response = await fetch(this.config.tokenUrl, {
9636
+ method: "POST",
9637
+ redirect: "error",
9638
+ signal: controller.signal,
9639
+ headers: { "content-type": "application/x-www-form-urlencoded" },
9640
+ body: new URLSearchParams({
9641
+ grant_type: "authorization_code",
9642
+ code,
9643
+ code_verifier: verifier,
9644
+ client_id: this.config.nativeClientId,
9645
+ redirect_uri: callback
9646
+ })
9647
+ });
9648
+ if (!response.ok) throw new Error("Approval exchange lost; request fresh approval");
9649
+ const tokens = await response.json();
9650
+ if (!tokens.access_token || tokens.refresh_token) throw new Error("Invalid approval token response");
9651
+ const operator = await this.verify(tokens.access_token, scope, this.config.nativeClientId);
9652
+ controller.signal.throwIfAborted();
9653
+ if (grant) {
9654
+ const actual = operator.provisioning;
9655
+ 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");
9656
+ }
9657
+ if (this.operator && (this.operator.issuer !== operator.issuer || this.operator.subject !== operator.subject)) throw new Error("Change the operator through native administration first");
9658
+ if (!this.operator) {
9659
+ try {
9660
+ writeFileSync(join(this.root, "operator.json"), JSON.stringify({
9661
+ issuer: operator.issuer,
9662
+ subject: operator.subject
9663
+ }), {
9664
+ mode: 384,
9665
+ flag: "wx"
9666
+ });
9667
+ } catch (error) {
9668
+ if (error.code !== "EEXIST") throw error;
9669
+ const pinned = JSON.parse(readFileSync(join(this.root, "operator.json"), "utf8"));
9670
+ 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");
9671
+ }
9672
+ this.operator = {
9673
+ issuer: operator.issuer,
9674
+ subject: operator.subject
9675
+ };
9676
+ }
9677
+ return tokens.access_token;
9678
+ } finally {
9679
+ clearTimeout(timeout);
9680
+ signal?.removeEventListener("abort", abort);
9681
+ server?.close();
9682
+ server?.closeAllConnections();
9683
+ this.active = false;
9684
+ this.pending = void 0;
9685
+ }
9074
9686
  }
9075
9687
  };
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
9688
  //#endregion
9128
9689
  //#region src/lib/agent-server/provider-login.ts
9129
9690
  /**
9130
9691
  * Subscription-provider OAuth brokering for Agent Server (#2061 slice 4).
9131
9692
  *
9132
- * The console clicks "Connect"; agent server runs the Pi OAuth flow host-side via
9133
- * `ModelRuntime.login()` (which owns persistence into the shared
9693
+ * An authorized controller starts the flow; Agent Server runs Pi OAuth
9694
+ * host-side via `ModelRuntime.login()` (which owns persistence into the shared
9134
9695
  * `pi/auth.json` and token rotation thereafter). The browser only ever sees
9135
9696
  * the provider's authorize URL or device code — never tokens.
9136
9697
  *
@@ -9669,53 +10230,6 @@ async function attachExternalAgent(store, secretProviders, input, connectAgent =
9669
10230
  releaseAlias();
9670
10231
  }
9671
10232
  }
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
10233
  /** Never let request or activation metadata redirect persisted credentials. */
9720
10234
  function requireTrustedApiOverride(override, configApiUrl, configPath) {
9721
10235
  if (!override) return configApiUrl;
@@ -9854,6 +10368,129 @@ function requireActivation(store, alias) {
9854
10368
  if (!activation) throw new AgentServerStoreError("not_found", `agent "${alias}" is not configured`);
9855
10369
  return activation;
9856
10370
  }
10371
+ /** Enrollment may recover a central identity before its first online activation. */
10372
+ async function loadEnrollmentIdentity(store, alias) {
10373
+ if (store.readActivation(alias)) return loadAgentActivation(store, alias);
10374
+ const configPath = store.agentPath(alias);
10375
+ const config = await readCurrentConfig(configPath);
10376
+ if (!isCanonicalConfig(config)) throw new AgentServerIdentityError("verification_failed", "Enrollment requires a canonical agent identity");
10377
+ const apiUrl = requireTrustedConfigApiUrl(config, configPath);
10378
+ return {
10379
+ config,
10380
+ activation: {
10381
+ source: "external",
10382
+ alias,
10383
+ configPath,
10384
+ configApiUrl: apiUrl,
10385
+ apiUrl,
10386
+ subjectId: config.subject_id,
10387
+ ...identityFromConfig(config),
10388
+ createdAt: config.registered_at
10389
+ }
10390
+ };
10391
+ }
10392
+ /** Resolve pinned local identity state without requiring a live API credential. */
10393
+ async function loadAgentActivation(store, alias) {
10394
+ const activation = requireActivation(store, alias);
10395
+ const configPath = activation.source === "managed" ? store.agentPath(alias) : activation.configPath;
10396
+ if (activation.source === "external" && configPath !== store.agentPath(alias)) externalAgentLocation(configPath);
10397
+ const config = await readCurrentConfig(configPath);
10398
+ const apiUrl = activation.source === "managed" ? activation.apiUrl : activation.configApiUrl;
10399
+ assertActivatedConfig(config, activation, configPath, requireTrustedConfigApiUrl(config, configPath), apiUrl);
10400
+ requireTrustedApiOverride(activation.apiUrl, apiUrl, configPath);
10401
+ return {
10402
+ activation,
10403
+ config
10404
+ };
10405
+ }
10406
+ //#endregion
10407
+ //#region src/lib/agent-server/team-credentials.ts
10408
+ var AGENT_SERVER_REQUIRED_SCOPES = [
10409
+ ...DAEMON_MINIMUM_SCOPES,
10410
+ "team:read",
10411
+ "diary:read"
10412
+ ];
10413
+ var TeamCredentialError = class extends Error {
10414
+ constructor(blocker) {
10415
+ super(blocker.message);
10416
+ this.blocker = blocker;
10417
+ }
10418
+ };
10419
+ function credentialBlocker(error) {
10420
+ return error instanceof TeamCredentialError ? error.blocker : {
10421
+ code: "agent_key_unavailable",
10422
+ message: "This team credential could not be verified or read its team resources.",
10423
+ remedy: "Check connectivity and team access, or renew this team credential."
10424
+ };
10425
+ }
10426
+ var snapshots = /* @__PURE__ */ new WeakMap();
10427
+ /** Capture is internal to the verifier; exported for injected verifier test doubles. */
10428
+ function captureTeamCredential(agent, snapshot) {
10429
+ snapshots.set(agent, snapshot);
10430
+ return agent;
10431
+ }
10432
+ function requireCredentialSnapshot(agent) {
10433
+ const snapshot = snapshots.get(agent);
10434
+ if (!snapshot) throw new Error("A verified team credential snapshot is required");
10435
+ return snapshot;
10436
+ }
10437
+ /** The only supervised credential path. No fallback reference or OAuth resolution. */
10438
+ async function verifyTeamActivation(store, alias, managed, external, connectImpl = connect, signal, teamId) {
10439
+ const activated = await loadAgentActivation(store, alias);
10440
+ const { config, activation } = activated;
10441
+ const reference = teamId ? config.agent_key_refs?.[teamId] : void 0;
10442
+ if (!teamId || !reference) throw new TeamCredentialError({
10443
+ code: "agent_key_missing",
10444
+ message: "No credential is indexed for this team.",
10445
+ remedy: "Enroll into this team or explicitly index its existing team-bound credential."
10446
+ });
10447
+ let agentKey;
10448
+ try {
10449
+ const resolved = await resolveAgentKey({
10450
+ ...config,
10451
+ agent_key_ref: void 0,
10452
+ agent_key_refs: { [teamId]: reference }
10453
+ }, activation.source === "managed" ? managed : external, teamId);
10454
+ if (!resolved) throw new Error("Missing selected key");
10455
+ agentKey = resolved;
10456
+ } catch {
10457
+ throw new TeamCredentialError({
10458
+ code: "agent_key_unavailable",
10459
+ message: "The selected team credential is unavailable.",
10460
+ remedy: "Repair its secret provider or renew this team credential."
10461
+ });
10462
+ }
10463
+ const client = await connectImpl({
10464
+ agentKey,
10465
+ apiUrl: activation.apiUrl ?? (activation.source === "external" ? activation.configApiUrl : void 0),
10466
+ signal
10467
+ });
10468
+ const whoami = await client.agents.whoami({ signal });
10469
+ 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({
10470
+ code: "agent_key_binding_invalid",
10471
+ message: "The selected credential does not match this identity and team.",
10472
+ remedy: "Renew the selected team credential."
10473
+ });
10474
+ const metadata = {
10475
+ keyId: whoami.credentialBinding.keyId,
10476
+ ...Object.hasOwn(whoami.credentialBinding, "expiresAt") ? { expiresAt: whoami.credentialBinding.expiresAt } : {},
10477
+ scopes: [...whoami.scopes ?? []],
10478
+ verifiedAt: (/* @__PURE__ */ new Date()).toISOString()
10479
+ };
10480
+ store.writeCredentialMetadata(alias, teamId, metadata);
10481
+ const missing = AGENT_SERVER_REQUIRED_SCOPES.filter((scope) => !metadata.scopes.includes(scope));
10482
+ if (missing.length) throw new TeamCredentialError({
10483
+ code: "agent_key_scopes_insufficient",
10484
+ message: `This credential lacks ${missing.join(", ")}.`,
10485
+ remedy: "Renew through Console approval with the required desktop scopes."
10486
+ });
10487
+ activated.boundTeamId = teamId;
10488
+ return captureTeamCredential(activated, {
10489
+ agentKey,
10490
+ client,
10491
+ metadata
10492
+ });
10493
+ }
9857
10494
  //#endregion
9858
10495
  //#region src/lib/agent-server/runs.ts
9859
10496
  var STOP_GRACE_MS = 1e4;
@@ -9893,6 +10530,19 @@ var INHERITED_MOLTNET_ENV_NAMES = new Set([
9893
10530
  "MOLTNET_SIGNER_URL",
9894
10531
  "MOLTNET_TRACE_IDLE_POLLING"
9895
10532
  ]);
10533
+ /** The runtime kind bundled with the agent; needs no registration. */
10534
+ var BUILT_IN_RUNTIME_KIND = "gondolin_pi";
10535
+ /** Persist only process status; worker stderr can contain provider secrets. */
10536
+ function describeFailure(code, signal) {
10537
+ if (signal) return {
10538
+ code: "run_signalled",
10539
+ message: `The worker was stopped by ${signal}.`
10540
+ };
10541
+ return {
10542
+ code: "run_failed",
10543
+ message: `The worker exited with code ${code ?? "unknown"}. Open the log for detail.`
10544
+ };
10545
+ }
9896
10546
  var AgentServerRunError = class extends Error {
9897
10547
  name = "AgentServerRunError";
9898
10548
  constructor(code, message) {
@@ -9975,23 +10625,16 @@ var RunManager = class {
9975
10625
  String(runtimeSettings.warmRetentionSec),
9976
10626
  ...target.extraArgs
9977
10627
  ];
10628
+ env["MOLTNET_AGENT_KEY"] = requireCredentialSnapshot(agent).agentKey;
10629
+ env["MOLTNET_API_URL"] = activation.apiUrl ?? (activation.source === "external" ? activation.configApiUrl : "");
9978
10630
  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);
10631
+ if (!config.keys.private_key_ref) throw new AgentServerRunError("invalid_spec", "The managed signing key reference is missing");
9983
10632
  env["MOLTNET_PRIVATE_KEY_REF"] = formatSecretReferenceString(config.keys.private_key_ref);
9984
10633
  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
- }
10634
+ } else try {
10635
+ env["MOLTNET_PRIVATE_KEY"] = await resolveIdentitySeed(config, this.options.externalSecretProviders);
10636
+ } catch {
10637
+ throw new AgentServerRunError("invalid_spec", "The selected signing key could not be projected");
9995
10638
  }
9996
10639
  env["MOLTNET_EXPECTED_SUBJECT_ID"] = activation.subjectId;
9997
10640
  env["MOLTNET_EXPECTED_SUBJECT_TYPE"] = "agent";
@@ -10025,9 +10668,9 @@ var RunManager = class {
10025
10668
  }
10026
10669
  async startReserved(spec, signal) {
10027
10670
  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;
10671
+ const agent = await (this.options.verifyActivationImpl ?? verifyTeamActivation)(this.store, spec.agent, this.options.secretProviders, this.options.externalSecretProviders, void 0, signal, spec.teamId).catch((cause) => {
10672
+ if (cause instanceof TeamCredentialError || cause instanceof AgentServerStoreError) throw cause;
10673
+ 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
10674
  });
10032
10675
  this.assertStartOpen(signal);
10033
10676
  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 +10747,8 @@ var RunManager = class {
10104
10747
  id,
10105
10748
  status: "running",
10106
10749
  pid: child.pid,
10107
- startedAt: (this.options.now?.() ?? /* @__PURE__ */ new Date()).toISOString()
10750
+ startedAt: (this.options.now?.() ?? /* @__PURE__ */ new Date()).toISOString(),
10751
+ credential: requireCredentialSnapshot(agent).metadata
10108
10752
  };
10109
10753
  child.once("exit", (code, signal) => {
10110
10754
  const activeRun = this.active.get(id);
@@ -10119,7 +10763,8 @@ var RunManager = class {
10119
10763
  });
10120
10764
  this.persistRunCompletion(id, spec.agent, {
10121
10765
  status,
10122
- exitCode: code
10766
+ exitCode: code,
10767
+ ...status === "failed" ? { lastError: describeFailure(code, signal) } : {}
10123
10768
  });
10124
10769
  });
10125
10770
  child.once("error", (error) => {
@@ -10130,7 +10775,13 @@ var RunManager = class {
10130
10775
  transition: "spawn_failed",
10131
10776
  ...safeRunError(error)
10132
10777
  });
10133
- this.persistRunCompletion(id, spec.agent, { status: "failed" });
10778
+ this.persistRunCompletion(id, spec.agent, {
10779
+ status: "failed",
10780
+ lastError: {
10781
+ code: "spawn_failed",
10782
+ message: `The worker could not be started: ${error.message}`
10783
+ }
10784
+ });
10134
10785
  });
10135
10786
  this.store.writeRun(record);
10136
10787
  this.log("info", "agent server run started", {
@@ -10157,7 +10808,7 @@ var RunManager = class {
10157
10808
  }
10158
10809
  async resolveRuntimeModule(spec, activated, cwd) {
10159
10810
  const profiles = await resolveRuntimeProfiles({
10160
- agent: await this.connectActivatedAgent(activated, spec.teamId),
10811
+ agent: await this.connectAgent(activated, spec.teamId),
10161
10812
  profiles: spec.profiles,
10162
10813
  teamId: spec.teamId,
10163
10814
  cwd
@@ -10179,14 +10830,9 @@ var RunManager = class {
10179
10830
  if (kind === "gondolin_pi") return void 0;
10180
10831
  throw new AgentServerRunError("invalid_spec", `No local runtime is registered for profile kind "${kind}".`);
10181
10832
  }
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
- });
10833
+ connectAgent(activated, teamId) {
10834
+ if (activated.boundTeamId !== teamId) throw new AgentServerRunError("invalid_spec", "Snapshot team mismatch");
10835
+ return Promise.resolve(requireCredentialSnapshot(activated).client);
10190
10836
  }
10191
10837
  stop(id) {
10192
10838
  const record = this.store.readRun(id);
@@ -10211,6 +10857,22 @@ var RunManager = class {
10211
10857
  if (!record) throw new AgentServerStoreError("not_found", `run "${id}" was not found`);
10212
10858
  return record;
10213
10859
  }
10860
+ async listAsync(limit) {
10861
+ const activeIds = new Set(this.active.keys());
10862
+ const records = await this.store.listRunsAsync(limit + activeIds.size, [...activeIds]);
10863
+ return [...records.filter((record) => activeIds.has(record.id)), ...records.filter((record) => !activeIds.has(record.id)).slice(0, limit)];
10864
+ }
10865
+ /** Freeze new starts only after local configuration has been persisted. */
10866
+ prepareServerRestart(persist) {
10867
+ if (this.closing || this.starting > 0 || this.active.size > 0) throw new AgentServerRunError("invalid_spec", "Stop running or starting work before changing connection settings");
10868
+ this.closing = true;
10869
+ try {
10870
+ return persist();
10871
+ } catch (error) {
10872
+ this.closing = false;
10873
+ throw error;
10874
+ }
10875
+ }
10214
10876
  list(limit = Number.POSITIVE_INFINITY) {
10215
10877
  if (!Number.isFinite(limit)) return this.store.listRuns();
10216
10878
  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 +11129,21 @@ var LOCKFILE_NAMES = [
10467
11129
  ];
10468
11130
  /** Local, operator-owned allowlist for executable daemon runtime modules. */
10469
11131
  var RuntimeRegistry = class {
11132
+ displayDigests = /* @__PURE__ */ new Map();
11133
+ displayHash(path) {
11134
+ const stat = statSync(path);
11135
+ const key = String(path);
11136
+ const stamp = `${stat.dev}:${stat.ino}:${stat.size}:${stat.mtimeMs}:${stat.ctimeMs}`;
11137
+ const previous = this.displayDigests.get(key);
11138
+ if (previous?.stamp === stamp) return previous.hash;
11139
+ const hash = hashPath(path);
11140
+ if (this.displayDigests.size >= 128) this.displayDigests.clear();
11141
+ this.displayDigests.set(key, {
11142
+ stamp,
11143
+ hash
11144
+ });
11145
+ return hash;
11146
+ }
10470
11147
  constructor(root) {
10471
11148
  this.root = root;
10472
11149
  }
@@ -10489,7 +11166,7 @@ var RuntimeRegistry = class {
10489
11166
  const adapter = await loadDaemonRuntimeAdapter(specifier, { cwd });
10490
11167
  if (adapter.runtimeKind !== kind) throw new Error(`Runtime module provides "${adapter.runtimeKind}", not registered kind "${kind}".`);
10491
11168
  if (!moduleUrl.startsWith("file:")) throw new Error("Runtime registration must resolve to a local file URL.");
10492
- const entryHash = hashFile(new URL(moduleUrl));
11169
+ const entryHash = hashPath(new URL(moduleUrl));
10493
11170
  const lockfilePath = isPackageSpecifier(specifier) ? findLockfile(cwd) : void 0;
10494
11171
  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
11172
  const entry = {
@@ -10515,18 +11192,16 @@ var RuntimeRegistry = class {
10515
11192
  writeRegistry(this.path, next);
10516
11193
  return true;
10517
11194
  }
10518
- resolve(kind) {
11195
+ resolve(kind, options = {}) {
10519
11196
  kind = assertStoreName("runtime kind", kind);
10520
11197
  const entry = this.list().find((candidate) => candidate.kind === kind);
10521
11198
  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.`);
11199
+ const digest = (path) => options.forDisplay ? this.displayHash(path) : hashPath(path);
11200
+ if (digest(new URL(entry.moduleUrl)) !== entry.entryHash) throw new Error(`Registered runtime "${kind}" has changed; re-register it before starting a run.`);
11201
+ 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
11202
  return entry;
10525
11203
  }
10526
11204
  };
10527
- function hashFile(url) {
10528
- return createHash("sha256").update(readFileSync(url)).digest("hex");
10529
- }
10530
11205
  function hashPath(path) {
10531
11206
  return createHash("sha256").update(readFileSync(path)).digest("hex");
10532
11207
  }
@@ -10556,6 +11231,229 @@ function writeRegistry(path, entries) {
10556
11231
  renameSync(temp, path);
10557
11232
  }
10558
11233
  //#endregion
11234
+ //#region src/lib/agent-server/readiness.ts
11235
+ /**
11236
+ * Whether a runtime profile can execute on *this* machine.
11237
+ *
11238
+ * A profile is authored in Console against a team; whether it can run depends
11239
+ * on local facts only the server knows — which provider keys are configured
11240
+ * here and which runtime kinds this machine can produce.
11241
+ *
11242
+ * The prerequisite comparison itself is **not** reimplemented here. Run start
11243
+ * calls `validateRuntimeProfilePrerequisites`, and the catalogue calls the same
11244
+ * function, so the composer cannot promise a run that startup would reject for
11245
+ * a reason the two evaluated differently.
11246
+ */
11247
+ function deriveProfileReadiness(profile, machine) {
11248
+ const blockers = [];
11249
+ const env = {};
11250
+ for (const [name, configured] of machine.providerEnv) if (configured) env[name] = "configured";
11251
+ try {
11252
+ validateRuntimeProfilePrerequisites(profile, env, {
11253
+ tools: machine.inventory?.tools ?? profile.requiredTools,
11254
+ executables: machine.inventory?.executables ?? profile.requiredExecutables
11255
+ });
11256
+ } catch (error) {
11257
+ if (!(error instanceof RuntimeProfilePrerequisiteError)) throw error;
11258
+ for (const name of error.missingEnv) blockers.push({
11259
+ code: "env_missing",
11260
+ message: `${name} is not configured on this machine.`,
11261
+ remedy: "Add the key under Providers, then reopen this run."
11262
+ });
11263
+ for (const name of error.missingTools) blockers.push({
11264
+ code: "tool_missing",
11265
+ message: `The runtime does not provide the tool ${name}.`,
11266
+ remedy: `Use a profile whose runtime provides ${name}, or change the profile in Console.`
11267
+ });
11268
+ for (const name of error.missingExecutables) blockers.push({
11269
+ code: "executable_missing",
11270
+ message: `The runtime does not provide the executable ${name}.`,
11271
+ remedy: `Use a runtime that ships ${name}, or drop the requirement in Console.`
11272
+ });
11273
+ }
11274
+ if (!machine.runtimeKinds.has(profile.runtimeKind)) blockers.push({
11275
+ code: "runtime_unregistered",
11276
+ message: `Runtime kind ${profile.runtimeKind} is not available on this machine.`,
11277
+ remedy: "Register the runtime under Runtimes, then reopen this run."
11278
+ });
11279
+ return {
11280
+ ready: blockers.length === 0,
11281
+ blockers
11282
+ };
11283
+ }
11284
+ //#endregion
11285
+ //#region src/lib/agent-server/catalogue.ts
11286
+ async function buildCatalogue(options) {
11287
+ const { agent, machine, identityDefault } = options;
11288
+ const entries = await Promise.all(agent.teamIds.map(async (teamId) => {
11289
+ try {
11290
+ const result = await agent.readTeam(teamId);
11291
+ if (result.team.id !== teamId) throw new Error("Team response mismatch");
11292
+ const diaries = result.diaries.filter((diary) => diary.teamId === teamId).map(({ id, name }) => ({
11293
+ id,
11294
+ name
11295
+ }));
11296
+ return {
11297
+ team: {
11298
+ teamId,
11299
+ teamName: result.team.name,
11300
+ available: true,
11301
+ blockers: [],
11302
+ credential: result.credential,
11303
+ diaries,
11304
+ defaultDiaryId: resolveDefaultDiary(teamId, diaries, identityDefault)
11305
+ },
11306
+ profiles: result.profiles.filter((profile) => profile.teamId === teamId).map((profile) => ({
11307
+ ...profile,
11308
+ ...deriveProfileReadiness(profile, machine)
11309
+ }))
11310
+ };
11311
+ } catch (error) {
11312
+ return {
11313
+ team: {
11314
+ teamId,
11315
+ teamName: teamId,
11316
+ available: false,
11317
+ blockers: [credentialBlocker(error)],
11318
+ credential: agent.lastVerified(teamId),
11319
+ diaries: [],
11320
+ defaultDiaryId: null
11321
+ },
11322
+ profiles: []
11323
+ };
11324
+ }
11325
+ }));
11326
+ const teams = entries.map(({ team }) => team);
11327
+ const available = teams.filter((team) => team.available);
11328
+ return {
11329
+ teams,
11330
+ defaultTeamId: available.find((team) => team.teamId === identityDefault.teamId)?.teamId ?? available[0]?.teamId ?? null,
11331
+ profiles: entries.flatMap((entry) => entry.profiles)
11332
+ };
11333
+ }
11334
+ function resolveDefaultDiary(teamId, diaries, identityDefault) {
11335
+ const bound = diaries.find((diary) => diary.id === identityDefault.diaryId);
11336
+ if (bound && identityDefault.teamId === teamId) return bound.id;
11337
+ return diaries.length === 1 ? diaries[0]?.id ?? null : null;
11338
+ }
11339
+ //#endregion
11340
+ //#region src/lib/agent-server/enrollment.ts
11341
+ /** Native callers receive metadata only; approval and storage stay local. */
11342
+ async function enrollIdentityTeam(options) {
11343
+ const apiUrl = new URL(options.apiUrl);
11344
+ 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");
11345
+ const { activation, config } = await loadEnrollmentIdentity(options.store, options.alias);
11346
+ const identityApiUrl = activation.apiUrl ?? config.endpoints?.api;
11347
+ if (!identityApiUrl) throw new Error("The identity has no API environment configured");
11348
+ 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.");
11349
+ const registry = activation.source === "managed" ? options.managed : options.external;
11350
+ const replacement = options.input.mode === "replace" ? { teamId: options.input.teamId } : void 0;
11351
+ const providerName = replacement ? config.agent_key_refs?.[replacement.teamId]?.provider : activation.source === "managed" ? "file" : config.keys.private_key_ref?.provider;
11352
+ const provider = providerName ? registry.get(providerName) : void 0;
11353
+ if (!provider?.capabilities.write) throw new Error("Enrollment requires a writable identity secret provider");
11354
+ const configPath = activation.source === "managed" ? options.store.agentPath(options.alias) : activation.configPath;
11355
+ try {
11356
+ const result = await enrollTeam({
11357
+ idempotencyKey: options.input.idempotencyKey,
11358
+ provisioningContext: {
11359
+ teamId: options.input.teamId,
11360
+ operation: replacement ? "renew" : "enroll",
11361
+ scopes: [...AGENT_SERVER_REQUIRED_SCOPES]
11362
+ },
11363
+ replacement,
11364
+ provision: async () => {
11365
+ const grant = {
11366
+ agentId: config.subject_id,
11367
+ teamId: options.input.teamId,
11368
+ operation: replacement ? "renew" : "enroll",
11369
+ scopes: [...AGENT_SERVER_REQUIRED_SCOPES],
11370
+ idempotencyKey: options.input.idempotencyKey
11371
+ };
11372
+ let token;
11373
+ let agentProof;
11374
+ try {
11375
+ token = await options.oauth.authorize(grant, options.signal);
11376
+ agentProof = replacement ? void 0 : await signBytes(Buffer.from(enrollmentProofMessage({
11377
+ accessToken: token,
11378
+ grant
11379
+ })).toString("base64"), dirname(configPath), registry);
11380
+ } catch (error) {
11381
+ throw new ProvisioningNotStartedError(error);
11382
+ }
11383
+ const response = await fetch(new URL("/oauth2/provision", options.apiUrl), {
11384
+ method: "POST",
11385
+ redirect: "error",
11386
+ signal: options.signal ? AbortSignal.any([options.signal, AbortSignal.timeout(3e4)]) : AbortSignal.timeout(3e4),
11387
+ headers: {
11388
+ authorization: `Bearer ${token}`,
11389
+ "content-type": "application/json"
11390
+ },
11391
+ body: JSON.stringify(agentProof ? { agentProof } : {})
11392
+ });
11393
+ if (!response.ok) throw new Error("Provisioning unavailable; inspect recovery before fresh approval");
11394
+ const agentKey = await response.json();
11395
+ return {
11396
+ teamId: options.input.teamId,
11397
+ role: "member",
11398
+ agentKey
11399
+ };
11400
+ },
11401
+ configDir: dirname(configPath),
11402
+ secretProvider: provider,
11403
+ apiUrl: options.apiUrl
11404
+ });
11405
+ if (!options.store.readActivation(options.alias)) options.store.writeActivation(activation);
11406
+ return {
11407
+ state: "persisted",
11408
+ teamId: result.teamId,
11409
+ keyId: result.key.id
11410
+ };
11411
+ } catch (error) {
11412
+ if (error instanceof ProvisioningNotStartedError) throw error;
11413
+ if (error instanceof CredentialPersistenceError || error instanceof EnrollmentRecoveryError) return {
11414
+ state: "recovery_required",
11415
+ secretCaptured: error.secretCaptured,
11416
+ ...error.issuedKeyId ? { issuedKeyId: error.issuedKeyId } : {},
11417
+ ...error.recoveryPath ? { recoveryId: basename(error.recoveryPath) } : {},
11418
+ 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."
11419
+ };
11420
+ throw new Error("Team enrollment could not be completed", { cause: error });
11421
+ }
11422
+ }
11423
+ //#endregion
11424
+ //#region src/lib/agent-server/identity-binding.ts
11425
+ /**
11426
+ * The identity-wide team/diary binding, read from `<identityDir>/env`.
11427
+ *
11428
+ * This mirrors the Go CLI's `identityDefaultBinding` (`project_selection.go`),
11429
+ * which is the fallback the CLI uses when a working directory has no
11430
+ * registered project binding.
11431
+ *
11432
+ * The desktop cannot use the CLI's *location* bindings at all — its composer
11433
+ * has no working directory to key on — but it can honour this identity-wide
11434
+ * default, so an operator sees their familiar team preselected rather than an
11435
+ * arbitrary first entry.
11436
+ *
11437
+ * Like the CLI, a half-filled pair is treated as no binding: a team without a
11438
+ * diary is ignored.
11439
+ */
11440
+ function readIdentityDefaultBinding(identityDir) {
11441
+ let contents;
11442
+ try {
11443
+ contents = readFileSync(join(identityDir, "env"), "utf8");
11444
+ } catch {
11445
+ return {};
11446
+ }
11447
+ const env = parseEnv(contents);
11448
+ const teamId = env["MOLTNET_TEAM_ID"]?.trim();
11449
+ const diaryId = env["MOLTNET_DIARY_ID"]?.trim();
11450
+ if (!teamId || !diaryId) return {};
11451
+ return {
11452
+ teamId,
11453
+ diaryId
11454
+ };
11455
+ }
11456
+ //#endregion
10559
11457
  //#region src/lib/agent-server/protocol.ts
10560
11458
  var DateTime = Type.String({ format: "date-time" });
10561
11459
  var StringList = Type.Array(Type.String());
@@ -10601,6 +11499,63 @@ var AgentServerProviderSchema = Type.Object({
10601
11499
  models: ProviderModelList,
10602
11500
  hasApiKey: Type.Boolean()
10603
11501
  }, { $id: "AgentServerProvider" });
11502
+ var CredentialMetadataSchema = Type.Object({
11503
+ keyId: Type.String(),
11504
+ expiresAt: Type.Optional(Type.Union([DateTime, Type.Null()])),
11505
+ verifiedAt: DateTime,
11506
+ scopes: StringList
11507
+ });
11508
+ var AgentServerCatalogueTeamSchema = Type.Object({
11509
+ teamId: Type.String(),
11510
+ teamName: Type.String(),
11511
+ available: Type.Boolean(),
11512
+ credential: Type.Optional(CredentialMetadataSchema),
11513
+ blockers: Type.Array(Type.Object({
11514
+ code: Type.String(),
11515
+ message: Type.String(),
11516
+ remedy: Type.String()
11517
+ })),
11518
+ diaries: Type.Array(Type.Object({
11519
+ id: Type.String(),
11520
+ name: Type.String()
11521
+ })),
11522
+ defaultDiaryId: Type.Union([Type.String(), Type.Null()])
11523
+ }, { $id: "AgentServerCatalogueTeam" });
11524
+ /**
11525
+ * Composed from the canonical `RuntimeProfile` schema rather than restated, so
11526
+ * the wire contract cannot drift from the profile the API serves — and so the
11527
+ * constrained fields keep their real unions instead of degrading to `string`.
11528
+ */
11529
+ var AgentServerCatalogueProfileSchema = Type.Intersect([Type.Pick(RuntimeProfile, [
11530
+ "id",
11531
+ "name",
11532
+ "teamId",
11533
+ "description",
11534
+ "provider",
11535
+ "model",
11536
+ "runtimeKind",
11537
+ "toolEnforcement",
11538
+ "defaultWorkspaceMode",
11539
+ "maxTurns",
11540
+ "revision",
11541
+ "definitionCid",
11542
+ "requiredEnv",
11543
+ "requiredTools",
11544
+ "requiredExecutables"
11545
+ ]), Type.Object({
11546
+ ready: Type.Boolean(),
11547
+ blockers: Type.Array(Type.Object({
11548
+ code: Type.String(),
11549
+ message: Type.String(),
11550
+ remedy: Type.String()
11551
+ }))
11552
+ })], { $id: "AgentServerCatalogueProfile" });
11553
+ var AgentServerCatalogueSchema = Type.Object({
11554
+ teams: Type.Array(schemaRef(AgentServerCatalogueTeamSchema)),
11555
+ defaultTeamId: Type.Union([Type.String(), Type.Null()]),
11556
+ profiles: Type.Array(schemaRef(AgentServerCatalogueProfileSchema))
11557
+ }, { $id: "AgentServerCatalogue" });
11558
+ var CatalogueQuerySchema = Type.Object({ identity: Type.String({ minLength: 1 }) });
10604
11559
  var AgentServerRunRecordSchema = Type.Object({
10605
11560
  id: Type.String(),
10606
11561
  agent: Type.String(),
@@ -10617,6 +11572,11 @@ var AgentServerRunRecordSchema = Type.Object({
10617
11572
  ]),
10618
11573
  pid: Type.Optional(Type.Number()),
10619
11574
  exitCode: Type.Optional(Type.Union([Type.Number(), Type.Null()])),
11575
+ credential: Type.Optional(CredentialMetadataSchema),
11576
+ lastError: Type.Optional(Type.Object({
11577
+ code: Type.String(),
11578
+ message: Type.String()
11579
+ })),
10620
11580
  startedAt: DateTime,
10621
11581
  endedAt: Type.Optional(DateTime)
10622
11582
  }, { $id: "AgentServerRunRecord" });
@@ -10653,12 +11613,6 @@ var AgentServerStatusSchema = Type.Object({
10653
11613
  warmRetentionSec: Type.Integer({ minimum: 0 })
10654
11614
  })
10655
11615
  }, { $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
11616
  var ProviderParamsSchema = Type.Object({ providerId: Type.String() });
10663
11617
  var AgentParamsSchema = Type.Object({ agentName: Type.String() });
10664
11618
  var RunParamsSchema = Type.Object({ runId: Type.String() });
@@ -10704,19 +11658,20 @@ var AGENT_SERVER_SCHEMAS = [
10704
11658
  AgentServerIdentitySchema,
10705
11659
  AgentServerTaskTypeSchema,
10706
11660
  AgentServerProviderSchema,
11661
+ AgentServerCatalogueTeamSchema,
11662
+ AgentServerCatalogueProfileSchema,
11663
+ AgentServerCatalogueSchema,
10707
11664
  AgentServerRunRecordSchema,
10708
11665
  AgentServerRunSchema,
10709
11666
  AgentServerSubscriptionSchema,
10710
11667
  AgentServerSubscriptionLoginSchema,
10711
11668
  AgentServerStatusSchema,
10712
- PairingStartedSchema,
10713
- PairingClaimedSchema,
10714
11669
  ReconcileAgentResultSchema,
10715
11670
  DiscoverModelsSchema,
10716
11671
  CancelledSubscriptionSchema,
10717
11672
  LogStreamSchema
10718
11673
  ];
10719
- var pairedSecurity = [{ agentServerToken: [] }];
11674
+ var localControlSecurity = [{ agentServerToken: [] }];
10720
11675
  var problemResponse = { default: schemaRef(AgentServerProblemSchema) };
10721
11676
  var AgentServerRouteSchemas = {
10722
11677
  health: {
@@ -10724,27 +11679,10 @@ var AgentServerRouteSchemas = {
10724
11679
  tags: ["system"],
10725
11680
  response: { 200: schemaRef(AgentServerHealthSchema) }
10726
11681
  },
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
11682
  status: {
10745
11683
  operationId: "getAgentServerStatus",
10746
11684
  tags: ["system"],
10747
- security: pairedSecurity,
11685
+ security: localControlSecurity,
10748
11686
  response: {
10749
11687
  200: schemaRef(AgentServerStatusSchema),
10750
11688
  ...problemResponse
@@ -10753,7 +11691,7 @@ var AgentServerRouteSchemas = {
10753
11691
  listAgents: {
10754
11692
  operationId: "listAgentServerAgents",
10755
11693
  tags: ["agents"],
10756
- security: pairedSecurity,
11694
+ security: localControlSecurity,
10757
11695
  response: {
10758
11696
  200: Type.Array(schemaRef(AgentServerAgentSchema)),
10759
11697
  ...problemResponse
@@ -10762,17 +11700,44 @@ var AgentServerRouteSchemas = {
10762
11700
  createAgent: {
10763
11701
  operationId: "createAgentServerAgent",
10764
11702
  tags: ["agents"],
10765
- security: pairedSecurity,
11703
+ security: localControlSecurity,
10766
11704
  body: CreateAgentSchema,
10767
11705
  response: {
10768
11706
  201: schemaRef(AgentServerAgentSchema),
10769
11707
  ...problemResponse
10770
11708
  }
10771
11709
  },
11710
+ enrollTeam: {
11711
+ operationId: "enrollAgentServerTeam",
11712
+ tags: ["agents"],
11713
+ security: localControlSecurity,
11714
+ params: AgentParamsSchema,
11715
+ body: Type.Intersect([Type.Object({
11716
+ teamId: Type.String({ format: "uuid" }),
11717
+ idempotencyKey: Type.String({
11718
+ minLength: 1,
11719
+ maxLength: 256
11720
+ })
11721
+ }), Type.Union([Type.Object({ mode: Type.Literal("enroll") }), Type.Object({ mode: Type.Literal("replace") })])]),
11722
+ response: {
11723
+ 200: Type.Union([Type.Object({
11724
+ state: Type.Literal("persisted"),
11725
+ teamId: Type.String(),
11726
+ keyId: Type.String()
11727
+ }), Type.Object({
11728
+ state: Type.Literal("recovery_required"),
11729
+ secretCaptured: Type.Boolean(),
11730
+ issuedKeyId: Type.Optional(Type.String()),
11731
+ recoveryId: Type.String(),
11732
+ message: Type.String()
11733
+ })]),
11734
+ ...problemResponse
11735
+ }
11736
+ },
10772
11737
  reconcileAgent: {
10773
11738
  operationId: "reconcileAgentServerAgent",
10774
11739
  tags: ["agents"],
10775
- security: pairedSecurity,
11740
+ security: localControlSecurity,
10776
11741
  params: AgentParamsSchema,
10777
11742
  body: ReconcileAgentSchema,
10778
11743
  response: {
@@ -10783,7 +11748,7 @@ var AgentServerRouteSchemas = {
10783
11748
  listProviders: {
10784
11749
  operationId: "listAgentServerProviders",
10785
11750
  tags: ["providers"],
10786
- security: pairedSecurity,
11751
+ security: localControlSecurity,
10787
11752
  response: {
10788
11753
  200: Type.Record(Type.String(), schemaRef(AgentServerProviderSchema)),
10789
11754
  ...problemResponse
@@ -10792,7 +11757,7 @@ var AgentServerRouteSchemas = {
10792
11757
  discoverModels: {
10793
11758
  operationId: "discoverAgentServerProviderModels",
10794
11759
  tags: ["providers"],
10795
- security: pairedSecurity,
11760
+ security: localControlSecurity,
10796
11761
  params: ProviderParamsSchema,
10797
11762
  response: {
10798
11763
  200: schemaRef(DiscoverModelsSchema),
@@ -10802,7 +11767,7 @@ var AgentServerRouteSchemas = {
10802
11767
  putProvider: {
10803
11768
  operationId: "putAgentServerProvider",
10804
11769
  tags: ["providers"],
10805
- security: pairedSecurity,
11770
+ security: localControlSecurity,
10806
11771
  params: ProviderParamsSchema,
10807
11772
  body: PutProviderSchema,
10808
11773
  response: {
@@ -10813,7 +11778,7 @@ var AgentServerRouteSchemas = {
10813
11778
  deleteProvider: {
10814
11779
  operationId: "deleteAgentServerProvider",
10815
11780
  tags: ["providers"],
10816
- security: pairedSecurity,
11781
+ security: localControlSecurity,
10817
11782
  params: ProviderParamsSchema,
10818
11783
  response: {
10819
11784
  204: Type.Any(),
@@ -10823,7 +11788,7 @@ var AgentServerRouteSchemas = {
10823
11788
  listSubscriptions: {
10824
11789
  operationId: "listAgentServerSubscriptions",
10825
11790
  tags: ["subscriptions"],
10826
- security: pairedSecurity,
11791
+ security: localControlSecurity,
10827
11792
  response: {
10828
11793
  200: Type.Array(schemaRef(AgentServerSubscriptionSchema)),
10829
11794
  ...problemResponse
@@ -10832,7 +11797,7 @@ var AgentServerRouteSchemas = {
10832
11797
  startSubscriptionLogin: {
10833
11798
  operationId: "startAgentServerSubscriptionLogin",
10834
11799
  tags: ["subscriptions"],
10835
- security: pairedSecurity,
11800
+ security: localControlSecurity,
10836
11801
  params: ProviderParamsSchema,
10837
11802
  response: {
10838
11803
  201: schemaRef(AgentServerSubscriptionLoginSchema),
@@ -10842,7 +11807,7 @@ var AgentServerRouteSchemas = {
10842
11807
  getSubscriptionLogin: {
10843
11808
  operationId: "getAgentServerSubscriptionLogin",
10844
11809
  tags: ["subscriptions"],
10845
- security: pairedSecurity,
11810
+ security: localControlSecurity,
10846
11811
  params: ProviderParamsSchema,
10847
11812
  response: {
10848
11813
  200: schemaRef(AgentServerSubscriptionLoginSchema),
@@ -10852,17 +11817,27 @@ var AgentServerRouteSchemas = {
10852
11817
  cancelSubscriptionLogin: {
10853
11818
  operationId: "cancelAgentServerSubscriptionLogin",
10854
11819
  tags: ["subscriptions"],
10855
- security: pairedSecurity,
11820
+ security: localControlSecurity,
10856
11821
  params: ProviderParamsSchema,
10857
11822
  response: {
10858
11823
  200: schemaRef(CancelledSubscriptionSchema),
10859
11824
  ...problemResponse
10860
11825
  }
10861
11826
  },
11827
+ catalogue: {
11828
+ operationId: "getAgentServerCatalogue",
11829
+ tags: ["catalogue"],
11830
+ security: localControlSecurity,
11831
+ querystring: CatalogueQuerySchema,
11832
+ response: {
11833
+ 200: schemaRef(AgentServerCatalogueSchema),
11834
+ ...problemResponse
11835
+ }
11836
+ },
10862
11837
  listRuns: {
10863
11838
  operationId: "listAgentServerRuns",
10864
11839
  tags: ["runs"],
10865
- security: pairedSecurity,
11840
+ security: localControlSecurity,
10866
11841
  response: {
10867
11842
  200: Type.Array(schemaRef(AgentServerRunSchema)),
10868
11843
  ...problemResponse
@@ -10871,7 +11846,7 @@ var AgentServerRouteSchemas = {
10871
11846
  startRun: {
10872
11847
  operationId: "startAgentServerRun",
10873
11848
  tags: ["runs"],
10874
- security: pairedSecurity,
11849
+ security: localControlSecurity,
10875
11850
  body: StartRunSchema,
10876
11851
  response: {
10877
11852
  201: schemaRef(AgentServerRunSchema),
@@ -10881,7 +11856,7 @@ var AgentServerRouteSchemas = {
10881
11856
  stopRun: {
10882
11857
  operationId: "stopAgentServerRun",
10883
11858
  tags: ["runs"],
10884
- security: pairedSecurity,
11859
+ security: localControlSecurity,
10885
11860
  params: RunParamsSchema,
10886
11861
  response: {
10887
11862
  200: schemaRef(AgentServerRunRecordSchema),
@@ -10891,7 +11866,7 @@ var AgentServerRouteSchemas = {
10891
11866
  streamRunLogs: {
10892
11867
  operationId: "streamAgentServerRunLogs",
10893
11868
  tags: ["runs"],
10894
- security: pairedSecurity,
11869
+ security: localControlSecurity,
10895
11870
  params: RunParamsSchema,
10896
11871
  response: {
10897
11872
  200: schemaRef(LogStreamSchema),
@@ -10906,9 +11881,8 @@ var AgentServerRouteSchemas = {
10906
11881
  * loopback-companion security profile (#2066): loopback Host enforcement,
10907
11882
  * exact-origin CORS, Fetch-Metadata guards, strict JSON parsing.
10908
11883
  *
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.
11884
+ * Control routes require a native process grant or an OAuth token bound to
11885
+ * the native operator and this server instance. Origin checks apply to both.
10912
11886
  */
10913
11887
  var AGENT_SERVER_TOKEN_HEADER = "x-moltnet-agent-server-token";
10914
11888
  var BODY_LIMIT = 64 * 1024;
@@ -11024,19 +11998,19 @@ function requestOperationSignal(request, shutdownSignal) {
11024
11998
  return shutdownSignal ? AbortSignal.any([disconnected.signal, shutdownSignal]) : disconnected.signal;
11025
11999
  }
11026
12000
  function buildAgentServer(options) {
11027
- const { pairing } = options;
11028
- const fastifyOptions = {
11029
- bodyLimit: BODY_LIMIT,
11030
- ...options.tls ? { https: options.tls } : {}
11031
- };
12001
+ const { nativeGrant } = options;
12002
+ const oauth = options.operatorOAuth;
12003
+ let restartRequired = false;
12004
+ const fastifyOptions = { bodyLimit: BODY_LIMIT };
11032
12005
  const app = options.logger ? Fastify({
11033
12006
  ...fastifyOptions,
11034
12007
  loggerInstance: options.logger
11035
12008
  }) : Fastify(fastifyOptions);
11036
12009
  options.registerOpenApi?.(app);
11037
12010
  for (const schema of AGENT_SERVER_SCHEMAS) app.addSchema(schema);
12011
+ const browserOrigins = new OriginAllowlist(options.allowedOrigins);
11038
12012
  registerLoopbackSecurity(app, {
11039
- allowedOrigins: options.allowedOrigins,
12013
+ isOriginAllowed: (origin) => origin === "moltnet-agent-desktop://native" || browserOrigins.has(origin),
11040
12014
  ...options.selfOrigin ? { selfOrigins: [options.selfOrigin] } : {},
11041
12015
  allowedHeaders: [AGENT_SERVER_TOKEN_HEADER],
11042
12016
  methods: [
@@ -11047,40 +12021,213 @@ function buildAgentServer(options) {
11047
12021
  "OPTIONS"
11048
12022
  ]
11049
12023
  });
12024
+ let verificationWindow = Date.now();
12025
+ let verifications = 0;
12026
+ const browserVerification = /* @__PURE__ */ new WeakMap();
12027
+ function verifyBrowser(request, token) {
12028
+ const previous = browserVerification.get(request);
12029
+ if (previous) return previous;
12030
+ if (Date.now() - verificationWindow >= RATE_LIMIT_WINDOW_MS) {
12031
+ verificationWindow = Date.now();
12032
+ verifications = 0;
12033
+ }
12034
+ if (++verifications > RATE_LIMIT_MAX) throw new AgentServerHttpError(429, "rate_limited", "Too many authorization attempts");
12035
+ if (!oauth) throw new AgentServerHttpError(503, "oauth_unavailable", "Local OAuth is not configured");
12036
+ const pending = oauth.verifyBrowser(token);
12037
+ browserVerification.set(request, pending);
12038
+ return pending;
12039
+ }
12040
+ function hasValidNativeGrant(origin, token) {
12041
+ if (origin !== "moltnet-agent-desktop://native" || typeof token !== "string" || token.length === 0) return false;
12042
+ try {
12043
+ nativeGrant.verify(origin, token);
12044
+ return true;
12045
+ } catch (error) {
12046
+ if (error instanceof NativeGrantError) return false;
12047
+ throw error;
12048
+ }
12049
+ }
12050
+ const nativeVerification = /* @__PURE__ */ new WeakSet();
12051
+ function requireNativeGrant(request) {
12052
+ if (nativeVerification.has(request)) return;
12053
+ const origin = request.headers.origin;
12054
+ const token = request.headers[AGENT_SERVER_TOKEN_HEADER];
12055
+ if (hasValidNativeGrant(origin, token)) {
12056
+ nativeVerification.add(request);
12057
+ return;
12058
+ }
12059
+ request.log.warn({
12060
+ stage: "native-control-authorization",
12061
+ outcome: "rejected"
12062
+ }, "Native control authorization failed");
12063
+ throw new AgentServerHttpError(401, "native_token_invalid", "Native authorization required");
12064
+ }
11050
12065
  app.register(rateLimit, {
11051
- global: false,
12066
+ global: true,
11052
12067
  max: options.rateLimitMax ?? RATE_LIMIT_MAX,
11053
12068
  timeWindow: RATE_LIMIT_WINDOW_MS,
11054
12069
  errorResponseBuilder: () => new AgentServerHttpError(429, "rate_limited", "Too many requests"),
11055
- keyGenerator: (request) => {
12070
+ keyGenerator: async (request) => {
11056
12071
  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);
12072
+ if (!isConfiguredOrigin(origin, options)) return `ip:${request.ip}`;
12073
+ const presented = request.headers[AGENT_SERVER_TOKEN_HEADER];
12074
+ let authenticated = false;
12075
+ if (typeof presented === "string" && presented.length > 0) try {
12076
+ if (origin === "moltnet-agent-desktop://native") authenticated = hasValidNativeGrant(origin, presented);
12077
+ else {
12078
+ if (!oauth) return `unauth:${origin}:${request.ip}`;
12079
+ await verifyBrowser(request, presented);
12080
+ authenticated = true;
12081
+ }
12082
+ } catch (error) {
12083
+ if (error instanceof AgentServerHttpError && error.statusCode === 429) throw error;
12084
+ if (origin === "moltnet-agent-desktop://native") throw error;
12085
+ }
12086
+ return authenticated ? `origin:${origin}` : `unauth:${origin}:${request.ip}`;
11066
12087
  }
11067
12088
  });
11068
- const requirePairedOrigin = (request) => {
12089
+ const requireAuthorizedOrigin = async (request) => {
12090
+ if (restartRequired) throw new AgentServerHttpError(409, "restart_required", "Restart the Agent Server to apply connection settings");
11069
12091
  const origin = requireOriginHeader(request.headers);
11070
12092
  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);
12093
+ if (typeof token !== "string" || token.length === 0) throw new AgentServerHttpError(401, "authorization_required", "Local control token is required");
12094
+ if (origin === "moltnet-agent-desktop://native") requireNativeGrant(request);
12095
+ else try {
12096
+ if (!oauth) throw new AgentServerHttpError(503, "oauth_unavailable", "Local OAuth is not configured");
12097
+ const admission = browserVerification.get(request);
12098
+ browserVerification.delete(request);
12099
+ await (admission ?? oauth.verifyBrowser(token));
12100
+ } catch (error) {
12101
+ if (error instanceof AgentServerHttpError) throw error;
12102
+ const code = error && typeof error === "object" && "code" in error ? error.code : void 0;
12103
+ const rejected = error instanceof InvalidOperatorGrantError || typeof code === "string" && [
12104
+ "ERR_JWT_EXPIRED",
12105
+ "ERR_JWT_CLAIM_VALIDATION_FAILED",
12106
+ "ERR_JWS_SIGNATURE_VERIFICATION_FAILED",
12107
+ "ERR_JWS_INVALID",
12108
+ "ERR_JWT_INVALID",
12109
+ "ERR_JOSE_ALG_NOT_ALLOWED",
12110
+ "ERR_JWKS_NO_MATCHING_KEY"
12111
+ ].includes(code);
12112
+ request.log.warn({
12113
+ stage: "local-control-authorization",
12114
+ outcome: rejected ? "rejected" : "unavailable",
12115
+ code: typeof code === "string" ? code : void 0
12116
+ }, "Local control authorization failed");
12117
+ if (!rejected) throw new AgentServerHttpError(503, "authorization_unavailable", "Local authorization is unavailable. Check Server settings or retry shortly.");
12118
+ throw new AgentServerHttpError(401, "authorization_required", "Sign in to authorize local control");
12119
+ }
11073
12120
  return origin;
11074
12121
  };
11075
12122
  app.after(() => {
11076
- app.addHook("onRequest", app.rateLimit());
12123
+ if (options.nativeOnly) app.addHook("preParsing", async (request) => {
12124
+ requireNativeGrant(request);
12125
+ });
11077
12126
  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);
12127
+ app.get("/v1/native/connection-settings", { schema: { hide: true } }, async (request) => {
12128
+ if (await requireAuthorizedOrigin(request) !== "moltnet-agent-desktop://native" || !options.connectionSettings) throw new AgentServerHttpError(403, "native_required", "Native administration required");
12129
+ return options.connectionSettings.view();
12130
+ });
12131
+ app.post("/v1/native/connection-settings", { schema: { hide: true } }, async (request) => {
12132
+ if (await requireAuthorizedOrigin(request) !== "moltnet-agent-desktop://native" || !options.connectionSettings) throw new AgentServerHttpError(403, "native_required", "Native administration required");
12133
+ try {
12134
+ const settings = options.runs.prepareServerRestart(() => options.connectionSettings.save(request.body));
12135
+ oauth?.cancel();
12136
+ restartRequired = true;
12137
+ return settings;
12138
+ } catch (error) {
12139
+ throw new AgentServerHttpError(400, "invalid_connection_settings", error instanceof Error ? error.message : "Invalid connection settings");
12140
+ }
12141
+ });
12142
+ app.get("/oauth/metadata", { schema: {
12143
+ operationId: "getAgentServerOAuthMetadata",
12144
+ tags: ["operator"],
12145
+ response: { 200: {
12146
+ type: "object",
12147
+ required: [
12148
+ "protocolVersion",
12149
+ "instance",
12150
+ "issuer",
12151
+ "authorizationUrl",
12152
+ "tokenUrl",
12153
+ "clientId",
12154
+ "operatorConfigured"
12155
+ ],
12156
+ properties: {
12157
+ protocolVersion: {
12158
+ type: "integer",
12159
+ const: OPERATOR_OAUTH.protocolVersion
12160
+ },
12161
+ instance: {
12162
+ type: "string",
12163
+ format: "uuid"
12164
+ },
12165
+ issuer: { type: "string" },
12166
+ authorizationUrl: { type: "string" },
12167
+ tokenUrl: { type: "string" },
12168
+ clientId: { type: "string" },
12169
+ operatorConfigured: { type: "boolean" }
12170
+ }
12171
+ } }
12172
+ } }, async () => {
12173
+ if (!oauth) throw new AgentServerHttpError(503, "oauth_unavailable", "Local OAuth is not configured");
12174
+ return oauth.metadata();
12175
+ });
12176
+ app.post("/v1/operator/sign-in", { schema: {
12177
+ operationId: "signInAgentServerOperator",
12178
+ response: { 200: {
12179
+ type: "object",
12180
+ properties: { state: { type: "string" } },
12181
+ required: ["state"]
12182
+ } }
12183
+ } }, async (request) => {
12184
+ if (await requireAuthorizedOrigin(request) !== "moltnet-agent-desktop://native" || !oauth) throw new AgentServerHttpError(403, "native_required", "Native administration required");
12185
+ await oauth.authorize(void 0, requestOperationSignal(request, options.shutdownSignal));
12186
+ return { state: "authorized" };
12187
+ });
12188
+ app.post("/v1/operator/cancel", { schema: {
12189
+ operationId: "cancelAgentServerOperatorApproval",
12190
+ tags: ["operator"],
12191
+ security: [{ agentServerToken: [] }],
12192
+ response: { 200: {
12193
+ type: "object",
12194
+ properties: { state: {
12195
+ type: "string",
12196
+ const: "cancelled"
12197
+ } },
12198
+ required: ["state"]
12199
+ } }
12200
+ } }, async (request) => {
12201
+ if (await requireAuthorizedOrigin(request) !== "moltnet-agent-desktop://native" || !oauth) throw new AgentServerHttpError(403, "native_required", "Native administration required");
12202
+ oauth.cancel();
12203
+ return { state: "cancelled" };
12204
+ });
12205
+ app.delete("/v1/operator", { schema: {
12206
+ operationId: "removeAgentServerOperator",
12207
+ tags: ["operator"],
12208
+ security: [{ agentServerToken: [] }],
12209
+ response: { 200: {
12210
+ type: "object",
12211
+ properties: { state: {
12212
+ type: "string",
12213
+ const: "removed"
12214
+ } },
12215
+ required: ["state"]
12216
+ } }
12217
+ } }, async (request) => {
12218
+ if (await requireAuthorizedOrigin(request) !== "moltnet-agent-desktop://native" || !oauth) throw new AgentServerHttpError(403, "native_required", "Native administration required");
12219
+ oauth.removeOperator();
12220
+ return { state: "removed" };
12221
+ });
12222
+ registerStatusRoute(app, options, requireAuthorizedOrigin);
12223
+ registerAgentRoutes(app, options, requireAuthorizedOrigin);
12224
+ registerProviderRoutes(app, options, requireAuthorizedOrigin);
12225
+ registerSubscriptionRoutes(app, options, requireAuthorizedOrigin);
12226
+ registerRunRoutes(app, options, requireAuthorizedOrigin);
12227
+ registerCatalogueRoute(app, options, requireAuthorizedOrigin);
12228
+ });
12229
+ app.addHook("preClose", async () => {
12230
+ options.operatorOAuth?.cancel();
11084
12231
  });
11085
12232
  app.addHook("onClose", () => {
11086
12233
  options.subscriptions.close();
@@ -11104,41 +12251,65 @@ function buildAgentServer(options) {
11104
12251
  });
11105
12252
  return app;
11106
12253
  }
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);
12254
+ function registerCatalogueRoute(app, options, requireAuthorizedOrigin) {
12255
+ app.get("/v1/catalogue", {
12256
+ schema: AgentServerRouteSchemas.catalogue,
12257
+ attachValidation: true
12258
+ }, async (request) => {
12259
+ await requireAuthorizedOrigin(request);
12260
+ const { identity } = request.query ?? {};
12261
+ if (!identity || identity.trim().length === 0) throw new AgentServerHttpError(400, "invalid_query", "\"identity\" is required");
12262
+ const alias = identity.trim();
12263
+ requireActivation(options.store, alias);
12264
+ return buildCatalogue({
12265
+ agent: await (options.catalogueAgentFor ? options.catalogueAgentFor(alias) : defaultCatalogueAgent(options, alias)),
12266
+ machine: machineCapabilities(options),
12267
+ identityDefault: readIdentityDefaultBinding(options.store.identityDir(alias))
12268
+ });
11136
12269
  });
11137
12270
  }
11138
- function registerStatusRoute(app, options, requirePairedOrigin) {
12271
+ /** Resolve and verify each indexed team independently with its exact key. */
12272
+ async function defaultCatalogueAgent(options, alias) {
12273
+ const { config } = await loadAgentActivation(options.store, alias);
12274
+ return {
12275
+ teamIds: Object.keys(config.agent_key_refs ?? {}),
12276
+ lastVerified: (teamId) => requireActivation(options.store, alias).credentialHealth?.[teamId],
12277
+ readTeam: async (teamId) => {
12278
+ const { client, metadata } = requireCredentialSnapshot(await verifyTeamActivation(options.store, alias, options.secretProviders, options.externalSecretProviders, void 0, options.shutdownSignal, teamId));
12279
+ const [team, diaries, profiles] = await Promise.all([
12280
+ client.teams.get(teamId),
12281
+ client.diaries.list(),
12282
+ client.runtimeProfiles.list({ teamId })
12283
+ ]);
12284
+ return {
12285
+ team,
12286
+ diaries: diaries.items,
12287
+ profiles: profiles.items,
12288
+ credential: metadata
12289
+ };
12290
+ }
12291
+ };
12292
+ }
12293
+ /** What this machine can execute right now: provider keys and runtime kinds. */
12294
+ function machineCapabilities(options) {
12295
+ const providerEnv = /* @__PURE__ */ new Map();
12296
+ for (const provider of Object.values(options.providers.list())) {
12297
+ const configured = providerEnv.get(provider.envName) === true;
12298
+ providerEnv.set(provider.envName, configured || provider.hasApiKey);
12299
+ }
12300
+ const runtimeKinds = new Set([BUILT_IN_RUNTIME_KIND]);
12301
+ for (const entry of options.runtimeRegistry?.list() ?? []) try {
12302
+ if (options.runtimeRegistry?.resolve(entry.kind, { forDisplay: true })) runtimeKinds.add(entry.kind);
12303
+ } catch {}
12304
+ return {
12305
+ providerEnv,
12306
+ runtimeKinds
12307
+ };
12308
+ }
12309
+ function registerStatusRoute(app, options, requireAuthorizedOrigin) {
11139
12310
  const { store, runs } = options;
11140
12311
  app.get("/v1/status", { schema: AgentServerRouteSchemas.status }, async (request) => {
11141
- requirePairedOrigin(request);
12312
+ await requireAuthorizedOrigin(request);
11142
12313
  const selected = selectedIdentity(store, options.activeIdentity);
11143
12314
  return {
11144
12315
  version: options.version,
@@ -11148,22 +12319,22 @@ function registerStatusRoute(app, options, requirePairedOrigin) {
11148
12319
  identities: identityViews(store),
11149
12320
  ...selected ? { selectedIdentity: selected } : {},
11150
12321
  providers: options.providers.list(),
11151
- runs: runViews(runs),
12322
+ runs: await runViews(runs),
11152
12323
  runtimeSettings: options.runtimeSettings ?? DEFAULT_LOCAL_OPERATIONAL_SETTINGS
11153
12324
  };
11154
12325
  });
11155
12326
  }
11156
- function registerAgentRoutes(app, options, requirePairedOrigin) {
12327
+ function registerAgentRoutes(app, options, requireAuthorizedOrigin) {
11157
12328
  const { store } = options;
11158
12329
  app.get("/v1/agents", { schema: AgentServerRouteSchemas.listAgents }, async (request) => {
11159
- requirePairedOrigin(request);
12330
+ await requireAuthorizedOrigin(request);
11160
12331
  return store.listActivations().map((activation) => publicAgentView(store, activation));
11161
12332
  });
11162
12333
  app.post("/v1/agents", {
11163
12334
  schema: AgentServerRouteSchemas.createAgent,
11164
12335
  attachValidation: true
11165
12336
  }, async (request, reply) => {
11166
- requirePairedOrigin(request);
12337
+ await requireAuthorizedOrigin(request);
11167
12338
  const body = requireBody(request);
11168
12339
  const signal = requestOperationSignal(request, options.shutdownSignal);
11169
12340
  const kind = requireString(body, "kind");
@@ -11189,11 +12360,26 @@ function registerAgentRoutes(app, options, requirePairedOrigin) {
11189
12360
  }
11190
12361
  throw new AgentServerHttpError(400, "invalid_body", "\"kind\" must be \"managed\" or \"external\"");
11191
12362
  });
12363
+ app.post("/v1/agents/:agentName/teams", { schema: AgentServerRouteSchemas.enrollTeam }, async (request) => {
12364
+ await requireAuthorizedOrigin(request);
12365
+ const { agentName } = request.params;
12366
+ if (request.headers.origin !== "moltnet-agent-desktop://native" || !options.operatorOAuth || !options.operatorApiUrl) throw new AgentServerHttpError(403, "native_required", "Native OAuth enrollment required");
12367
+ return enrollIdentityTeam({
12368
+ oauth: options.operatorOAuth,
12369
+ apiUrl: options.operatorApiUrl,
12370
+ store,
12371
+ alias: agentName,
12372
+ managed: options.secretProviders,
12373
+ external: options.externalSecretProviders,
12374
+ input: request.body,
12375
+ signal: requestOperationSignal(request, options.shutdownSignal)
12376
+ });
12377
+ });
11192
12378
  app.post("/v1/agents/:agentName/reconcile", {
11193
12379
  schema: AgentServerRouteSchemas.reconcileAgent,
11194
12380
  attachValidation: true
11195
12381
  }, async (request) => {
11196
- requirePairedOrigin(request);
12382
+ await requireAuthorizedOrigin(request);
11197
12383
  const { agentName } = request.params;
11198
12384
  const action = requireString(requireBody(request), "action");
11199
12385
  if (action !== "resume" && action !== "abandon") throw new AgentServerHttpError(400, "invalid_body", "\"action\" must be \"resume\" or \"abandon\"");
@@ -11217,13 +12403,13 @@ function identityViews(store) {
11217
12403
  hasAgentKey: hasAgentKeyConfiguration(store.readAgentConfig(alias) ?? {})
11218
12404
  }));
11219
12405
  }
11220
- function registerProviderRoutes(app, options, requirePairedOrigin) {
12406
+ function registerProviderRoutes(app, options, requireAuthorizedOrigin) {
11221
12407
  app.get("/v1/providers", { schema: AgentServerRouteSchemas.listProviders }, async (request) => {
11222
- requirePairedOrigin(request);
12408
+ await requireAuthorizedOrigin(request);
11223
12409
  return options.providers.list();
11224
12410
  });
11225
12411
  app.post("/v1/providers/:providerId/discover-models", { schema: AgentServerRouteSchemas.discoverModels }, async (request) => {
11226
- requirePairedOrigin(request);
12412
+ await requireAuthorizedOrigin(request);
11227
12413
  const { providerId } = request.params;
11228
12414
  return options.providers.discover(providerId, { signal: requestOperationSignal(request, options.shutdownSignal) });
11229
12415
  });
@@ -11231,7 +12417,7 @@ function registerProviderRoutes(app, options, requirePairedOrigin) {
11231
12417
  schema: AgentServerRouteSchemas.putProvider,
11232
12418
  attachValidation: true
11233
12419
  }, async (request, reply) => {
11234
- requirePairedOrigin(request);
12420
+ await requireAuthorizedOrigin(request);
11235
12421
  const { providerId } = request.params;
11236
12422
  const body = requireBody(request);
11237
12423
  const entry = await options.providers.set(providerId, {
@@ -11247,7 +12433,7 @@ function registerProviderRoutes(app, options, requirePairedOrigin) {
11247
12433
  schema: AgentServerRouteSchemas.deleteProvider,
11248
12434
  attachValidation: true
11249
12435
  }, async (request, reply) => {
11250
- requirePairedOrigin(request);
12436
+ await requireAuthorizedOrigin(request);
11251
12437
  const { providerId } = request.params;
11252
12438
  try {
11253
12439
  await options.providers.remove(providerId);
@@ -11258,45 +12444,45 @@ function registerProviderRoutes(app, options, requirePairedOrigin) {
11258
12444
  return reply.code(204).send(null);
11259
12445
  });
11260
12446
  }
11261
- function runViews(runs) {
11262
- return runs.list(RUN_HISTORY_LIMIT).map((record) => ({
12447
+ async function runViews(runs) {
12448
+ return (await runs.listAsync(RUN_HISTORY_LIMIT)).map((record) => ({
11263
12449
  ...record,
11264
12450
  active: runs.isActive(record.id)
11265
12451
  }));
11266
12452
  }
11267
- function registerSubscriptionRoutes(app, options, requirePairedOrigin) {
12453
+ function registerSubscriptionRoutes(app, options, requireAuthorizedOrigin) {
11268
12454
  app.get("/v1/subscriptions", { schema: AgentServerRouteSchemas.listSubscriptions }, async (request) => {
11269
- requirePairedOrigin(request);
12455
+ await requireAuthorizedOrigin(request);
11270
12456
  return options.subscriptions.list();
11271
12457
  });
11272
12458
  app.post("/v1/subscriptions/:providerId/login", { schema: AgentServerRouteSchemas.startSubscriptionLogin }, async (request, reply) => {
11273
- requirePairedOrigin(request);
12459
+ await requireAuthorizedOrigin(request);
11274
12460
  const { providerId } = request.params;
11275
12461
  const login = await options.subscriptions.start(providerId);
11276
12462
  return reply.code(201).send(login);
11277
12463
  });
11278
12464
  app.get("/v1/subscriptions/:providerId/login", { schema: AgentServerRouteSchemas.getSubscriptionLogin }, async (request) => {
11279
- requirePairedOrigin(request);
12465
+ await requireAuthorizedOrigin(request);
11280
12466
  const { providerId } = request.params;
11281
12467
  return options.subscriptions.status(providerId);
11282
12468
  });
11283
12469
  app.delete("/v1/subscriptions/:providerId/login", { schema: AgentServerRouteSchemas.cancelSubscriptionLogin }, async (request) => {
11284
- requirePairedOrigin(request);
12470
+ await requireAuthorizedOrigin(request);
11285
12471
  const { providerId } = request.params;
11286
12472
  return options.subscriptions.cancel(providerId);
11287
12473
  });
11288
12474
  }
11289
- function registerRunRoutes(app, options, requirePairedOrigin) {
12475
+ function registerRunRoutes(app, options, requireAuthorizedOrigin) {
11290
12476
  const { runs } = options;
11291
12477
  app.get("/v1/runs", { schema: AgentServerRouteSchemas.listRuns }, async (request) => {
11292
- requirePairedOrigin(request);
12478
+ await requireAuthorizedOrigin(request);
11293
12479
  return runViews(runs);
11294
12480
  });
11295
12481
  app.post("/v1/runs", {
11296
12482
  schema: AgentServerRouteSchemas.startRun,
11297
12483
  attachValidation: true
11298
12484
  }, async (request, reply) => {
11299
- requirePairedOrigin(request);
12485
+ await requireAuthorizedOrigin(request);
11300
12486
  const body = requireBody(request);
11301
12487
  const diaryId = optionalString(body, "diaryId");
11302
12488
  const record = await runs.start({
@@ -11313,17 +12499,57 @@ function registerRunRoutes(app, options, requirePairedOrigin) {
11313
12499
  });
11314
12500
  });
11315
12501
  app.delete("/v1/runs/:runId", { schema: AgentServerRouteSchemas.stopRun }, async (request) => {
11316
- requirePairedOrigin(request);
12502
+ await requireAuthorizedOrigin(request);
11317
12503
  const { runId } = request.params;
11318
12504
  return runs.stop(runId);
11319
12505
  });
11320
- registerRunLogRoute(app, options, requirePairedOrigin);
12506
+ registerRunLogRoute(app, options, requireAuthorizedOrigin);
11321
12507
  }
11322
- function registerRunLogRoute(app, options, requirePairedOrigin) {
12508
+ function registerRunLogRoute(app, options, requireAuthorizedOrigin) {
11323
12509
  const { runs, store } = options;
12510
+ app.get("/v1/runs/:runId/logs/snapshot", { schema: {
12511
+ operationId: "getAgentServerRunLogSnapshot",
12512
+ tags: ["runs"],
12513
+ security: [{ agentServerToken: [] }],
12514
+ params: {
12515
+ type: "object",
12516
+ required: ["runId"],
12517
+ properties: { runId: {
12518
+ type: "string",
12519
+ minLength: 1
12520
+ } }
12521
+ },
12522
+ response: { 200: {
12523
+ type: "object",
12524
+ required: ["lines"],
12525
+ properties: { lines: {
12526
+ type: "array",
12527
+ items: { type: "string" }
12528
+ } }
12529
+ } }
12530
+ } }, async (request) => {
12531
+ await requireAuthorizedOrigin(request);
12532
+ const { runId } = request.params;
12533
+ const record = runs.status(runId);
12534
+ const handle = await open(store.resolveRunLogPath(record.id), constants.O_RDONLY | constants.O_NOFOLLOW);
12535
+ try {
12536
+ const state = {
12537
+ offset: 0,
12538
+ fragment: ""
12539
+ };
12540
+ const { lines, omitted } = await readAgentServerLogDelta(handle, state);
12541
+ return { lines: [
12542
+ ...omitted ? ["[older log output omitted]"] : [],
12543
+ ...lines,
12544
+ ...state.fragment ? [state.fragment] : []
12545
+ ] };
12546
+ } finally {
12547
+ await handle.close();
12548
+ }
12549
+ });
11324
12550
  let openStreams = 0;
11325
12551
  app.get("/v1/runs/:runId/logs", { schema: AgentServerRouteSchemas.streamRunLogs }, async (request, reply) => {
11326
- requirePairedOrigin(request);
12552
+ await requireAuthorizedOrigin(request);
11327
12553
  const { runId } = request.params;
11328
12554
  const record = runs.status(runId);
11329
12555
  store.resolveRunLogPath(record.id);
@@ -11366,6 +12592,7 @@ function registerRunLogRoute(app, options, requirePairedOrigin) {
11366
12592
  });
11367
12593
  };
11368
12594
  const push = async () => {
12595
+ if (request.headers.origin !== "moltnet-agent-desktop://native") await requireAuthorizedOrigin(request);
11369
12596
  const handle = await open(store.resolveRunLogPath(record.id), constants.O_RDONLY | constants.O_NOFOLLOW);
11370
12597
  try {
11371
12598
  const { lines, omitted } = await readAgentServerLogDelta(handle, readState);
@@ -11414,9 +12641,14 @@ function corsHeadersFor(request, options) {
11414
12641
  return {};
11415
12642
  }
11416
12643
  function isConfiguredOrigin(origin, options) {
11417
- return typeof origin === "string" && (options.allowedOrigins.includes(origin) || origin === options.selfOrigin);
12644
+ return typeof origin === "string" && (origin === "moltnet-agent-desktop://native" || options.allowedOrigins.includes(origin) || origin === options.selfOrigin);
11418
12645
  }
11419
12646
  function normalizeAgentServerError(error) {
12647
+ if (error instanceof TeamCredentialError) return {
12648
+ statusCode: 400,
12649
+ code: error.blocker.code,
12650
+ message: error.blocker.message
12651
+ };
11420
12652
  if (error instanceof AgentServerHttpError) return {
11421
12653
  statusCode: error.statusCode,
11422
12654
  code: error.code,
@@ -11427,8 +12659,8 @@ function normalizeAgentServerError(error) {
11427
12659
  code: error.kind,
11428
12660
  message: error.message
11429
12661
  };
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,
12662
+ if (error instanceof NativeGrantError) return {
12663
+ statusCode: 401,
11432
12664
  code: error.code,
11433
12665
  message: error.message
11434
12666
  };
@@ -11469,185 +12701,42 @@ function normalizeAgentServerError(error) {
11469
12701
  };
11470
12702
  }
11471
12703
  //#endregion
11472
- //#region src/lib/agent-server/tls.ts
11473
- var execFileAsync = promisify(execFile);
11474
- var CA_COMMON_NAME = "MoltNet Local Agent CA";
11475
- var LEAF_COMMON_NAME = "MoltNet Local Agent";
11476
- var RENEW_BEFORE_MS = 720 * 60 * 60 * 1e3;
11477
- function loginKeychainPath() {
11478
- return join(homedir(), "Library", "Keychains", "login.keychain-db");
11479
- }
11480
- function pemPrivateKey(key) {
11481
- return webcrypto.subtle.exportKey("pkcs8", key).then((der) => createPrivateKey({
11482
- key: Buffer.from(der),
11483
- format: "der",
11484
- type: "pkcs8"
11485
- }).export({
11486
- format: "pem",
11487
- type: "pkcs8"
11488
- }).toString());
11489
- }
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
- async function localTlsMaterialFromDirectory(dir) {
11513
- try {
11514
- const [key, cert, ca] = await Promise.all([
11515
- readFile(join(dir, "loopback-key.pem"), "utf8"),
11516
- readFile(join(dir, "loopback-cert.pem"), "utf8"),
11517
- readFile(join(dir, "local-ca.pem"), "utf8")
11518
- ]);
11519
- const parsed = new X509Certificate(cert);
11520
- if (Date.parse(parsed.validTo) - Date.now() > RENEW_BEFORE_MS) return {
11521
- key,
11522
- cert,
11523
- ca,
11524
- fingerprint: new X509Certificate(ca).fingerprint256
11525
- };
11526
- } catch {}
11527
- return null;
11528
- }
11529
- /** Creates a per-user CA and loopback-only leaf certificate under a 0700 directory. */
11530
- async function ensureLocalTlsMaterial(root) {
11531
- const dir = join(root, "tls");
11532
- await mkdir(dir, {
11533
- recursive: true,
11534
- mode: 448
11535
- });
11536
- 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
- }
11557
- const leafKeys = await webcrypto.subtle.generateKey({
11558
- name: "ECDSA",
11559
- namedCurve: "P-256"
11560
- }, true, ["sign", "verify"]);
11561
- const leafCert = await X509CertificateGenerator.create({
11562
- subject: `CN=${LEAF_COMMON_NAME}`,
11563
- issuer: `CN=${CA_COMMON_NAME}`,
11564
- publicKey: leafKeys.publicKey,
11565
- signingKey: caKeys.privateKey,
11566
- notAfter: new Date(Date.now() + 365 * 24 * 60 * 60 * 1e3),
11567
- extensions: [
11568
- new BasicConstraintsExtension(false, void 0, true),
11569
- new KeyUsagesExtension(KeyUsageFlags.digitalSignature, true),
11570
- new ExtendedKeyUsageExtension([ExtendedKeyUsage.serverAuth]),
11571
- new SubjectAlternativeNameExtension([{
11572
- type: IP,
11573
- value: "127.0.0.1"
11574
- }], true)
11575
- ]
11576
- });
11577
- const key = await pemPrivateKey(leafKeys.privateKey);
11578
- const cert = leafCert.toString("pem");
11579
- 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 }));
11581
- await Promise.all(writes);
11582
- return {
11583
- key,
11584
- cert,
11585
- ca,
11586
- fingerprint: new X509Certificate(ca).fingerprint256
11587
- };
11588
- }
11589
- async function trustLocalCa(root) {
11590
- const caPath = join(root, "tls", "local-ca.pem");
11591
- await execFileAsync("security", [
11592
- "add-trusted-cert",
11593
- "-d",
11594
- "-r",
11595
- "trustRoot",
11596
- "-k",
11597
- loginKeychainPath(),
11598
- caPath
11599
- ]);
11600
- }
11601
- async function isLocalCaTrusted(root) {
11602
- const ca = await readFile(join(root, "tls", "local-ca.pem"), "utf8");
11603
- try {
11604
- const { stdout } = await execFileAsync("security", [
11605
- "find-certificate",
11606
- "-a",
11607
- "-p",
11608
- "-c",
11609
- CA_COMMON_NAME,
11610
- loginKeychainPath()
11611
- ]);
11612
- return stdout.includes(ca.trim());
11613
- } catch {
11614
- return false;
11615
- }
11616
- }
11617
- async function removeLocalCa(root) {
11618
- await execFileAsync("security", [
11619
- "delete-certificate",
11620
- "-Z",
11621
- new X509Certificate(await readFile(join(root, "tls", "local-ca.pem"), "utf8")).fingerprint256.replaceAll(":", ""),
11622
- loginKeychainPath()
11623
- ]);
11624
- }
11625
- function isMacos() {
11626
- return process.platform === "darwin";
11627
- }
11628
- //#endregion
11629
12704
  //#region src/cli/server.ts
11630
12705
  /**
11631
12706
  * `moltnet-agent server` — per-user loopback supervisor (#2061).
11632
12707
  *
11633
- * Starts nothing on its own: it binds 127.0.0.1 and waits for a paired
11634
- * Console origin to configure agents/providers and start/stop runs.
12708
+ * Starts nothing on its own: it binds 127.0.0.1 or a private native socket and
12709
+ * waits for an authorized controller to configure agents/providers and
12710
+ * start or stop runs.
11635
12711
  */
11636
- var DEFAULT_PORT = 17374;
12712
+ var DEFAULT_PORT = OPERATOR_OAUTH.serverPort;
11637
12713
  var DEFAULT_ALLOWED_ORIGINS = "https://console.themolt.net";
11638
- var DEFAULT_API_URL = "https://api.themolt.net";
11639
12714
  var SHUTDOWN_TIMEOUT_MS = 15e3;
12715
+ var LOCK_HELD_EXIT_CODE = 75;
12716
+ function agentServerLockExitCode(error) {
12717
+ return error.code === "held" ? LOCK_HELD_EXIT_CODE : 1;
12718
+ }
12719
+ function validateNativeSocketOptions(options) {
12720
+ if (!options.nativeSocket) return void 0;
12721
+ if (!options.supervised) return "--native-socket requires --supervised";
12722
+ if (options.port || options.allowedOrigins) return "--native-socket cannot be combined with TCP options";
12723
+ }
12724
+ function nativeSocketValidationOptions(input) {
12725
+ return {
12726
+ ...input.nativeSocket ? { nativeSocket: input.nativeSocket } : {},
12727
+ ...input.supervised ? { supervised: true } : {},
12728
+ ...input.cliPort ? { port: input.cliPort } : {},
12729
+ ...input.cliAllowedOrigins ? { allowedOrigins: input.cliAllowedOrigins } : {}
12730
+ };
12731
+ }
11640
12732
  async function runAgentServer(argv) {
11641
12733
  if (isHelpFlag(argv)) {
11642
12734
  console.log(AGENT_SERVER_HELP);
11643
12735
  return 0;
11644
12736
  }
11645
- const trustRequested = argv[0] === "trust";
11646
- const commandArgs = trustRequested ? argv.slice(1) : argv;
11647
12737
  const envConfig = loadAgentServerEnvConfig();
11648
- if (trustRequested) return runTrustCommand(commandArgs, resolveAgentServerRoot({ root: envConfig.root }));
11649
12738
  const { values } = parseArgs({
11650
- args: commandArgs,
12739
+ args: argv,
11651
12740
  options: {
11652
12741
  port: { type: "string" },
11653
12742
  "allowed-origins": { type: "string" },
@@ -11655,17 +12744,36 @@ async function runAgentServer(argv) {
11655
12744
  "api-url": { type: "string" },
11656
12745
  "heartbeat-interval-ms": { type: "string" },
11657
12746
  "warm-retention-sec": { type: "string" },
11658
- supervised: { type: "boolean" }
12747
+ supervised: { type: "boolean" },
12748
+ "native-socket": { type: "string" }
11659
12749
  }
11660
12750
  });
12751
+ const nativeSocket = values["native-socket"];
12752
+ const nativeSocketError = validateNativeSocketOptions(nativeSocketValidationOptions({
12753
+ ...nativeSocket ? { nativeSocket } : {},
12754
+ ...values.supervised ? { supervised: true } : {},
12755
+ ...values.port ? { cliPort: values.port } : {},
12756
+ ...values["allowed-origins"] ? { cliAllowedOrigins: values["allowed-origins"] } : {},
12757
+ ...envConfig.port ? { envPort: envConfig.port } : {},
12758
+ ...envConfig.allowedOrigins ? { envAllowedOrigins: envConfig.allowedOrigins } : {}
12759
+ }));
12760
+ if (nativeSocketError) {
12761
+ console.error(nativeSocketError);
12762
+ return 1;
12763
+ }
11661
12764
  const port = Number.parseInt(values.port ?? (envConfig.port || `${DEFAULT_PORT}`), 10);
11662
12765
  if (!Number.isInteger(port) || port < 1 || port > 65535) {
11663
12766
  console.error(`Invalid --port: ${String(values.port)}`);
11664
12767
  return 1;
11665
12768
  }
11666
12769
  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);
12770
+ const connectionSettings = new ConnectionSettingsStore(values.root ?? resolveAgentServerRoot({ root: envConfig.root }), {
12771
+ ...envConfig.operatorOAuth,
12772
+ ...values["api-url"] || envConfig.apiUrl ? { apiUrl: values["api-url"] || envConfig.apiUrl } : {}
12773
+ });
12774
+ const connection = connectionSettings.view().effective;
12775
+ const root = connectionSettings.stateRoot(connection);
12776
+ const defaultApiUrl = connection.apiUrl;
11669
12777
  const runtimeSettings = parseLocalOperationalSettings(values);
11670
12778
  const store = new AgentServerStore(root).ensure();
11671
12779
  const { logger, shutdown: shutdownLogger } = createRootLogger({
@@ -11681,7 +12789,15 @@ async function runAgentServer(argv) {
11681
12789
  });
11682
12790
  const secretProviders = createNodeSecretProviderRegistry().register(secrets);
11683
12791
  const externalSecretProviders = createNodeSecretProviderRegistry();
11684
- const pairing = new PairingService();
12792
+ const nativeGrant = new NativeGrantService();
12793
+ const nativeClient = applyNativeClientGrant({
12794
+ nativeGrant,
12795
+ env: processEnvSnapshot()
12796
+ });
12797
+ if (Boolean(values.supervised) && !nativeClient) {
12798
+ console.error(`A supervised Agent Server requires ${NATIVE_TOKEN_ENV}. Start it from MoltNet Agent, or omit --supervised for an authorized standalone controller.`);
12799
+ return 1;
12800
+ }
11685
12801
  const shutdownController = new AbortController();
11686
12802
  const subscriptions = await ProviderLoginService.create({
11687
12803
  authPath: store.piAuthJsonPath,
@@ -11693,31 +12809,42 @@ async function runAgentServer(argv) {
11693
12809
  secretProviders,
11694
12810
  logger
11695
12811
  });
12812
+ const runtimeRegistry = new RuntimeRegistry(store.root);
11696
12813
  const runs = new RunManager({
11697
12814
  store,
11698
12815
  secretProviders,
11699
12816
  externalSecretProviders,
11700
12817
  baseEnv: processEnvSnapshot(),
11701
12818
  logger,
11702
- runtimeRegistry: new RuntimeRegistry(store.root),
12819
+ runtimeRegistry,
11703
12820
  runtimeSettings
11704
12821
  });
11705
- const tls = isMacos() ? await ensureTrustedLocalTls(root) : void 0;
12822
+ if (nativeSocket) await validateNativeSocket(nativeSocket);
12823
+ const selfOrigin = nativeSocket ? void 0 : `http://127.0.0.1:${port}`;
11706
12824
  const app = buildAgentServer({
12825
+ operatorOAuth: new OperatorOAuth({
12826
+ issuer: connection.issuer,
12827
+ authorizationUrl: new URL("/oauth2/auth", connection.publicUrl).href,
12828
+ tokenUrl: new URL("/oauth2/token", connection.publicUrl).href,
12829
+ jwksUrl: new URL("/.well-known/jwks.json", connection.publicUrl).href,
12830
+ nativeClientId: connection.nativeClientId,
12831
+ consoleClientId: connection.consoleClientId,
12832
+ callbackPort: OPERATOR_OAUTH.callbackPort
12833
+ }, root),
12834
+ nativeOnly: Boolean(nativeSocket),
12835
+ connectionSettings,
12836
+ operatorApiUrl: connection.apiUrl,
11707
12837
  store,
11708
12838
  secrets,
11709
12839
  secretProviders,
11710
12840
  externalSecretProviders,
11711
- pairing,
12841
+ nativeGrant,
11712
12842
  runs,
11713
12843
  subscriptions,
11714
12844
  providers,
11715
- allowedOrigins,
11716
- selfOrigin: `${tls ? "https" : "http"}://127.0.0.1:${port}`,
11717
- ...tls ? { tls: {
11718
- key: tls.key,
11719
- cert: tls.cert
11720
- } } : {},
12845
+ runtimeRegistry,
12846
+ allowedOrigins: nativeSocket ? [] : allowedOrigins,
12847
+ ...selfOrigin ? { selfOrigin } : {},
11721
12848
  defaultApiUrl,
11722
12849
  runtimeSettings,
11723
12850
  ...envConfig.activeIdentity ? { activeIdentity: envConfig.activeIdentity } : {},
@@ -11726,14 +12853,17 @@ async function runAgentServer(argv) {
11726
12853
  shutdownSignal: shutdownController.signal
11727
12854
  });
11728
12855
  try {
11729
- const address = await app.listen({
12856
+ const address = await app.listen(nativeSocket ? { path: nativeSocket } : {
11730
12857
  host: "127.0.0.1",
11731
12858
  port
11732
12859
  });
12860
+ if (nativeSocket) await chmod(nativeSocket, 384);
11733
12861
  console.error(`moltnet-agent server listening on ${address}`);
11734
12862
  console.error(`config root: ${root}`);
11735
- console.error(`allowed origins: ${allowedOrigins.join(", ")}`);
11736
- console.error("Pair from the Console \"Local runtime\" page; approve the one-click prompt this server opens.");
12863
+ if (nativeSocket) console.error(`native control socket: ${nativeSocket}`);
12864
+ else console.error(`allowed origins: ${allowedOrigins.join(", ")}`);
12865
+ if (nativeClient) console.error("native desktop client: authorized");
12866
+ if (!nativeSocket) console.error("Connect from an allowed local-control client after operator authorization.");
11737
12867
  return await waitForAgentServerShutdown(runs, app, shutdownController, Boolean(values.supervised));
11738
12868
  } catch (cause) {
11739
12869
  await app.close().catch(() => void 0);
@@ -11747,7 +12877,7 @@ async function runAgentServer(argv) {
11747
12877
  } catch (cause) {
11748
12878
  if (cause instanceof AgentServerLockError) {
11749
12879
  console.error(cause.message);
11750
- return 1;
12880
+ return agentServerLockExitCode(cause);
11751
12881
  }
11752
12882
  throw cause;
11753
12883
  }
@@ -11755,98 +12885,6 @@ async function runAgentServer(argv) {
11755
12885
  await shutdownLogger();
11756
12886
  }
11757
12887
  }
11758
- async function runTrustCommand(argv, defaultRoot) {
11759
- try {
11760
- const { values } = parseArgs({
11761
- args: argv,
11762
- options: {
11763
- root: { type: "string" },
11764
- remove: { type: "boolean" },
11765
- status: { type: "boolean" },
11766
- yes: { type: "boolean" },
11767
- json: { type: "boolean" }
11768
- }
11769
- });
11770
- const root = values.root ?? defaultRoot;
11771
- const statusRequested = Boolean(values.status);
11772
- const removeRequested = Boolean(values.remove);
11773
- const yes = Boolean(values.yes);
11774
- const json = Boolean(values.json);
11775
- if (statusRequested && (removeRequested || yes)) {
11776
- console.error("Usage: moltnet-agent server trust --status [--json]");
11777
- return 1;
11778
- }
11779
- if (!isMacos()) {
11780
- if (json) {
11781
- printTrustStatus({
11782
- supported: false,
11783
- trusted: false,
11784
- fingerprint: null
11785
- });
11786
- return 0;
11787
- }
11788
- console.error("Local HTTPS trust setup is currently supported on macOS only.");
11789
- return 1;
11790
- }
11791
- const material = await ensureLocalTlsMaterial(root);
11792
- if (statusRequested) {
11793
- const trusted = await isLocalCaTrusted(root);
11794
- if (json) printTrustStatus({
11795
- supported: true,
11796
- trusted,
11797
- fingerprint: material.fingerprint
11798
- });
11799
- else console.log(trusted ? `MoltNet local CA ${material.fingerprint} is trusted.` : `MoltNet local CA ${material.fingerprint} is not trusted.`);
11800
- return 0;
11801
- }
11802
- if (json && !yes) {
11803
- console.error("Machine-readable trust changes require --yes after native app consent.");
11804
- return 1;
11805
- }
11806
- if (removeRequested) {
11807
- await removeLocalCa(root);
11808
- if (json) printTrustStatus({
11809
- supported: true,
11810
- trusted: false,
11811
- fingerprint: material.fingerprint
11812
- });
11813
- else console.log("Removed the MoltNet local CA from your login keychain.");
11814
- return 0;
11815
- }
11816
- if (yes) await trustLocalCa(root);
11817
- else await ensureTrustedLocalTls(root);
11818
- if (json) printTrustStatus({
11819
- supported: true,
11820
- trusted: await isLocalCaTrusted(root),
11821
- fingerprint: material.fingerprint
11822
- });
11823
- else console.log("MoltNet local HTTPS trust is ready for this macOS user.");
11824
- return 0;
11825
- } catch (cause) {
11826
- console.error(`Agent Server trust command failed: ${cause instanceof Error ? cause.message : String(cause)}`);
11827
- return 1;
11828
- }
11829
- }
11830
- function printTrustStatus(status) {
11831
- console.log(JSON.stringify(status));
11832
- }
11833
- async function ensureTrustedLocalTls(root) {
11834
- const material = await ensureLocalTlsMaterial(root);
11835
- if (await isLocalCaTrusted(root)) return material;
11836
- if (!process.stdin.isTTY || !process.stdout.isTTY) throw new Error("Local HTTPS trust is not configured. Run `moltnet-agent server trust` from an interactive terminal.");
11837
- const prompt = createInterface({
11838
- input: process.stdin,
11839
- output: process.stdout
11840
- });
11841
- try {
11842
- const answer = await prompt.question(`Trust MoltNet's local CA (${material.fingerprint}) in this macOS login keychain? [y/N] `);
11843
- if (!/^y(es)?$/i.test(answer.trim())) throw new Error("Local HTTPS trust was not approved.");
11844
- } finally {
11845
- prompt.close();
11846
- }
11847
- await trustLocalCa(root);
11848
- return material;
11849
- }
11850
12888
  function waitForAgentServerShutdown(runs, app, shutdownController, supervised) {
11851
12889
  return new Promise((resolvePromise) => {
11852
12890
  let shuttingDown = false;
@@ -12021,6 +13059,7 @@ async function runSyncSessions(argv) {
12021
13059
  args: argv,
12022
13060
  options: {
12023
13061
  ...identityOptionDefs(),
13062
+ ...projectRunOptionDefs(),
12024
13063
  team: { type: "string" },
12025
13064
  "runtime-profile-id": { type: "string" },
12026
13065
  state: { type: "string" },
@@ -12028,11 +13067,6 @@ async function runSyncSessions(argv) {
12028
13067
  "dry-run": { type: "boolean" }
12029
13068
  }
12030
13069
  });
12031
- if (!values.team) {
12032
- console.error("Missing required flag: --team\n");
12033
- console.error(SYNC_SESSIONS_HELP);
12034
- return 1;
12035
- }
12036
13070
  let identity;
12037
13071
  try {
12038
13072
  identity = parseIdentityProcessOptions(values);
@@ -12049,18 +13083,38 @@ async function runSyncSessions(argv) {
12049
13083
  const agentRootDir = resolve(process.cwd(), values["agent-root"] ?? process.cwd());
12050
13084
  const explicitAgentRootDir = values["agent-root"] ? resolve(process.cwd(), values["agent-root"]) : void 0;
12051
13085
  const cfg = loadConfig();
13086
+ let selection;
13087
+ try {
13088
+ const apiUrl = await resolveSelectionApiUrl(identity.agent, {
13089
+ agentRootDir: explicitAgentRootDir,
13090
+ credentialSource: cfg.credentialSource,
13091
+ envApiUrl: cfg.apiUrl
13092
+ });
13093
+ selection = await resolveRunProjectSelection({
13094
+ ...values,
13095
+ agent: identity.agent,
13096
+ cwd: process.cwd(),
13097
+ apiUrl
13098
+ });
13099
+ if (!selection.teamId) throw new Error("Select --team or a binding with a team");
13100
+ } catch (error) {
13101
+ console.error(error instanceof Error ? error.message : String(error));
13102
+ console.error(SYNC_SESSIONS_HELP);
13103
+ return 1;
13104
+ }
12052
13105
  const ctx = await resolveAgentContext(identity.agent, {
12053
13106
  credentialSource: cfg.credentialSource,
12054
13107
  envApiUrl: cfg.apiUrl,
12055
- teamId: values.team,
13108
+ projectApiUrl: selection.binding?.apiUrl,
13109
+ teamId: selection.teamId,
12056
13110
  agentRootDir: explicitAgentRootDir
12057
13111
  });
12058
13112
  await validateStartupBinding({
12059
13113
  agent: ctx.agent,
12060
- teamId: values.team,
13114
+ teamId: selection.teamId,
12061
13115
  credentialTeamId: ctx.credentialTeamId
12062
13116
  });
12063
- const stateDirs = ensureDaemonStateDirs(agentRootDir);
13117
+ const stateDirs = ensureDaemonStateDirs(selection.stateRootDir ?? (selection.binding || values.source ? selection.source ?? agentRootDir : agentRootDir));
12064
13118
  const result = await syncRuntimeSessions({
12065
13119
  runtimeSessionStore: createApiRuntimeSessionStore({ agent: ctx.agent }),
12066
13120
  runtimeSlotStore: createApiRuntimeSlotStore({ agent: ctx.agent }),
@@ -12072,7 +13126,7 @@ async function runSyncSessions(argv) {
12072
13126
  runtimeProfileId: values["runtime-profile-id"],
12073
13127
  sessionRootDir: stateDirs.piSessionsDir,
12074
13128
  state,
12075
- teamId: values.team
13129
+ teamId: selection.teamId
12076
13130
  });
12077
13131
  console.log(JSON.stringify(result, null, 2));
12078
13132
  return result.failedUpload > 0 || result.unsafeSessionPath > 0 ? 1 : 0;
@@ -12243,7 +13297,7 @@ async function writeCache(cache) {
12243
13297
  }
12244
13298
  //#endregion
12245
13299
  //#region src/version.ts
12246
- var DAEMON_VERSION = "0.60.0";
13300
+ var DAEMON_VERSION = "0.62.0";
12247
13301
  //#endregion
12248
13302
  //#region src/cli.ts
12249
13303
  async function runAgentDaemonCli(options) {