@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.
- package/README.md +81 -9
- package/dist/cli.js +2013 -959
- package/dist/pi.js +16 -1
- 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
|
|
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 {
|
|
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 {
|
|
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
|
|
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
|
-
|
|
3831
|
-
|
|
3832
|
-
|
|
3833
|
-
|
|
3834
|
-
|
|
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:
|
|
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
|
|
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
|
|
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
|
|
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
|
|
4385
|
-
if (!allowed.has(effectiveMode)) throw new
|
|
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 =
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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:
|
|
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:
|
|
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
|
-
|
|
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:
|
|
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:
|
|
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:
|
|
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 >=
|
|
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
|
|
8891
|
-
|
|
8892
|
-
|
|
8893
|
-
|
|
8894
|
-
|
|
8895
|
-
|
|
8896
|
-
|
|
8897
|
-
|
|
8898
|
-
|
|
8899
|
-
function
|
|
8900
|
-
|
|
8901
|
-
const
|
|
8902
|
-
const
|
|
8903
|
-
|
|
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
|
-
|
|
8907
|
-
|
|
8908
|
-
|
|
8909
|
-
|
|
8910
|
-
|
|
8911
|
-
|
|
8912
|
-
|
|
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/
|
|
9422
|
+
//#region src/lib/agent-server/native-grant.ts
|
|
8972
9423
|
/**
|
|
8973
|
-
*
|
|
8974
|
-
*
|
|
9424
|
+
* Environment variable the supervising desktop app uses to hand this server
|
|
9425
|
+
* process its control token.
|
|
8975
9426
|
*
|
|
8976
|
-
*
|
|
8977
|
-
*
|
|
8978
|
-
*
|
|
8979
|
-
|
|
8980
|
-
|
|
8981
|
-
|
|
8982
|
-
|
|
8983
|
-
|
|
8984
|
-
*
|
|
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
|
-
*
|
|
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
|
-
|
|
8994
|
-
|
|
8995
|
-
|
|
8996
|
-
|
|
8997
|
-
|
|
8998
|
-
|
|
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
|
-
|
|
9002
|
-
|
|
9003
|
-
|
|
9004
|
-
function
|
|
9005
|
-
|
|
9006
|
-
const
|
|
9007
|
-
|
|
9008
|
-
}
|
|
9009
|
-
|
|
9010
|
-
|
|
9011
|
-
|
|
9012
|
-
|
|
9013
|
-
|
|
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
|
-
|
|
9016
|
-
|
|
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
|
-
|
|
9019
|
-
|
|
9532
|
+
cancel() {
|
|
9533
|
+
this.pending?.abort();
|
|
9020
9534
|
}
|
|
9021
|
-
|
|
9022
|
-
|
|
9023
|
-
|
|
9535
|
+
removeOperator() {
|
|
9536
|
+
this.cancel();
|
|
9537
|
+
rmSync(join(this.root, "operator.json"), { force: true });
|
|
9538
|
+
this.operator = null;
|
|
9024
9539
|
}
|
|
9025
|
-
|
|
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
|
-
|
|
9037
|
-
|
|
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
|
-
|
|
9041
|
-
|
|
9042
|
-
|
|
9043
|
-
|
|
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
|
-
|
|
9046
|
-
|
|
9567
|
+
issuer: payload.iss,
|
|
9568
|
+
subject: payload.sub,
|
|
9569
|
+
provisioning: claims["moltnet:provisioning"]
|
|
9047
9570
|
};
|
|
9048
9571
|
}
|
|
9049
|
-
|
|
9050
|
-
const
|
|
9051
|
-
if (
|
|
9052
|
-
|
|
9053
|
-
|
|
9054
|
-
|
|
9055
|
-
|
|
9056
|
-
|
|
9057
|
-
const
|
|
9058
|
-
|
|
9059
|
-
|
|
9060
|
-
const
|
|
9061
|
-
this.pending
|
|
9062
|
-
|
|
9063
|
-
|
|
9064
|
-
|
|
9065
|
-
|
|
9066
|
-
|
|
9067
|
-
|
|
9068
|
-
|
|
9069
|
-
|
|
9070
|
-
|
|
9071
|
-
|
|
9072
|
-
|
|
9073
|
-
|
|
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("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll("\"", """);
|
|
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
|
-
*
|
|
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
|
-
|
|
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["
|
|
9987
|
-
|
|
9988
|
-
|
|
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 ??
|
|
10029
|
-
if (cause instanceof
|
|
10030
|
-
throw
|
|
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, {
|
|
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.
|
|
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
|
-
|
|
10183
|
-
|
|
10184
|
-
|
|
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 =
|
|
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
|
-
|
|
10523
|
-
if (
|
|
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
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
-
*
|
|
10910
|
-
*
|
|
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 {
|
|
11028
|
-
const
|
|
11029
|
-
|
|
11030
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
11058
|
-
|
|
11059
|
-
|
|
11060
|
-
|
|
11061
|
-
|
|
11062
|
-
|
|
11063
|
-
|
|
11064
|
-
|
|
11065
|
-
|
|
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
|
|
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, "
|
|
11072
|
-
|
|
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("
|
|
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
|
-
|
|
11079
|
-
|
|
11080
|
-
|
|
11081
|
-
|
|
11082
|
-
|
|
11083
|
-
|
|
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
|
|
11108
|
-
app.
|
|
11109
|
-
|
|
11110
|
-
|
|
11111
|
-
})
|
|
11112
|
-
|
|
11113
|
-
|
|
11114
|
-
|
|
11115
|
-
const
|
|
11116
|
-
|
|
11117
|
-
|
|
11118
|
-
|
|
11119
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
12327
|
+
function registerAgentRoutes(app, options, requireAuthorizedOrigin) {
|
|
11157
12328
|
const { store } = options;
|
|
11158
12329
|
app.get("/v1/agents", { schema: AgentServerRouteSchemas.listAgents }, async (request) => {
|
|
11159
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
12406
|
+
function registerProviderRoutes(app, options, requireAuthorizedOrigin) {
|
|
11221
12407
|
app.get("/v1/providers", { schema: AgentServerRouteSchemas.listProviders }, async (request) => {
|
|
11222
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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,
|
|
12453
|
+
function registerSubscriptionRoutes(app, options, requireAuthorizedOrigin) {
|
|
11268
12454
|
app.get("/v1/subscriptions", { schema: AgentServerRouteSchemas.listSubscriptions }, async (request) => {
|
|
11269
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
12475
|
+
function registerRunRoutes(app, options, requireAuthorizedOrigin) {
|
|
11290
12476
|
const { runs } = options;
|
|
11291
12477
|
app.get("/v1/runs", { schema: AgentServerRouteSchemas.listRuns }, async (request) => {
|
|
11292
|
-
|
|
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
|
-
|
|
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
|
-
|
|
12502
|
+
await requireAuthorizedOrigin(request);
|
|
11317
12503
|
const { runId } = request.params;
|
|
11318
12504
|
return runs.stop(runId);
|
|
11319
12505
|
});
|
|
11320
|
-
registerRunLogRoute(app, options,
|
|
12506
|
+
registerRunLogRoute(app, options, requireAuthorizedOrigin);
|
|
11321
12507
|
}
|
|
11322
|
-
function registerRunLogRoute(app, options,
|
|
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
|
-
|
|
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
|
|
11431
|
-
statusCode:
|
|
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
|
|
11634
|
-
*
|
|
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 =
|
|
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:
|
|
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
|
|
11668
|
-
|
|
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
|
|
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
|
|
12819
|
+
runtimeRegistry,
|
|
11703
12820
|
runtimeSettings
|
|
11704
12821
|
});
|
|
11705
|
-
|
|
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
|
-
|
|
12841
|
+
nativeGrant,
|
|
11712
12842
|
runs,
|
|
11713
12843
|
subscriptions,
|
|
11714
12844
|
providers,
|
|
11715
|
-
|
|
11716
|
-
|
|
11717
|
-
...
|
|
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(`
|
|
11736
|
-
console.error(
|
|
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
|
|
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
|
-
|
|
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:
|
|
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:
|
|
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.
|
|
13300
|
+
var DAEMON_VERSION = "0.62.0";
|
|
12247
13301
|
//#endregion
|
|
12248
13302
|
//#region src/cli.ts
|
|
12249
13303
|
async function runAgentDaemonCli(options) {
|