@themoltnet/agent-daemon 0.60.0 → 0.61.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +74 -3
- package/dist/cli.js +2011 -766
- package/dist/pi.js +16 -1
- package/package.json +10 -9
package/dist/cli.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { assertRuntimeAdapterSupportsProfile } from "./runtime.js";
|
|
3
|
-
import { a as
|
|
3
|
+
import { a as enrollmentProofMessage, defaultPiDaemonAdapter, i as compileExecutionPlan, n as createExecutionPlanSnapshot, o as cryptoService, r as parseCredentialRequirements, t as runtimeExecutionOffer } from "./pi.js";
|
|
4
4
|
import { createRequire } from "node:module";
|
|
5
5
|
import { pino, transport } from "pino";
|
|
6
6
|
import { Type } from "typebox";
|
|
@@ -8,14 +8,14 @@ import "multiformats/cid";
|
|
|
8
8
|
import "multiformats/codecs/json";
|
|
9
9
|
import "multiformats/hashes/sha2";
|
|
10
10
|
import "typebox/value";
|
|
11
|
+
import { execFile, spawn } from "node:child_process";
|
|
11
12
|
import { basename, dirname, isAbsolute, join, resolve, sep } from "node:path";
|
|
12
|
-
import { parseArgs, promisify } from "node:util";
|
|
13
|
-
import {
|
|
13
|
+
import { parseArgs, parseEnv, promisify } from "node:util";
|
|
14
|
+
import { CredentialPersistenceError, EnrollmentRecoveryError, FILE_SECRET_PROVIDER, FileSecretProvider, ProjectConfigError, ProvisioningNotStartedError, RegisterIdentityError, WORKSPACE_STRATEGIES, boundedIdentitySignal, canonicalDirectory, connect, createNodeSecretProviderRegistry, enrollTeam, getProjectConfigPath, readProjectConfig, register, resolveProjectBinding } from "@themoltnet/sdk/node";
|
|
15
|
+
import { constants, copyFileSync, createReadStream, createWriteStream, existsSync, lstatSync, mkdirSync, mkdtempSync, openSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, statSync, symlinkSync, writeFileSync } from "node:fs";
|
|
16
|
+
import { AuthenticationError, IDENTITY_ALIAS_PATTERN, MoltNetError, assertIdentityAlias, assertTrustedConfigApiUrl, createExecutorAttestor, formatSecretReferenceString, getConfigDir, getIdentityDir, hasAgentKeyConfiguration, isCanonicalConfig, parseSecretReferenceString, readConfig, requireSecureCredentialApiUrl, resolveAgentKey, resolveEnvSecretReference, resolveIdentitySeed, selectAgentKeyReference, signBytes } from "@themoltnet/sdk";
|
|
17
|
+
import { AgentRuntime, ApiTaskReporter, ApiTaskSource, PollingApiTaskSource, RuntimeProfilePrerequisiteError, createLocalSeedSigner, resolveAgentIdentity, resolveRuntimeProfile, resolveRuntimeProfiles, validateRuntimeProfilePrerequisites } from "@themoltnet/agent-runtime";
|
|
14
18
|
import { GuestEnvironmentBoundaryError, assertGuestEnvironmentBoundary, createPiRetryTriage, findMainWorktree, isResolvedPathInsideRoot, normalizeRetryTriageResult, redactRetryTriageSecrets, resolveRuntimeProfileModel } from "@themoltnet/pi-runtime";
|
|
15
|
-
import { FILE_SECRET_PROVIDER, FileSecretProvider, RegisterIdentityError, boundedIdentitySignal, connect, createNodeSecretProviderRegistry, register } from "@themoltnet/sdk/node";
|
|
16
|
-
import { constants, copyFileSync, createReadStream, createWriteStream, existsSync, lstatSync, mkdirSync, mkdtempSync, openSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
|
|
17
|
-
import { AuthenticationError, IDENTITY_ALIAS_PATTERN, MoltNetError, assertIdentityAlias, assertTrustedConfigApiUrl, createExecutorAttestor, formatSecretReferenceString, getConfigDir, getIdentityDir, hasAgentKeyConfiguration, isCanonicalConfig, parseSecretReferenceString, readConfig, requireSecureCredentialApiUrl, resolveAgentKey, resolveEnvSecretReference, resolveIdentitySeed, selectAgentKeyReference } from "@themoltnet/sdk";
|
|
18
|
-
import { execFile, spawn } from "node:child_process";
|
|
19
19
|
import { X509Certificate, createHash, createPrivateKey, createPublicKey, randomBytes, randomUUID, timingSafeEqual, webcrypto } from "node:crypto";
|
|
20
20
|
import { once } from "node:events";
|
|
21
21
|
import { metrics } from "@opentelemetry/api";
|
|
@@ -28,8 +28,8 @@ import { NodeTracerProvider } from "@opentelemetry/sdk-trace-node";
|
|
|
28
28
|
import { ATTR_SERVICE_NAME, ATTR_SERVICE_VERSION } from "@opentelemetry/semantic-conventions";
|
|
29
29
|
import { homedir, platform, tmpdir } from "node:os";
|
|
30
30
|
import { PI_MODEL_MODALITIES, writePiConfig } from "@themoltnet/pi-runtime/pi-config";
|
|
31
|
+
import { mkdir, open, readFile, readdir, realpath, rm, stat, writeFile } from "node:fs/promises";
|
|
31
32
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
32
|
-
import { mkdir, open, readFile, realpath, stat, writeFile } from "node:fs/promises";
|
|
33
33
|
import { pipeline } from "node:stream/promises";
|
|
34
34
|
import { createInterface } from "node:readline/promises";
|
|
35
35
|
import { ModelRuntime, readStoredCredential } from "@earendil-works/pi-coding-agent";
|
|
@@ -38,6 +38,8 @@ import { lock, lockSync } from "proper-lockfile";
|
|
|
38
38
|
import { isIP } from "node:net";
|
|
39
39
|
import cors from "@fastify/cors";
|
|
40
40
|
import helmet from "@fastify/helmet";
|
|
41
|
+
import { createServer } from "node:http";
|
|
42
|
+
import { createRemoteJWKSet, jwtVerify } from "jose";
|
|
41
43
|
import { Transform, Writable } from "node:stream";
|
|
42
44
|
import { pathToFileURL } from "node:url";
|
|
43
45
|
import { StringDecoder } from "node:string_decoder";
|
|
@@ -857,6 +859,23 @@ Object.freeze([...[
|
|
|
857
859
|
"offline_access"
|
|
858
860
|
], ...MCP_CLIENT_SCOPES]);
|
|
859
861
|
//#endregion
|
|
862
|
+
//#region ../../libs/models/src/operator-oauth.ts
|
|
863
|
+
/** Public protocol constants shared by consent, native control, and Console. */
|
|
864
|
+
var OPERATOR_OAUTH = Object.freeze({
|
|
865
|
+
protocolVersion: 2,
|
|
866
|
+
provisioningScope: "moltnet:provision",
|
|
867
|
+
localControlScope: "moltnet:local-control",
|
|
868
|
+
provisioningAudience: "moltnet:provisioning",
|
|
869
|
+
localControlAudience: "moltnet:agent-server",
|
|
870
|
+
nativeClientId: "moltnet-native",
|
|
871
|
+
consoleClientId: "moltnet-console",
|
|
872
|
+
approvalTransportGraceSeconds: 30,
|
|
873
|
+
callbackPort: 17375,
|
|
874
|
+
consoleLifetimeSeconds: 900,
|
|
875
|
+
nativeLifetimeSeconds: 300,
|
|
876
|
+
serverPort: 17374
|
|
877
|
+
});
|
|
878
|
+
//#endregion
|
|
860
879
|
//#region ../../libs/models/src/preview-sign.ts
|
|
861
880
|
function schemaRef$2(schema, id) {
|
|
862
881
|
return Type.Unsafe(Type.Ref(id));
|
|
@@ -1208,7 +1227,8 @@ Type.Object({
|
|
|
1208
1227
|
});
|
|
1209
1228
|
Type.Object({
|
|
1210
1229
|
code: Type.String({ minLength: 1 }),
|
|
1211
|
-
issueAgentKey: Type.Optional(Type.Literal(true))
|
|
1230
|
+
issueAgentKey: Type.Optional(Type.Literal(true)),
|
|
1231
|
+
expectedTeamId: Type.Optional(UuidSchema)
|
|
1212
1232
|
});
|
|
1213
1233
|
Type.Object({ role: Type.Union([
|
|
1214
1234
|
Type.Literal("manager"),
|
|
@@ -1449,6 +1469,7 @@ var ProblemCodeSchema = Type.Union([
|
|
|
1449
1469
|
Type.Literal("FORBIDDEN"),
|
|
1450
1470
|
Type.Literal("NOT_FOUND"),
|
|
1451
1471
|
Type.Literal("CONFLICT"),
|
|
1472
|
+
Type.Literal("PROJECT_MISMATCH"),
|
|
1452
1473
|
Type.Literal("UNSUPPORTED_MEDIA_TYPE"),
|
|
1453
1474
|
Type.Literal("VALIDATION_FAILED"),
|
|
1454
1475
|
Type.Literal("INVALID_CHALLENGE"),
|
|
@@ -1529,6 +1550,34 @@ Type.Intersect([ConflictProblemDetailsSchema, Type.Object({ flagged: Type.Option
|
|
|
1529
1550
|
threats: Type.Array(Type.Ref("InjectionThreat"))
|
|
1530
1551
|
}, { additionalProperties: false }))) })], { $id: "InjectionConflictProblemDetails" });
|
|
1531
1552
|
Type.Intersect([ProblemDetailsSchema, Type.Object({ errors: Type.Array(Type.Ref("ValidationError")) })], { $id: "ValidationProblemDetails" });
|
|
1553
|
+
Type.Object({
|
|
1554
|
+
id: Type.String({ format: "uuid" }),
|
|
1555
|
+
teamId: Type.String({ format: "uuid" }),
|
|
1556
|
+
creatorAgentId: Type.Union([Type.String({ format: "uuid" }), Type.Null()]),
|
|
1557
|
+
creatorHumanId: Type.Union([Type.String({ format: "uuid" }), Type.Null()]),
|
|
1558
|
+
name: Type.String(),
|
|
1559
|
+
description: Type.Union([Type.String(), Type.Null()]),
|
|
1560
|
+
defaultDiaryId: Type.Union([Type.String({ format: "uuid" }), Type.Null()]),
|
|
1561
|
+
archived: Type.Boolean(),
|
|
1562
|
+
createdAt: Type.String({ format: "date-time" }),
|
|
1563
|
+
updatedAt: Type.String({ format: "date-time" })
|
|
1564
|
+
});
|
|
1565
|
+
var CreateProjectSchema = Type.Object({
|
|
1566
|
+
name: Type.String({
|
|
1567
|
+
minLength: 1,
|
|
1568
|
+
maxLength: 255,
|
|
1569
|
+
pattern: "\\S"
|
|
1570
|
+
}),
|
|
1571
|
+
description: Type.Optional(Type.Union([Type.String({ maxLength: 1e4 }), Type.Null()])),
|
|
1572
|
+
defaultDiaryId: Type.Optional(Type.Union([Type.String({ format: "uuid" }), Type.Null()]))
|
|
1573
|
+
}, { additionalProperties: false });
|
|
1574
|
+
Type.Object({
|
|
1575
|
+
...Type.Partial(CreateProjectSchema).properties,
|
|
1576
|
+
archived: Type.Optional(Type.Boolean())
|
|
1577
|
+
}, {
|
|
1578
|
+
additionalProperties: false,
|
|
1579
|
+
minProperties: 1
|
|
1580
|
+
});
|
|
1532
1581
|
Type.Union([
|
|
1533
1582
|
Type.Literal("pack"),
|
|
1534
1583
|
Type.Literal("entry"),
|
|
@@ -1967,7 +2016,7 @@ var RuntimeProfileMaxBashTimeouts = Type.Integer({
|
|
|
1967
2016
|
minimum: 0,
|
|
1968
2017
|
maximum: 1e3
|
|
1969
2018
|
});
|
|
1970
|
-
Type.Object({
|
|
2019
|
+
var RuntimeProfile = Type.Object({
|
|
1971
2020
|
id: Type.String({ format: "uuid" }),
|
|
1972
2021
|
teamId: Type.String({ format: "uuid" }),
|
|
1973
2022
|
name: RuntimeProfileName,
|
|
@@ -3353,6 +3402,7 @@ Type.Object({
|
|
|
3353
3402
|
title: Type.Union([Type.String(), Type.Null()]),
|
|
3354
3403
|
tags: Type.Array(Type.String()),
|
|
3355
3404
|
teamId: Uuid,
|
|
3405
|
+
projectId: Type.Union([Uuid, Type.Null()]),
|
|
3356
3406
|
diaryId: Type.Union([Uuid, Type.Null()]),
|
|
3357
3407
|
outputKind: OutputKind,
|
|
3358
3408
|
input: Type.Record(Type.String(), Type.Unknown()),
|
|
@@ -3461,279 +3511,28 @@ Type.Object({
|
|
|
3461
3511
|
additionalProperties: false
|
|
3462
3512
|
});
|
|
3463
3513
|
//#endregion
|
|
3464
|
-
//#region src/lib/
|
|
3465
|
-
|
|
3466
|
-
|
|
3467
|
-
|
|
3468
|
-
|
|
3469
|
-
|
|
3470
|
-
|
|
3471
|
-
|
|
3472
|
-
|
|
3473
|
-
--sandbox <path> Deprecated. Remote runtime profiles define
|
|
3474
|
-
sandbox policy.
|
|
3475
|
-
--agent-root <path> Directory that owns .moltnet/<agent>. Default:
|
|
3476
|
-
CWD, with git root fallback when available.
|
|
3477
|
-
--git-author <"Name <email>">
|
|
3478
|
-
Non-secret git identity projected into the
|
|
3479
|
-
guest for host-brokered commit signing. Default:
|
|
3480
|
-
host git config. Configless agent-key runs must
|
|
3481
|
-
provide this flag or MOLTNET_GIT_AUTHOR.
|
|
3482
|
-
Env: MOLTNET_GIT_AUTHOR.
|
|
3483
|
-
--heartbeat-interval-ms <n> Reporter heartbeat cadence. Default: 60000.
|
|
3484
|
-
--warm-retention-sec <n> Resumability window for runtime slots
|
|
3485
|
-
(Pi sessions + reusable worktrees) after use.
|
|
3486
|
-
Default: 1800.
|
|
3487
|
-
--debug Verbose logging: also log successful list/claim
|
|
3488
|
-
outcomes (candidate counts, claim attempts).`;
|
|
3489
|
-
var REGISTERED_TASK_TYPES = Object.keys(BUILT_IN_TASK_TYPES).sort();
|
|
3490
|
-
function knownTaskTypesList() {
|
|
3491
|
-
return REGISTERED_TASK_TYPES.join(", ");
|
|
3514
|
+
//#region src/lib/identity-pin.ts
|
|
3515
|
+
/** Compare every pinned field without choosing a caller-specific error type. */
|
|
3516
|
+
function assessIdentityPin(current, expected) {
|
|
3517
|
+
for (const [field, label] of [["publicKey", "public key"], ["fingerprint", "fingerprint"]]) if (!current[field] || current[field] !== expected[field]) return {
|
|
3518
|
+
ok: false,
|
|
3519
|
+
field,
|
|
3520
|
+
label
|
|
3521
|
+
};
|
|
3522
|
+
return { ok: true };
|
|
3492
3523
|
}
|
|
3493
|
-
|
|
3494
|
-
|
|
3495
|
-
|
|
3496
|
-
|
|
3497
|
-
|
|
3498
|
-
|
|
3499
|
-
|
|
3500
|
-
|
|
3501
|
-
|
|
3502
|
-
|
|
3503
|
-
|
|
3504
|
-
|
|
3505
|
-
matching the configured filter until SIGINT/SIGTERM.
|
|
3506
|
-
once Claim and execute one specific queued task by id, then exit.
|
|
3507
|
-
drain Poll until the queue has nothing claimable, then exit.
|
|
3508
|
-
Useful for batch eval runs and demos.
|
|
3509
|
-
server Loopback supervisor for console-managed runs: pairing,
|
|
3510
|
-
agent/provider config store, and start/stop of poll/drain
|
|
3511
|
-
child processes. Binds 127.0.0.1 only.
|
|
3512
|
-
server trust
|
|
3513
|
-
Install the per-user macOS local-HTTPS CA after explicit consent.
|
|
3514
|
-
providers Manage configured endpoints and Pi OAuth subscriptions without
|
|
3515
|
-
starting the Agent Server. See \`agent-daemon providers --help\`.
|
|
3516
|
-
sync-sessions
|
|
3517
|
-
Repair durable runtime-session checkpoints from local slot files.
|
|
3518
|
-
update check
|
|
3519
|
-
Check the stable MoltNet agent release without reading credentials.
|
|
3520
|
-
|
|
3521
|
-
Run \`agent-daemon <command> --help\` for command-specific flags.
|
|
3522
|
-
|
|
3523
|
-
Prerequisites:
|
|
3524
|
-
- configless: MOLTNET_AGENT_KEY (or MOLTNET_AGENT_KEY_REF) and
|
|
3525
|
-
MOLTNET_PRIVATE_KEY (or MOLTNET_PRIVATE_KEY_REF); no agent files
|
|
3526
|
-
- config-based: ~/.config/moltnet/identities/<agent>/moltnet.json
|
|
3527
|
-
carrying agent_key_refs or agent_key_ref (OAuth2 is not accepted)
|
|
3528
|
-
--agent-root explicitly selects a legacy .moltnet/<agent> bundle
|
|
3529
|
-
|
|
3530
|
-
No key yet? Mint one with the CLI (--store writes the team slot into
|
|
3531
|
-
moltnet.json and keeps the secret in a provider):
|
|
3532
|
-
|
|
3533
|
-
moltnet teams list # find the team id
|
|
3534
|
-
moltnet agents keys create --team-id <team-uuid> \\
|
|
3535
|
-
--name <agent>-daemon --store
|
|
3536
|
-
|
|
3537
|
-
https://docs.themolt.net/operate/agent-keys#run-the-daemon-with-an-agent-key
|
|
3538
|
-
- --profile — remote runtime profile supplies provider/model/sandbox
|
|
3539
|
-
policy and CWD is used as the VM mountPath.
|
|
3540
|
-
|
|
3541
|
-
Registered task types: ${knownTaskTypesList()}`;
|
|
3542
|
-
var POLL_HELP = `\
|
|
3543
|
-
agent-daemon poll — long-running task worker.
|
|
3544
|
-
|
|
3545
|
-
Usage:
|
|
3546
|
-
agent-daemon poll --team <uuid> --agent <name> --profile <uuid|name> [...]
|
|
3547
|
-
|
|
3548
|
-
Required:
|
|
3549
|
-
--team <uuid> Team whose queue to serve. The daemon must be
|
|
3550
|
-
a member of this team (canAccessTeam permit).
|
|
3551
|
-
${COMMON_REQUIRED_FLAGS}
|
|
3552
|
-
|
|
3553
|
-
Optional:
|
|
3554
|
-
--task-types <csv> Whitelist of task types to claim. Default:
|
|
3555
|
-
accept any registered type. Known types:
|
|
3556
|
-
${knownTaskTypesList()}
|
|
3557
|
-
--correlation-id <uuid> Restrict claims to one orchestration run.
|
|
3558
|
-
--diary-ids <csv> Further client-side filter on task.diaryId.
|
|
3559
|
-
--poll-interval-ms <n> Idle backoff floor. Default: 2000.
|
|
3560
|
-
--max-poll-interval-ms <n> Idle backoff ceiling. Default: 30000.
|
|
3561
|
-
--list-limit <n> Page size per list call. Default: 10.
|
|
3562
|
-
${COMMON_OPTIONAL_FLAGS}
|
|
3563
|
-
|
|
3564
|
-
Example:
|
|
3565
|
-
agent-daemon poll \\
|
|
3566
|
-
--team 6743b4b1-6b93-46e2-a048-19490f04f91a \\
|
|
3567
|
-
--task-types curate_pack,fulfill_brief \\
|
|
3568
|
-
--agent legreffier \\
|
|
3569
|
-
--profile github-linear \\
|
|
3570
|
-
--profile local-fallback
|
|
3571
|
-
|
|
3572
|
-
Stops cleanly on SIGINT/SIGTERM (drains the in-flight task before exit).`;
|
|
3573
|
-
var ONCE_HELP = `\
|
|
3574
|
-
agent-daemon once — execute one specific queued task by id, then exit.
|
|
3575
|
-
|
|
3576
|
-
Usage:
|
|
3577
|
-
agent-daemon once --task-id <uuid> --agent <name> --profile <uuid|name> [...]
|
|
3578
|
-
|
|
3579
|
-
Required:
|
|
3580
|
-
-t, --task-id <uuid> Task to claim and execute. Must already be
|
|
3581
|
-
in 'queued' status.
|
|
3582
|
-
${COMMON_REQUIRED_FLAGS}
|
|
3583
|
-
|
|
3584
|
-
Optional:
|
|
3585
|
-
--team <uuid> Team scope for resolving --profile by name.
|
|
3586
|
-
Required only when --profile is a name.
|
|
3587
|
-
${COMMON_OPTIONAL_FLAGS}
|
|
3588
|
-
|
|
3589
|
-
Example:
|
|
3590
|
-
agent-daemon once \\
|
|
3591
|
-
--task-id 26004a77-bc10-43ef-a79f-c8e62faf59b1 \\
|
|
3592
|
-
--agent legreffier \\
|
|
3593
|
-
--profile github-linear
|
|
3594
|
-
|
|
3595
|
-
Exits 0 on completed, 1 on failed/cancelled/runtime-error.`;
|
|
3596
|
-
var DRAIN_HELP = `\
|
|
3597
|
-
agent-daemon drain — poll until the queue is empty, then exit.
|
|
3598
|
-
|
|
3599
|
-
Usage:
|
|
3600
|
-
agent-daemon drain --team <uuid> --agent <name> --profile <uuid|name> [...]
|
|
3601
|
-
|
|
3602
|
-
Same flags as \`poll\`. The only behavioural difference: \`drain\` exits
|
|
3603
|
-
when a list call confirms no claimable tasks remain (vs \`poll\` which
|
|
3604
|
-
sleeps and retries forever).
|
|
3605
|
-
|
|
3606
|
-
Required:
|
|
3607
|
-
--team <uuid> Team whose queue to drain.
|
|
3608
|
-
${COMMON_REQUIRED_FLAGS}
|
|
3609
|
-
|
|
3610
|
-
Optional:
|
|
3611
|
-
--task-types <csv> Whitelist. Known types: ${knownTaskTypesList()}
|
|
3612
|
-
--correlation-id <uuid> Restrict claims to one orchestration run.
|
|
3613
|
-
--wait-for-first-task-sec <n>
|
|
3614
|
-
Wait this long for an initially empty run before
|
|
3615
|
-
exiting. After the first claim, exit on empty.
|
|
3616
|
-
--wait-after-task-sec <n> Require the queue to remain empty for this long
|
|
3617
|
-
after a claim before exiting.
|
|
3618
|
-
--diary-ids <csv> Diary filter.
|
|
3619
|
-
--poll-interval-ms <n> Default: 2000.
|
|
3620
|
-
--max-poll-interval-ms <n> Default: 30000.
|
|
3621
|
-
--list-limit <n> Default: 10.
|
|
3622
|
-
${COMMON_OPTIONAL_FLAGS}
|
|
3623
|
-
|
|
3624
|
-
Example:
|
|
3625
|
-
agent-daemon drain \\
|
|
3626
|
-
--team 6743b4b1-6b93-46e2-a048-19490f04f91a \\
|
|
3627
|
-
--task-types judge_pack \\
|
|
3628
|
-
--agent legreffier \\
|
|
3629
|
-
--profile eval-judge`;
|
|
3630
|
-
var SYNC_SESSIONS_HELP = `\
|
|
3631
|
-
agent-daemon sync-sessions — repair durable runtime-session checkpoints.
|
|
3632
|
-
|
|
3633
|
-
Usage:
|
|
3634
|
-
agent-daemon sync-sessions --team <uuid> --agent <name> [...]
|
|
3635
|
-
|
|
3636
|
-
Scans this daemon's team-scoped runtime slots, compares local Pi session files
|
|
3637
|
-
with durable runtime-session metadata, and uploads missing or stale checkpoints.
|
|
3638
|
-
|
|
3639
|
-
Required:
|
|
3640
|
-
--team <uuid> Team whose runtime slots to inspect.
|
|
3641
|
-
-a, --agent <name> MoltNet agent identity. Reads credentials
|
|
3642
|
-
from <agent-root>/.moltnet/<name>/moltnet.json.
|
|
3643
|
-
|
|
3644
|
-
Optional:
|
|
3645
|
-
--runtime-profile-id <uuid> Limit repair to one runtime profile.
|
|
3646
|
-
--state <active|idle> Limit scanned slots by state. Default: all.
|
|
3647
|
-
--limit <n> Max slots to scan, 1..200. Default: 100.
|
|
3648
|
-
--dry-run Report missing/stale sessions without uploading.
|
|
3649
|
-
--agent-root <path> Directory that owns .moltnet/<agent>. Default:
|
|
3650
|
-
CWD, with git root fallback when available.
|
|
3651
|
-
--debug Accepted for consistency; no extra output yet.
|
|
3652
|
-
|
|
3653
|
-
Example:
|
|
3654
|
-
agent-daemon sync-sessions \\
|
|
3655
|
-
--team 6743b4b1-6b93-46e2-a048-19490f04f91a \\
|
|
3656
|
-
--agent legreffier \\
|
|
3657
|
-
--state idle`;
|
|
3658
|
-
function isHelpFlag(args) {
|
|
3659
|
-
return args.includes("--help") || args.includes("-h");
|
|
3660
|
-
}
|
|
3661
|
-
var AGENT_SERVER_HELP = `\
|
|
3662
|
-
agent-daemon server — loopback supervisor for console-managed runs.
|
|
3663
|
-
|
|
3664
|
-
Binds 127.0.0.1 only. A paired Console origin configures agents and
|
|
3665
|
-
providers (secret references only) and starts/stops poll/drain runs as
|
|
3666
|
-
child processes of this supervisor.
|
|
3667
|
-
|
|
3668
|
-
Options:
|
|
3669
|
-
--port <n> Loopback port. Default: 17374.
|
|
3670
|
-
Env: MOLTNET_AGENT_SERVER_PORT.
|
|
3671
|
-
--allowed-origins <csv> Exact Console origins allowed to pair.
|
|
3672
|
-
Default: https://console.themolt.net.
|
|
3673
|
-
Env: MOLTNET_AGENT_SERVER_ALLOWED_ORIGINS.
|
|
3674
|
-
--root <path> Config root. Default: ~/.config/moltnet
|
|
3675
|
-
(or MOLTNET_AGENT_SERVER_ROOT).
|
|
3676
|
-
--api-url <url> Default MoltNet API for new managed agents.
|
|
3677
|
-
Default: https://api.themolt.net.
|
|
3678
|
-
--heartbeat-interval-ms <n> Child reporter heartbeat cadence. Default: 60000.
|
|
3679
|
-
--warm-retention-sec <n> Child session/workspace retention. Default: 1800.
|
|
3680
|
-
--supervised Also stop gracefully when stdin reaches EOF.
|
|
3681
|
-
|
|
3682
|
-
On macOS, the first interactive run asks to trust a per-user local CA in the
|
|
3683
|
-
login keychain and serves HTTPS. Native supervisors use:
|
|
3684
|
-
server trust --status --json
|
|
3685
|
-
server trust --yes --json
|
|
3686
|
-
server trust --remove --yes --json
|
|
3687
|
-
Run \`agent-daemon server trust --remove\` interactively to remove that exact
|
|
3688
|
-
CA. Linux continues to use the Chromium PNA HTTP path.
|
|
3689
|
-
`;
|
|
3690
|
-
var PROVIDERS_HELP = `\
|
|
3691
|
-
moltnet-agent providers — manage local model providers.
|
|
3692
|
-
|
|
3693
|
-
Usage:
|
|
3694
|
-
moltnet-agent providers list [--json] [--root <path>]
|
|
3695
|
-
moltnet-agent providers set <id> [--base-url <url>] [--api <pi-api-kind>]
|
|
3696
|
-
[--model <id> ... | --clear-models]
|
|
3697
|
-
[--model-input <id>=text,image ...]
|
|
3698
|
-
[--api-key-stdin | --clear-api-key] [--root <path>]
|
|
3699
|
-
moltnet-agent providers discover <id> [--save] [--json] [--root <path>]
|
|
3700
|
-
moltnet-agent providers remove <id> [--yes] [--root <path>]
|
|
3701
|
-
moltnet-agent providers login <id> [--auth-method <method-id>]
|
|
3702
|
-
[--root <path>]
|
|
3703
|
-
moltnet-agent providers logout <id> [--yes] [--root <path>]
|
|
3704
|
-
|
|
3705
|
-
The default root is ~/.config/moltnet. MOLTNET_AGENT_SERVER_ROOT remains the
|
|
3706
|
-
environment override. API keys are accepted only from redirected stdin; they
|
|
3707
|
-
are stored separately and providers.json contains only a secret reference.
|
|
3708
|
-
|
|
3709
|
-
--model declares a text-only model. --model-input declares a model together
|
|
3710
|
-
with the input modalities it accepts, and is what makes a vision model usable:
|
|
3711
|
-
a model with no declared modalities is text-only to Pi, which drops image
|
|
3712
|
-
content parts before the request leaves the runtime.
|
|
3713
|
-
`;
|
|
3714
|
-
//#endregion
|
|
3715
|
-
//#region src/lib/identity-pin.ts
|
|
3716
|
-
/** Compare every pinned field without choosing a caller-specific error type. */
|
|
3717
|
-
function assessIdentityPin(current, expected) {
|
|
3718
|
-
for (const [field, label] of [["publicKey", "public key"], ["fingerprint", "fingerprint"]]) if (!current[field] || current[field] !== expected[field]) return {
|
|
3719
|
-
ok: false,
|
|
3720
|
-
field,
|
|
3721
|
-
label
|
|
3722
|
-
};
|
|
3723
|
-
return { ok: true };
|
|
3724
|
-
}
|
|
3725
|
-
function assessAgentStartupPin(current, expected) {
|
|
3726
|
-
for (const [field, label] of [
|
|
3727
|
-
["subjectId", "subject id"],
|
|
3728
|
-
["subjectType", "subject type"],
|
|
3729
|
-
["publicKey", "public key"],
|
|
3730
|
-
["fingerprint", "fingerprint"]
|
|
3731
|
-
]) if (!current[field] || current[field] !== expected[field]) return {
|
|
3732
|
-
ok: false,
|
|
3733
|
-
field,
|
|
3734
|
-
label
|
|
3735
|
-
};
|
|
3736
|
-
return { ok: true };
|
|
3524
|
+
function assessAgentStartupPin(current, expected) {
|
|
3525
|
+
for (const [field, label] of [
|
|
3526
|
+
["subjectId", "subject id"],
|
|
3527
|
+
["subjectType", "subject type"],
|
|
3528
|
+
["publicKey", "public key"],
|
|
3529
|
+
["fingerprint", "fingerprint"]
|
|
3530
|
+
]) if (!current[field] || current[field] !== expected[field]) return {
|
|
3531
|
+
ok: false,
|
|
3532
|
+
field,
|
|
3533
|
+
label
|
|
3534
|
+
};
|
|
3535
|
+
return { ok: true };
|
|
3737
3536
|
}
|
|
3738
3537
|
/** A configured team slot must authenticate with that exact team ceiling. */
|
|
3739
3538
|
function matchesCredentialTeam(current, credentialTeamId) {
|
|
@@ -3748,7 +3547,7 @@ function matchesCredentialTeam(current, credentialTeamId) {
|
|
|
3748
3547
|
* `docs/.vitepress/config.ts`.
|
|
3749
3548
|
*/
|
|
3750
3549
|
var AGENT_KEYS_DOC_URL = "https://docs.themolt.net/operate/agent-keys#run-the-daemon-with-an-agent-key";
|
|
3751
|
-
/** Scopes a daemon key
|
|
3550
|
+
/** Scopes a new daemon key should be minted with; mirrors `DAEMON_RECOMMENDED_SCOPES`. */
|
|
3752
3551
|
var DAEMON_KEY_SCOPES = AGENT_CREDENTIAL_SCOPES;
|
|
3753
3552
|
/**
|
|
3754
3553
|
* Report where `connect()` will find the key, without ever reading the secret
|
|
@@ -3827,14 +3626,24 @@ async function validateStartupBinding(options) {
|
|
|
3827
3626
|
async function resolveAgentContext(agentName, options = {}) {
|
|
3828
3627
|
assertIdentityAlias(agentName);
|
|
3829
3628
|
const { agentDir, agentRootDir } = resolveIdentityLocation(agentName, options.agentRootDir, { requireConfig: options.credentialSource !== "environment" });
|
|
3830
|
-
|
|
3831
|
-
|
|
3832
|
-
|
|
3833
|
-
|
|
3834
|
-
|
|
3835
|
-
|
|
3629
|
+
const projectApiUrl = options.projectApiUrl;
|
|
3630
|
+
if (options.credentialSource === "environment") {
|
|
3631
|
+
if (projectApiUrl) assertTrustedConfigApiUrl(projectApiUrl, options.envApiUrl?.trim() || "https://api.themolt.net");
|
|
3632
|
+
return {
|
|
3633
|
+
agentDir,
|
|
3634
|
+
agentRootDir,
|
|
3635
|
+
agent: await connect({
|
|
3636
|
+
...projectApiUrl ? { apiUrl: projectApiUrl } : {},
|
|
3637
|
+
secretProviders: createNodeSecretProviderRegistry()
|
|
3638
|
+
}),
|
|
3639
|
+
credentialSource: "environment"
|
|
3640
|
+
};
|
|
3641
|
+
}
|
|
3836
3642
|
const config = await readConfig(agentDir);
|
|
3837
3643
|
if (!config || !hasAgentKeyConfiguration(config)) throw new Error(agentKeyRequiredMessage(agentDir, agentName));
|
|
3644
|
+
const configuredApiUrl = resolveConfigApiUrl(config, options.envApiUrl);
|
|
3645
|
+
if (options.envApiUrl?.trim()) requireSecureCredentialApiUrl(options.envApiUrl);
|
|
3646
|
+
if (projectApiUrl) assertTrustedConfigApiUrl(projectApiUrl, options.envApiUrl?.trim() || configuredApiUrl || "https://api.themolt.net");
|
|
3838
3647
|
const secretProviders = createNodeSecretProviderRegistry();
|
|
3839
3648
|
const agentKey = await resolveAgentKey(config, secretProviders, options.teamId);
|
|
3840
3649
|
if (!agentKey) throw new Error(agentKeyRequiredMessage(agentDir, agentName));
|
|
@@ -3845,7 +3654,7 @@ async function resolveAgentContext(agentName, options = {}) {
|
|
|
3845
3654
|
configDir: agentDir,
|
|
3846
3655
|
secretProviders,
|
|
3847
3656
|
agentKey,
|
|
3848
|
-
apiUrl:
|
|
3657
|
+
apiUrl: projectApiUrl ?? configuredApiUrl
|
|
3849
3658
|
}),
|
|
3850
3659
|
credentialSource: "config",
|
|
3851
3660
|
credentialTeamId: selectAgentKeyReference(config, options.teamId)?.teamId
|
|
@@ -3934,6 +3743,13 @@ function isTransientWhoamiError(error) {
|
|
|
3934
3743
|
const statusCode = error.statusCode;
|
|
3935
3744
|
return typeof statusCode === "number" && (statusCode === 408 || statusCode === 429 || statusCode >= 500);
|
|
3936
3745
|
}
|
|
3746
|
+
/** Read only non-secret endpoint metadata before selecting a team credential. */
|
|
3747
|
+
async function resolveSelectionApiUrl(agentName, options) {
|
|
3748
|
+
if (options.envApiUrl?.trim()) return options.envApiUrl.trim();
|
|
3749
|
+
if (options.credentialSource === "environment") throw new Error("Set MOLTNET_API_URL for an environment-key worker");
|
|
3750
|
+
const { agentDir } = resolveIdentityLocation(agentName, options.agentRootDir, { requireConfig: true });
|
|
3751
|
+
return resolveConfigApiUrl(await readConfig(agentDir) ?? {}) ?? "https://api.themolt.net";
|
|
3752
|
+
}
|
|
3937
3753
|
//#endregion
|
|
3938
3754
|
//#region src/config.ts
|
|
3939
3755
|
/**
|
|
@@ -4002,7 +3818,19 @@ function activatePiCodingAgentDir(path, env = {}) {
|
|
|
4002
3818
|
Object.assign(process.env, env);
|
|
4003
3819
|
}
|
|
4004
3820
|
function loadAgentServerEnvConfig() {
|
|
3821
|
+
const issuer = process.env["MOLTNET_OPERATOR_OAUTH_ISSUER"];
|
|
3822
|
+
const publicUrl = process.env["MOLTNET_OPERATOR_OAUTH_PUBLIC_URL"] ?? issuer;
|
|
3823
|
+
const nativeClientId = process.env["MOLTNET_NATIVE_OAUTH_CLIENT_ID"];
|
|
3824
|
+
const consoleClientId = process.env["MOLTNET_CONSOLE_OAUTH_CLIENT_ID"];
|
|
3825
|
+
const apiUrl = process.env["MOLTNET_OPERATOR_API_URL"];
|
|
4005
3826
|
return {
|
|
3827
|
+
operatorOAuth: {
|
|
3828
|
+
...issuer ? { issuer } : {},
|
|
3829
|
+
...publicUrl ? { publicUrl } : {},
|
|
3830
|
+
...nativeClientId ? { nativeClientId } : {},
|
|
3831
|
+
...consoleClientId ? { consoleClientId } : {},
|
|
3832
|
+
...apiUrl ? { apiUrl } : {}
|
|
3833
|
+
},
|
|
4006
3834
|
port: process.env["MOLTNET_AGENT_SERVER_PORT"] ?? "",
|
|
4007
3835
|
allowedOrigins: process.env["MOLTNET_AGENT_SERVER_ALLOWED_ORIGINS"] ?? "",
|
|
4008
3836
|
root: process.env["MOLTNET_AGENT_SERVER_ROOT"] ?? "",
|
|
@@ -4022,6 +3850,378 @@ function loadUpdateEnvConfig() {
|
|
|
4022
3850
|
};
|
|
4023
3851
|
}
|
|
4024
3852
|
//#endregion
|
|
3853
|
+
//#region src/lib/run-project-selection.ts
|
|
3854
|
+
var execFileAsync$2 = promisify(execFile);
|
|
3855
|
+
var GIT_TIMEOUT_MS = 1e4;
|
|
3856
|
+
var GIT_MAX_OUTPUT_BYTES = 64 * 1024;
|
|
3857
|
+
async function validateGitSource(source) {
|
|
3858
|
+
const inherited = processEnvSnapshot();
|
|
3859
|
+
try {
|
|
3860
|
+
const { stdout } = await execFileAsync$2("git", [
|
|
3861
|
+
"rev-parse",
|
|
3862
|
+
"--show-toplevel",
|
|
3863
|
+
"--verify",
|
|
3864
|
+
"HEAD^{commit}"
|
|
3865
|
+
], {
|
|
3866
|
+
cwd: source,
|
|
3867
|
+
env: {
|
|
3868
|
+
PATH: inherited.PATH,
|
|
3869
|
+
HOME: inherited.HOME,
|
|
3870
|
+
GIT_CONFIG_NOSYSTEM: "1",
|
|
3871
|
+
GIT_CONFIG_GLOBAL: "/dev/null"
|
|
3872
|
+
},
|
|
3873
|
+
timeout: GIT_TIMEOUT_MS,
|
|
3874
|
+
killSignal: "SIGKILL",
|
|
3875
|
+
maxBuffer: GIT_MAX_OUTPUT_BYTES
|
|
3876
|
+
});
|
|
3877
|
+
const top = stdout.trim().split("\n")[0];
|
|
3878
|
+
if (await canonicalDirectory(top) === source) return;
|
|
3879
|
+
} catch (cause) {
|
|
3880
|
+
throw new ProjectConfigError("selection", `git-worktree source ${source} requires a Git repository root with a committed revision: ${cause instanceof Error ? cause.message : String(cause)}`, { cause });
|
|
3881
|
+
}
|
|
3882
|
+
throw new ProjectConfigError("selection", `git-worktree source ${source} must be the Git repository root, not a subdirectory`);
|
|
3883
|
+
}
|
|
3884
|
+
function projectRunOptionDefs() {
|
|
3885
|
+
return {
|
|
3886
|
+
project: { type: "string" },
|
|
3887
|
+
binding: { type: "string" },
|
|
3888
|
+
general: { type: "boolean" },
|
|
3889
|
+
"config-file": { type: "string" },
|
|
3890
|
+
"state-dir": { type: "string" },
|
|
3891
|
+
source: { type: "string" },
|
|
3892
|
+
"workspace-strategy": { type: "string" }
|
|
3893
|
+
};
|
|
3894
|
+
}
|
|
3895
|
+
function strategy(value) {
|
|
3896
|
+
if (value === void 0) return void 0;
|
|
3897
|
+
if (WORKSPACE_STRATEGIES.includes(value)) return value;
|
|
3898
|
+
throw new ProjectConfigError("validation", `Unknown workspace strategy ${value}; choose ${WORKSPACE_STRATEGIES.join(", ")}`);
|
|
3899
|
+
}
|
|
3900
|
+
/** Resolve once at worker startup. No credentials, remote calls, hooks or workspace creation. */
|
|
3901
|
+
async function resolveRunProjectSelection(args) {
|
|
3902
|
+
const env = processEnvSnapshot();
|
|
3903
|
+
const inherited = env.MOLTNET_ACTIVE_IDENTITY === args.agent && !args.general;
|
|
3904
|
+
const bindingName = args.binding ?? (!args["config-file"] && !args.project && inherited ? env.MOLTNET_PROJECT_BINDING : void 0);
|
|
3905
|
+
const projectId = args.project ?? (!args["config-file"] && !args.binding && inherited ? env.MOLTNET_PROJECT_ID : void 0);
|
|
3906
|
+
const configPath = resolve(args.cwd, args["config-file"] ?? (inherited ? env.MOLTNET_PROJECT_CONFIG : void 0) ?? getProjectConfigPath());
|
|
3907
|
+
if (args.general && (args.project || args.binding)) throw new ProjectConfigError("selection", "General work cannot also declare a project or binding");
|
|
3908
|
+
const overrideStrategy = strategy(args["workspace-strategy"]);
|
|
3909
|
+
const stateRootDir = args["state-dir"] ? resolve(args.cwd, args["state-dir"]) : void 0;
|
|
3910
|
+
let binding = null;
|
|
3911
|
+
if (!args.general) try {
|
|
3912
|
+
binding = await resolveProjectBinding(await readProjectConfig(configPath), {
|
|
3913
|
+
configPath,
|
|
3914
|
+
cwd: args.cwd,
|
|
3915
|
+
binding: bindingName,
|
|
3916
|
+
projectId,
|
|
3917
|
+
native: !bindingName && !projectId,
|
|
3918
|
+
teamId: args.team,
|
|
3919
|
+
apiUrl: args.apiUrl || void 0,
|
|
3920
|
+
overrides: {
|
|
3921
|
+
...args.source === void 0 ? {} : { source: args.source },
|
|
3922
|
+
...overrideStrategy === void 0 ? {} : { strategy: overrideStrategy }
|
|
3923
|
+
}
|
|
3924
|
+
});
|
|
3925
|
+
if (!binding && (bindingName || projectId)) throw new ProjectConfigError("selection", `No matching project binding ${bindingName ?? projectId} in ${configPath} for endpoint ${args.apiUrl ?? "(unspecified)"}; run moltnet projects setup or select --binding/--general`);
|
|
3926
|
+
} catch (cause) {
|
|
3927
|
+
throw new ProjectConfigError(cause instanceof ProjectConfigError ? cause.kind : "selection", `Project selection in ${configPath} (binding ${bindingName ?? projectId ?? "ancestor"}, endpoint ${args.apiUrl ?? "unspecified"}): ${cause instanceof Error ? cause.message : String(cause)}. Use moltnet projects setup to register a folder, or select --binding/--general.`, { cause });
|
|
3928
|
+
}
|
|
3929
|
+
const workspaceStrategy = binding?.strategy ?? overrideStrategy ?? "existing";
|
|
3930
|
+
if (workspaceStrategy === "none" && args.source !== void 0) throw new ProjectConfigError("selection", "No-workspace execution cannot specify a source");
|
|
3931
|
+
let source = binding?.source;
|
|
3932
|
+
if (workspaceStrategy !== "none" && !source) source = await canonicalDirectory(resolve(args.cwd, args.source ?? "."));
|
|
3933
|
+
if (workspaceStrategy === "isolated-directory" || binding?.hooks?.afterCreate || binding?.hooks?.beforeRun) throw new ProjectConfigError("selection", `Binding ${binding?.name ?? "(run override)"} in ${configPath}: this runtime does not support isolated-directory preparation or setup hooks; choose a supported binding`);
|
|
3934
|
+
if (workspaceStrategy === "git-worktree" && source) await validateGitSource(source);
|
|
3935
|
+
return {
|
|
3936
|
+
configPath,
|
|
3937
|
+
selectedBy: args.binding || args.project ? "explicit" : bindingName ? "activation" : binding ? "ancestor" : "general",
|
|
3938
|
+
projectId: binding?.projectId ?? null,
|
|
3939
|
+
teamId: binding?.teamId ?? args.team,
|
|
3940
|
+
apiUrl: binding?.apiUrl ?? (args.apiUrl || void 0),
|
|
3941
|
+
...binding ? { binding } : {},
|
|
3942
|
+
source,
|
|
3943
|
+
strategy: workspaceStrategy,
|
|
3944
|
+
stateRootDir,
|
|
3945
|
+
workspaceExplicit: Boolean(binding || overrideStrategy || args.source)
|
|
3946
|
+
};
|
|
3947
|
+
}
|
|
3948
|
+
/** A selected location fixes this worker's strategy; saved profiles are never mutated. */
|
|
3949
|
+
function applyProjectWorkspacePolicy(profile, selection) {
|
|
3950
|
+
if (!selection.workspaceExplicit) return profile;
|
|
3951
|
+
if (selection.strategy === "isolated-directory") throw new Error("This runtime does not yet support isolated-directory preparation");
|
|
3952
|
+
if (selection.binding?.hooks?.afterCreate || selection.binding?.hooks?.beforeRun) throw new Error("This runtime does not yet support project setup hooks");
|
|
3953
|
+
const mode = selection.strategy === "existing" ? "shared_mount" : selection.strategy === "git-worktree" ? "dedicated_worktree" : "none";
|
|
3954
|
+
if (profile.allowedWorkspaceModes.length && !profile.allowedWorkspaceModes.includes(mode)) throw new Error(`Workspace strategy ${selection.strategy} is not allowed by profile ${profile.name}`);
|
|
3955
|
+
return {
|
|
3956
|
+
...profile,
|
|
3957
|
+
mountPath: selection.source ?? profile.mountPath,
|
|
3958
|
+
defaultWorkspaceMode: mode,
|
|
3959
|
+
allowedWorkspaceModes: [mode]
|
|
3960
|
+
};
|
|
3961
|
+
}
|
|
3962
|
+
var PROJECT_RUN_FLAGS = ` --binding <name> Select a saved local project location.
|
|
3963
|
+
--project <uuid> Select a project and its unambiguous binding.
|
|
3964
|
+
--general Serve General work (projectId: null).
|
|
3965
|
+
--config-file <path> Explicit project bindings JSON.
|
|
3966
|
+
--source <path> Run-only source folder override.
|
|
3967
|
+
--workspace-strategy <name> existing, git-worktree, none; isolated-directory
|
|
3968
|
+
is reserved and currently unsupported.
|
|
3969
|
+
--state-dir <path> Supervisor/session state, separate from source.
|
|
3970
|
+
Default: profile mount root (existing state retained).`;
|
|
3971
|
+
//#endregion
|
|
3972
|
+
//#region src/lib/help.ts
|
|
3973
|
+
var COMMON_REQUIRED_FLAGS = `\
|
|
3974
|
+
-a, --agent <name> MoltNet agent identity. Agent-key auth is
|
|
3975
|
+
configless; OAuth2 reads moltnet.json.
|
|
3976
|
+
--profile <uuid|name> Remote runtime profile. Repeat for poll/drain
|
|
3977
|
+
to declare priority order. Provider, model,
|
|
3978
|
+
sandbox policy, prerequisites, and runtime
|
|
3979
|
+
execution policy come from the selected profile.`;
|
|
3980
|
+
var COMMON_OPTIONAL_FLAGS = `\
|
|
3981
|
+
--sandbox <path> Deprecated. Remote runtime profiles define
|
|
3982
|
+
sandbox policy.
|
|
3983
|
+
--agent-root <path> Explicit legacy identity bundle location.
|
|
3984
|
+
Omitted: use the central identity store.
|
|
3985
|
+
${PROJECT_RUN_FLAGS}
|
|
3986
|
+
--git-author <"Name <email>">
|
|
3987
|
+
Non-secret git identity projected into the
|
|
3988
|
+
guest for host-brokered commit signing. Default:
|
|
3989
|
+
host git config. Configless agent-key runs must
|
|
3990
|
+
provide this flag or MOLTNET_GIT_AUTHOR.
|
|
3991
|
+
Env: MOLTNET_GIT_AUTHOR.
|
|
3992
|
+
--heartbeat-interval-ms <n> Reporter heartbeat cadence. Default: 60000.
|
|
3993
|
+
--warm-retention-sec <n> Resumability window for runtime slots
|
|
3994
|
+
(Pi sessions + reusable worktrees) after use.
|
|
3995
|
+
Default: 1800.
|
|
3996
|
+
--debug Verbose logging: also log successful list/claim
|
|
3997
|
+
outcomes (candidate counts, claim attempts).`;
|
|
3998
|
+
var REGISTERED_TASK_TYPES = Object.keys(BUILT_IN_TASK_TYPES).sort();
|
|
3999
|
+
function knownTaskTypesList() {
|
|
4000
|
+
return REGISTERED_TASK_TYPES.join(", ");
|
|
4001
|
+
}
|
|
4002
|
+
var ROOT_USAGE = `\
|
|
4003
|
+
agent-daemon — long-running task worker for MoltNet.
|
|
4004
|
+
|
|
4005
|
+
Usage: agent-daemon [--runtime <module>] <command> [...flags]
|
|
4006
|
+
|
|
4007
|
+
Runtime:
|
|
4008
|
+
--runtime <module> Trusted local file or installed package whose
|
|
4009
|
+
default export is a DaemonRuntimeAdapter.
|
|
4010
|
+
Omit to use the built-in gondolin_pi runtime.
|
|
4011
|
+
|
|
4012
|
+
Commands:
|
|
4013
|
+
poll Long-running worker. Polls the task queue and claims tasks
|
|
4014
|
+
matching the configured filter until SIGINT/SIGTERM.
|
|
4015
|
+
once Claim and execute one specific queued task by id, then exit.
|
|
4016
|
+
drain Poll until the queue has nothing claimable, then exit.
|
|
4017
|
+
Useful for batch eval runs and demos.
|
|
4018
|
+
server Loopback supervisor for console-managed runs: OAuth local control,
|
|
4019
|
+
agent/provider config store, and start/stop of poll/drain
|
|
4020
|
+
child processes. Binds 127.0.0.1 only.
|
|
4021
|
+
server trust
|
|
4022
|
+
Install the per-user macOS local-HTTPS CA after explicit consent.
|
|
4023
|
+
providers Manage configured endpoints and Pi OAuth subscriptions without
|
|
4024
|
+
starting the Agent Server. See \`agent-daemon providers --help\`.
|
|
4025
|
+
sync-sessions
|
|
4026
|
+
Repair durable runtime-session checkpoints from local slot files.
|
|
4027
|
+
update check
|
|
4028
|
+
Check the stable MoltNet agent release without reading credentials.
|
|
4029
|
+
|
|
4030
|
+
Run \`agent-daemon <command> --help\` for command-specific flags.
|
|
4031
|
+
|
|
4032
|
+
Prerequisites:
|
|
4033
|
+
- configless: MOLTNET_AGENT_KEY (or MOLTNET_AGENT_KEY_REF) and
|
|
4034
|
+
MOLTNET_PRIVATE_KEY (or MOLTNET_PRIVATE_KEY_REF); no agent files
|
|
4035
|
+
- config-based: ~/.config/moltnet/identities/<agent>/moltnet.json
|
|
4036
|
+
carrying agent_key_refs or agent_key_ref (OAuth2 is not accepted)
|
|
4037
|
+
--agent-root explicitly selects a legacy .moltnet/<agent> bundle
|
|
4038
|
+
|
|
4039
|
+
No key yet? Mint one with the CLI (--store writes the team slot into
|
|
4040
|
+
moltnet.json and keeps the secret in a provider):
|
|
4041
|
+
|
|
4042
|
+
moltnet teams list # find the team id
|
|
4043
|
+
moltnet agents keys create --team-id <team-uuid> \\
|
|
4044
|
+
--name <agent>-daemon --store
|
|
4045
|
+
|
|
4046
|
+
https://docs.themolt.net/operate/agent-keys#run-the-daemon-with-an-agent-key
|
|
4047
|
+
- --profile — remote runtime profile supplies provider/model/sandbox
|
|
4048
|
+
policy and CWD is used as the VM mountPath.
|
|
4049
|
+
|
|
4050
|
+
Registered task types: ${knownTaskTypesList()}`;
|
|
4051
|
+
var POLL_HELP = `\
|
|
4052
|
+
agent-daemon poll — long-running task worker.
|
|
4053
|
+
|
|
4054
|
+
Usage:
|
|
4055
|
+
agent-daemon poll --team <uuid> --agent <name> --profile <uuid|name> [...]
|
|
4056
|
+
|
|
4057
|
+
Required:
|
|
4058
|
+
--team <uuid> Team whose queue to serve. The daemon must be
|
|
4059
|
+
a member of this team (canAccessTeam permit).
|
|
4060
|
+
${COMMON_REQUIRED_FLAGS}
|
|
4061
|
+
|
|
4062
|
+
Optional:
|
|
4063
|
+
--task-types <csv> Whitelist of task types to claim. Default:
|
|
4064
|
+
accept any registered type. Known types:
|
|
4065
|
+
${knownTaskTypesList()}
|
|
4066
|
+
--correlation-id <uuid> Restrict claims to one orchestration run.
|
|
4067
|
+
--diary-ids <csv> Further client-side filter on task.diaryId.
|
|
4068
|
+
--poll-interval-ms <n> Idle backoff floor. Default: 2000.
|
|
4069
|
+
--max-poll-interval-ms <n> Idle backoff ceiling. Default: 30000.
|
|
4070
|
+
--list-limit <n> Page size per list call. Default: 10.
|
|
4071
|
+
${COMMON_OPTIONAL_FLAGS}
|
|
4072
|
+
|
|
4073
|
+
Example:
|
|
4074
|
+
agent-daemon poll \\
|
|
4075
|
+
--team 6743b4b1-6b93-46e2-a048-19490f04f91a \\
|
|
4076
|
+
--task-types curate_pack,fulfill_brief \\
|
|
4077
|
+
--agent legreffier \\
|
|
4078
|
+
--profile github-linear \\
|
|
4079
|
+
--profile local-fallback
|
|
4080
|
+
|
|
4081
|
+
Stops cleanly on SIGINT/SIGTERM (drains the in-flight task before exit).`;
|
|
4082
|
+
var ONCE_HELP = `\
|
|
4083
|
+
agent-daemon once — execute one specific queued task by id, then exit.
|
|
4084
|
+
|
|
4085
|
+
Usage:
|
|
4086
|
+
agent-daemon once --task-id <uuid> --agent <name> --profile <uuid|name> [...]
|
|
4087
|
+
|
|
4088
|
+
Required:
|
|
4089
|
+
-t, --task-id <uuid> Task to claim and execute. Must already be
|
|
4090
|
+
in 'queued' status.
|
|
4091
|
+
${COMMON_REQUIRED_FLAGS}
|
|
4092
|
+
|
|
4093
|
+
Optional:
|
|
4094
|
+
--team <uuid> Team scope for resolving --profile by name.
|
|
4095
|
+
Required only when --profile is a name.
|
|
4096
|
+
${COMMON_OPTIONAL_FLAGS}
|
|
4097
|
+
|
|
4098
|
+
Example:
|
|
4099
|
+
agent-daemon once \\
|
|
4100
|
+
--task-id 26004a77-bc10-43ef-a79f-c8e62faf59b1 \\
|
|
4101
|
+
--agent legreffier \\
|
|
4102
|
+
--profile github-linear
|
|
4103
|
+
|
|
4104
|
+
Exits 0 on completed, 1 on failed/cancelled/runtime-error.`;
|
|
4105
|
+
var DRAIN_HELP = `\
|
|
4106
|
+
agent-daemon drain — poll until the queue is empty, then exit.
|
|
4107
|
+
|
|
4108
|
+
Usage:
|
|
4109
|
+
agent-daemon drain --team <uuid> --agent <name> --profile <uuid|name> [...]
|
|
4110
|
+
|
|
4111
|
+
Same flags as \`poll\`. The only behavioural difference: \`drain\` exits
|
|
4112
|
+
when a list call confirms no claimable tasks remain (vs \`poll\` which
|
|
4113
|
+
sleeps and retries forever).
|
|
4114
|
+
|
|
4115
|
+
Required:
|
|
4116
|
+
--team <uuid> Team whose queue to drain.
|
|
4117
|
+
${COMMON_REQUIRED_FLAGS}
|
|
4118
|
+
|
|
4119
|
+
Optional:
|
|
4120
|
+
--task-types <csv> Whitelist. Known types: ${knownTaskTypesList()}
|
|
4121
|
+
--correlation-id <uuid> Restrict claims to one orchestration run.
|
|
4122
|
+
--wait-for-first-task-sec <n>
|
|
4123
|
+
Wait this long for an initially empty run before
|
|
4124
|
+
exiting. After the first claim, exit on empty.
|
|
4125
|
+
--wait-after-task-sec <n> Require the queue to remain empty for this long
|
|
4126
|
+
after a claim before exiting.
|
|
4127
|
+
--diary-ids <csv> Diary filter.
|
|
4128
|
+
--poll-interval-ms <n> Default: 2000.
|
|
4129
|
+
--max-poll-interval-ms <n> Default: 30000.
|
|
4130
|
+
--list-limit <n> Default: 10.
|
|
4131
|
+
${COMMON_OPTIONAL_FLAGS}
|
|
4132
|
+
|
|
4133
|
+
Example:
|
|
4134
|
+
agent-daemon drain \\
|
|
4135
|
+
--team 6743b4b1-6b93-46e2-a048-19490f04f91a \\
|
|
4136
|
+
--task-types judge_pack \\
|
|
4137
|
+
--agent legreffier \\
|
|
4138
|
+
--profile eval-judge`;
|
|
4139
|
+
var SYNC_SESSIONS_HELP = `\
|
|
4140
|
+
agent-daemon sync-sessions — repair durable runtime-session checkpoints.
|
|
4141
|
+
|
|
4142
|
+
Usage:
|
|
4143
|
+
agent-daemon sync-sessions --team <uuid> --agent <name> [...]
|
|
4144
|
+
|
|
4145
|
+
Scans this daemon's team-scoped runtime slots, compares local Pi session files
|
|
4146
|
+
with durable runtime-session metadata, and uploads missing or stale checkpoints.
|
|
4147
|
+
|
|
4148
|
+
Required:
|
|
4149
|
+
--team <uuid> Team whose runtime slots to inspect.
|
|
4150
|
+
-a, --agent <name> MoltNet agent identity. Reads credentials
|
|
4151
|
+
from <agent-root>/.moltnet/<name>/moltnet.json.
|
|
4152
|
+
|
|
4153
|
+
Optional:
|
|
4154
|
+
--runtime-profile-id <uuid> Limit repair to one runtime profile.
|
|
4155
|
+
--state <active|idle> Limit scanned slots by state. Default: all.
|
|
4156
|
+
--limit <n> Max slots to scan, 1..200. Default: 100.
|
|
4157
|
+
--dry-run Report missing/stale sessions without uploading.
|
|
4158
|
+
--agent-root <path> Explicit legacy identity bundle location.
|
|
4159
|
+
Omitted: use the central identity store.
|
|
4160
|
+
${PROJECT_RUN_FLAGS}
|
|
4161
|
+
--debug Accepted for consistency; no extra output yet.
|
|
4162
|
+
|
|
4163
|
+
Example:
|
|
4164
|
+
agent-daemon sync-sessions \\
|
|
4165
|
+
--team 6743b4b1-6b93-46e2-a048-19490f04f91a \\
|
|
4166
|
+
--agent legreffier \\
|
|
4167
|
+
--state idle`;
|
|
4168
|
+
function isHelpFlag(args) {
|
|
4169
|
+
return args.includes("--help") || args.includes("-h");
|
|
4170
|
+
}
|
|
4171
|
+
var AGENT_SERVER_HELP = `\
|
|
4172
|
+
agent-daemon server — loopback supervisor for console-managed runs.
|
|
4173
|
+
|
|
4174
|
+
Binds 127.0.0.1 only. An authorized Console origin configures agents and
|
|
4175
|
+
providers (secret references only) and starts/stops poll/drain runs as
|
|
4176
|
+
child processes of this supervisor.
|
|
4177
|
+
|
|
4178
|
+
Options:
|
|
4179
|
+
--port <n> Loopback port. Default: 17374.
|
|
4180
|
+
Env: MOLTNET_AGENT_SERVER_PORT.
|
|
4181
|
+
--allowed-origins <csv> Exact Console origins allowed local control.
|
|
4182
|
+
Default: https://console.themolt.net.
|
|
4183
|
+
Env: MOLTNET_AGENT_SERVER_ALLOWED_ORIGINS.
|
|
4184
|
+
--root <path> Config root. Default: ~/.config/moltnet
|
|
4185
|
+
(or MOLTNET_AGENT_SERVER_ROOT).
|
|
4186
|
+
--api-url <url> Default MoltNet API for new managed agents.
|
|
4187
|
+
Default: https://api.themolt.net.
|
|
4188
|
+
--heartbeat-interval-ms <n> Child reporter heartbeat cadence. Default: 60000.
|
|
4189
|
+
--warm-retention-sec <n> Child session/workspace retention. Default: 1800.
|
|
4190
|
+
--supervised Also stop gracefully when stdin reaches EOF.
|
|
4191
|
+
|
|
4192
|
+
On macOS, the first interactive run asks to trust a per-user local CA in the
|
|
4193
|
+
login keychain and serves HTTPS. Native supervisors use:
|
|
4194
|
+
server trust --status --json
|
|
4195
|
+
server trust --yes --json
|
|
4196
|
+
server trust --remove --yes --json
|
|
4197
|
+
Run \`agent-daemon server trust --remove\` interactively to remove that exact
|
|
4198
|
+
CA. Linux continues to use the Chromium PNA HTTP path.
|
|
4199
|
+
`;
|
|
4200
|
+
var PROVIDERS_HELP = `\
|
|
4201
|
+
moltnet-agent providers — manage local model providers.
|
|
4202
|
+
|
|
4203
|
+
Usage:
|
|
4204
|
+
moltnet-agent providers list [--json] [--root <path>]
|
|
4205
|
+
moltnet-agent providers set <id> [--base-url <url>] [--api <pi-api-kind>]
|
|
4206
|
+
[--model <id> ... | --clear-models]
|
|
4207
|
+
[--model-input <id>=text,image ...]
|
|
4208
|
+
[--api-key-stdin | --clear-api-key] [--root <path>]
|
|
4209
|
+
moltnet-agent providers discover <id> [--save] [--json] [--root <path>]
|
|
4210
|
+
moltnet-agent providers remove <id> [--yes] [--root <path>]
|
|
4211
|
+
moltnet-agent providers login <id> [--auth-method <method-id>]
|
|
4212
|
+
[--root <path>]
|
|
4213
|
+
moltnet-agent providers logout <id> [--yes] [--root <path>]
|
|
4214
|
+
|
|
4215
|
+
The default root is ~/.config/moltnet. MOLTNET_AGENT_SERVER_ROOT remains the
|
|
4216
|
+
environment override. API keys are accepted only from redirected stdin; they
|
|
4217
|
+
are stored separately and providers.json contains only a secret reference.
|
|
4218
|
+
|
|
4219
|
+
--model declares a text-only model. --model-input declares a model together
|
|
4220
|
+
with the input modalities it accepts, and is what makes a vision model usable:
|
|
4221
|
+
a model with no declared modalities is text-only to Pi, which drops image
|
|
4222
|
+
content parts before the request leaves the runtime.
|
|
4223
|
+
`;
|
|
4224
|
+
//#endregion
|
|
4025
4225
|
//#region src/lib/abort-active-attempt.ts
|
|
4026
4226
|
/** Best-effort signal cleanup; lease expiry remains the final backstop. */
|
|
4027
4227
|
async function abortActiveAttemptOnSignal(opts) {
|
|
@@ -4201,6 +4401,7 @@ function slugifySessionComponent(input) {
|
|
|
4201
4401
|
}
|
|
4202
4402
|
//#endregion
|
|
4203
4403
|
//#region src/lib/task-execution-plan.ts
|
|
4404
|
+
var WorkspaceModeMismatchError = class extends Error {};
|
|
4204
4405
|
function buildDaemonTaskExecutionPlan(task, stateDirs, identity, warmRetentionSec, runtimeProfileWorkspacePolicy = {}, attemptN) {
|
|
4205
4406
|
const descriptor = deriveTaskSessionDescriptor(task);
|
|
4206
4407
|
const workspaceMode = resolveTaskWorkspaceMode(task, descriptor.policy, runtimeProfileWorkspacePolicy);
|
|
@@ -4274,6 +4475,7 @@ function resolveTaskWorkspaceMode(task, policy, runtimeProfileWorkspacePolicy) {
|
|
|
4274
4475
|
const requestedWorkspace = policy.acceptsInputWorkspaceOverride && typeof task.input.execution?.workspace === "string" ? task.input.execution.workspace : null;
|
|
4275
4476
|
if (isRuntimeProfileWorkspaceMode(requestedWorkspace)) {
|
|
4276
4477
|
if (allowed.has(requestedWorkspace)) return toDaemonWorkspaceMode(requestedWorkspace);
|
|
4478
|
+
if (runtimeProfileWorkspacePolicy.workspaceExplicit) throw new WorkspaceModeMismatchError(`Requested workspace mode ${requestedWorkspace} is not allowed by profile ${runtimeProfileWorkspacePolicy.profileName ?? "(selected)"}; allowed: ${[...allowed].join(", ")}`);
|
|
4277
4479
|
}
|
|
4278
4480
|
if (profileDefault && allowed.has(profileDefault)) return toDaemonWorkspaceMode(profileDefault);
|
|
4279
4481
|
if (allowed.has(policy.workspaceMode)) return policy.workspaceMode;
|
|
@@ -4381,8 +4583,8 @@ function assertPlanAllowedByWorkspacePolicy(plan, policy, runtimeProfileId) {
|
|
|
4381
4583
|
"dedicated_worktree"
|
|
4382
4584
|
]);
|
|
4383
4585
|
const effectiveMode = planToRuntimeProfileWorkspaceMode(plan);
|
|
4384
|
-
if (plan.workspaceRevision && effectiveMode !== "dedicated_worktree") throw new
|
|
4385
|
-
if (!allowed.has(effectiveMode)) throw new
|
|
4586
|
+
if (plan.workspaceRevision && effectiveMode !== "dedicated_worktree") throw new WorkspaceModeMismatchError(`Runtime profile "${runtimeProfileId}" does not allow "dedicated_worktree", required by a revision-pinned task (resolved workspace mode "${effectiveMode}")`);
|
|
4587
|
+
if (!allowed.has(effectiveMode)) throw new WorkspaceModeMismatchError(`Runtime profile "${policy?.profileName ?? runtimeProfileId}" forbids final workspace mode "${effectiveMode}"; allowed: ${[...allowed].join(", ")}`);
|
|
4386
4588
|
}
|
|
4387
4589
|
function planToRuntimeProfileWorkspaceMode(plan) {
|
|
4388
4590
|
if (plan.workspaceMode === "scratch_mount") return "none";
|
|
@@ -4569,7 +4771,8 @@ function resolveProducerWorkspaceCopySource(producer, stateDirs) {
|
|
|
4569
4771
|
if (isDisposableScratchWorkspace(producer, stateDirs)) return null;
|
|
4570
4772
|
throw new ProducerContextResolutionError(`Producer workspace path is missing on disk: ${workspacePath}`);
|
|
4571
4773
|
}
|
|
4572
|
-
const sharedMountRoot =
|
|
4774
|
+
const sharedMountRoot = stateDirs.mountPath;
|
|
4775
|
+
if (!sharedMountRoot) throw new ProducerContextResolutionError("Shared producer mount root was not supplied by the runtime profile");
|
|
4573
4776
|
if (!existsSync(sharedMountRoot)) throw new ProducerContextResolutionError(`Shared producer mount root is missing on disk: ${sharedMountRoot}`);
|
|
4574
4777
|
return sharedMountRoot;
|
|
4575
4778
|
}
|
|
@@ -5557,7 +5760,6 @@ var AgentServerStore = class {
|
|
|
5557
5760
|
activations: {}
|
|
5558
5761
|
};
|
|
5559
5762
|
if (!isRecord$1(state) || state.version !== 2) throw new AgentServerStoreError("invalid_state", `agent-server.json version ${String(isRecord$1(state) ? state.version : void 0)} is not supported; move agent-server.json aside, run \`moltnet config migrate\`, then add or attach the agents again`);
|
|
5560
|
-
if ("pairedOrigins" in state) throw new AgentServerStoreError("invalid_state", "agent-server.json uses the obsolete pairing format; move agent-server.json aside and configure the agent server again");
|
|
5561
5763
|
if (!isRecord$1(state.pendingRegistrations) || !isRecord$1(state.activations)) throw new AgentServerStoreError("invalid_state", "agent-server.json is missing the version 2 activation map; move agent-server.json aside, run `moltnet config migrate`, then add or attach the agents again");
|
|
5562
5764
|
for (const [alias, activation] of Object.entries(state.activations)) validateActivation(alias, activation);
|
|
5563
5765
|
for (const [alias, registration] of Object.entries(state.pendingRegistrations)) {
|
|
@@ -5661,6 +5863,17 @@ var AgentServerStore = class {
|
|
|
5661
5863
|
if (activation.source === "managed") delete state.pendingRegistrations[alias];
|
|
5662
5864
|
this.writeAgentServerState(state);
|
|
5663
5865
|
}
|
|
5866
|
+
writeCredentialMetadata(alias, teamId, metadata) {
|
|
5867
|
+
const activation = this.readActivation(alias);
|
|
5868
|
+
if (!activation) throw new AgentServerStoreError("not_found", "Identity is not activated");
|
|
5869
|
+
this.writeActivation({
|
|
5870
|
+
...activation,
|
|
5871
|
+
credentialHealth: {
|
|
5872
|
+
...activation.credentialHealth,
|
|
5873
|
+
[teamId]: metadata
|
|
5874
|
+
}
|
|
5875
|
+
});
|
|
5876
|
+
}
|
|
5664
5877
|
listActivations() {
|
|
5665
5878
|
return Object.values(this.readAgentServerState().activations).sort((a, b) => a.alias.localeCompare(b.alias));
|
|
5666
5879
|
}
|
|
@@ -5726,6 +5939,24 @@ var AgentServerStore = class {
|
|
|
5726
5939
|
writeRun(record) {
|
|
5727
5940
|
writeJsonAtomic(join(this.runDir(record.id), "run.json"), record);
|
|
5728
5941
|
}
|
|
5942
|
+
/** Status polling reads history without blocking the supervisor event loop. */
|
|
5943
|
+
async listRunsAsync(limit, includeIds = []) {
|
|
5944
|
+
let ids;
|
|
5945
|
+
try {
|
|
5946
|
+
ids = await readdir(this.runsDir);
|
|
5947
|
+
} catch {
|
|
5948
|
+
return [];
|
|
5949
|
+
}
|
|
5950
|
+
const selected = ids.filter((id) => NAME_RE.test(id)).sort().reverse().slice(0, Math.max(0, limit));
|
|
5951
|
+
return (await Promise.all([...new Set([...includeIds, ...selected])].map(async (id) => {
|
|
5952
|
+
try {
|
|
5953
|
+
return JSON.parse(await readFile(join(this.runDir(id), "run.json"), "utf8"));
|
|
5954
|
+
} catch (error) {
|
|
5955
|
+
if (error.code === "ENOENT") return null;
|
|
5956
|
+
throw error;
|
|
5957
|
+
}
|
|
5958
|
+
}))).filter((record) => record !== null).sort((a, b) => b.startedAt.localeCompare(a.startedAt));
|
|
5959
|
+
}
|
|
5729
5960
|
listRuns(limit = Number.POSITIVE_INFINITY) {
|
|
5730
5961
|
let ids;
|
|
5731
5962
|
try {
|
|
@@ -5956,14 +6187,32 @@ function mergePiModels(store, repo) {
|
|
|
5956
6187
|
//#endregion
|
|
5957
6188
|
//#region src/lib/state-dir.ts
|
|
5958
6189
|
function ensureDaemonStateDirs(mountPath) {
|
|
5959
|
-
const rootDir = join(mountPath, ".moltnet", "d");
|
|
6190
|
+
const rootDir = canonicalStatePath(join(mountPath, ".moltnet", "d"));
|
|
5960
6191
|
const piSessionsDir = join(rootDir, "pi-sessions");
|
|
5961
|
-
mkdirSync(
|
|
6192
|
+
mkdirSync(rootDir, {
|
|
6193
|
+
recursive: true,
|
|
6194
|
+
mode: 448
|
|
6195
|
+
});
|
|
6196
|
+
mkdirSync(piSessionsDir, {
|
|
6197
|
+
recursive: true,
|
|
6198
|
+
mode: 448
|
|
6199
|
+
});
|
|
5962
6200
|
return {
|
|
5963
|
-
rootDir,
|
|
5964
|
-
piSessionsDir
|
|
6201
|
+
rootDir: realpathSync(rootDir),
|
|
6202
|
+
piSessionsDir: realpathSync(piSessionsDir)
|
|
5965
6203
|
};
|
|
5966
6204
|
}
|
|
6205
|
+
function canonicalStatePath(path) {
|
|
6206
|
+
let parent = resolve(path);
|
|
6207
|
+
const missing = [];
|
|
6208
|
+
while (!existsSync(parent)) {
|
|
6209
|
+
missing.unshift(basename(parent));
|
|
6210
|
+
const next = dirname(parent);
|
|
6211
|
+
if (next === parent) throw new Error(`Cannot resolve daemon state root ${path}`);
|
|
6212
|
+
parent = next;
|
|
6213
|
+
}
|
|
6214
|
+
return join(realpathSync(parent), ...missing);
|
|
6215
|
+
}
|
|
5967
6216
|
//#endregion
|
|
5968
6217
|
//#region src/lib/prepare-runtime-profile.ts
|
|
5969
6218
|
/** Validate and prepare a profile through the shared daemon execution path. */
|
|
@@ -5985,7 +6234,8 @@ async function prepareRuntimeProfile(input) {
|
|
|
5985
6234
|
rootDir: profile.mountPath,
|
|
5986
6235
|
path: profile.source
|
|
5987
6236
|
};
|
|
5988
|
-
const stateDirs = ensureDaemonStateDirs(sandbox.rootDir);
|
|
6237
|
+
const stateDirs = ensureDaemonStateDirs(input.stateRootDir ?? sandbox.rootDir);
|
|
6238
|
+
stateDirs.mountPath = profile.mountPath;
|
|
5989
6239
|
const slotIdentity = {
|
|
5990
6240
|
agentName: input.agentName,
|
|
5991
6241
|
runtimeProfileId: profile.id,
|
|
@@ -6002,6 +6252,8 @@ async function prepareRuntimeProfile(input) {
|
|
|
6002
6252
|
slotIdentity,
|
|
6003
6253
|
warmRetentionSec: input.warmRetentionSec,
|
|
6004
6254
|
workspacePolicy: {
|
|
6255
|
+
workspaceExplicit: input.workspaceExplicit,
|
|
6256
|
+
profileName: profile.name,
|
|
6005
6257
|
defaultWorkspaceMode: profile.defaultWorkspaceMode,
|
|
6006
6258
|
allowedWorkspaceModes: profile.allowedWorkspaceModes
|
|
6007
6259
|
},
|
|
@@ -6012,6 +6264,32 @@ async function prepareRuntimeProfile(input) {
|
|
|
6012
6264
|
};
|
|
6013
6265
|
}
|
|
6014
6266
|
//#endregion
|
|
6267
|
+
//#region src/lib/project-task-source.ts
|
|
6268
|
+
function createProjectOnceSource(selection, options) {
|
|
6269
|
+
return new ApiTaskSource({
|
|
6270
|
+
...options,
|
|
6271
|
+
projectId: selection.projectId
|
|
6272
|
+
});
|
|
6273
|
+
}
|
|
6274
|
+
function createProjectPollingSource(selection, options) {
|
|
6275
|
+
return new PollingApiTaskSource({
|
|
6276
|
+
...options,
|
|
6277
|
+
projectId: selection.projectId,
|
|
6278
|
+
isTaskEligible: (task) => {
|
|
6279
|
+
if (!selection.workspaceExplicit) return true;
|
|
6280
|
+
if (resolveTaskWorkspaceRevision(task.input) && selection.strategy !== "git-worktree") return false;
|
|
6281
|
+
if (!getTaskExecutionPolicy(task.taskType).acceptsInputWorkspaceOverride) return true;
|
|
6282
|
+
const requested = task.input?.execution?.workspace;
|
|
6283
|
+
if (!requested || ![
|
|
6284
|
+
"none",
|
|
6285
|
+
"shared_mount",
|
|
6286
|
+
"dedicated_worktree"
|
|
6287
|
+
].includes(requested)) return true;
|
|
6288
|
+
return requested === (selection.strategy === "existing" ? "shared_mount" : selection.strategy === "git-worktree" ? "dedicated_worktree" : "none");
|
|
6289
|
+
}
|
|
6290
|
+
});
|
|
6291
|
+
}
|
|
6292
|
+
//#endregion
|
|
6015
6293
|
//#region src/lib/runtime-context.ts
|
|
6016
6294
|
var storage = new AsyncLocalStorage();
|
|
6017
6295
|
function runWithDaemonRuntimeContext(context, callback) {
|
|
@@ -6591,6 +6869,7 @@ async function runPolling(opts) {
|
|
|
6591
6869
|
args: opts.argv,
|
|
6592
6870
|
options: {
|
|
6593
6871
|
...runtimeCommandOptionDefs(),
|
|
6872
|
+
...projectRunOptionDefs(),
|
|
6594
6873
|
team: { type: "string" },
|
|
6595
6874
|
"task-types": { type: "string" },
|
|
6596
6875
|
"correlation-id": { type: "string" },
|
|
@@ -6607,12 +6886,12 @@ async function runPolling(opts) {
|
|
|
6607
6886
|
}
|
|
6608
6887
|
}
|
|
6609
6888
|
});
|
|
6610
|
-
if (!values.team) {
|
|
6889
|
+
if (!values.team && !values.binding && !values.project && !values["config-file"]) {
|
|
6611
6890
|
console.error("Missing required flag: --team\n");
|
|
6612
6891
|
console.error(opts.helpText);
|
|
6613
6892
|
return 1;
|
|
6614
6893
|
}
|
|
6615
|
-
|
|
6894
|
+
let teamId = values.team ?? "";
|
|
6616
6895
|
const profileValues = parseProfileValues(values.profile);
|
|
6617
6896
|
if (profileValues.length === 0) {
|
|
6618
6897
|
console.error("Missing required flag: --profile\n");
|
|
@@ -6655,6 +6934,41 @@ async function runPolling(opts) {
|
|
|
6655
6934
|
}
|
|
6656
6935
|
if (taskTypes.length === 0) console.error(`[${opts.modeLabel}] --task-types is empty — daemon will accept any registered type. Pass an explicit list to limit scope (e.g. --task-types fulfill_brief).`);
|
|
6657
6936
|
const cfg = loadConfig();
|
|
6937
|
+
let selection;
|
|
6938
|
+
try {
|
|
6939
|
+
const endpoint = await resolveSelectionApiUrl(identity.agent, {
|
|
6940
|
+
agentRootDir: values["agent-root"] ? resolve(process.cwd(), values["agent-root"]) : void 0,
|
|
6941
|
+
credentialSource: cfg.credentialSource,
|
|
6942
|
+
envApiUrl: cfg.apiUrl
|
|
6943
|
+
});
|
|
6944
|
+
selection = await resolveRunProjectSelection({
|
|
6945
|
+
agent: identity.agent,
|
|
6946
|
+
cwd: process.cwd(),
|
|
6947
|
+
apiUrl: endpoint,
|
|
6948
|
+
binding: values.binding,
|
|
6949
|
+
project: values.project,
|
|
6950
|
+
team: values.team,
|
|
6951
|
+
general: values.general,
|
|
6952
|
+
"config-file": values["config-file"],
|
|
6953
|
+
"state-dir": values["state-dir"],
|
|
6954
|
+
source: values.source,
|
|
6955
|
+
"workspace-strategy": values["workspace-strategy"]
|
|
6956
|
+
});
|
|
6957
|
+
teamId = selection.teamId ?? "";
|
|
6958
|
+
if (!teamId) throw new Error("Select --team or a binding with a team");
|
|
6959
|
+
} catch (error) {
|
|
6960
|
+
await logDaemonStartupFailure({
|
|
6961
|
+
serviceName: "agent-daemon.selection",
|
|
6962
|
+
level: cfg.logLevel || "info",
|
|
6963
|
+
gate: "project_selection",
|
|
6964
|
+
agent: identity.agent,
|
|
6965
|
+
credentialSource: cfg.credentialSource,
|
|
6966
|
+
error
|
|
6967
|
+
});
|
|
6968
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
6969
|
+
console.error(opts.helpText);
|
|
6970
|
+
return 1;
|
|
6971
|
+
}
|
|
6658
6972
|
const credentialSources = {
|
|
6659
6973
|
profileRequirements: cfg.profileCredentialRequirements,
|
|
6660
6974
|
bindings: cfg.credentialBindings
|
|
@@ -6668,6 +6982,7 @@ async function runPolling(opts) {
|
|
|
6668
6982
|
agentRootDir: explicitAgentRootDir,
|
|
6669
6983
|
credentialSource: cfg.credentialSource,
|
|
6670
6984
|
envApiUrl: cfg.apiUrl,
|
|
6985
|
+
projectApiUrl: selection.binding?.apiUrl,
|
|
6671
6986
|
teamId
|
|
6672
6987
|
});
|
|
6673
6988
|
gate = "authenticate_and_bind";
|
|
@@ -6722,18 +7037,25 @@ async function runPolling(opts) {
|
|
|
6722
7037
|
throw error;
|
|
6723
7038
|
}
|
|
6724
7039
|
})();
|
|
6725
|
-
const daemonRootDir = explicitAgentRootDir ?? process.cwd();
|
|
6726
|
-
const resolvedProfiles = await resolveRuntimeProfiles({
|
|
7040
|
+
const daemonRootDir = selection.binding || values.source ? selection.source ?? process.cwd() : explicitAgentRootDir ?? process.cwd();
|
|
7041
|
+
const resolvedProfiles = (await resolveRuntimeProfiles({
|
|
6727
7042
|
agent: ctx.agent,
|
|
6728
7043
|
profiles: profileValues,
|
|
6729
7044
|
teamId,
|
|
6730
7045
|
cwd: daemonRootDir
|
|
6731
|
-
});
|
|
7046
|
+
})).map((profile) => applyProjectWorkspacePolicy(profile, selection));
|
|
6732
7047
|
const { logger, shutdown: shutdownLogger } = createRootLogger({
|
|
6733
7048
|
name: `agent-daemon.${opts.modeLabel}`,
|
|
6734
7049
|
level: cfg.logLevel || (identity.debug ? "debug" : "info")
|
|
6735
7050
|
});
|
|
6736
7051
|
const rootLogger = logger.child({
|
|
7052
|
+
projectId: selection.projectId,
|
|
7053
|
+
binding: selection.binding?.name,
|
|
7054
|
+
selectedBy: selection.selectedBy,
|
|
7055
|
+
source: daemonRootDir,
|
|
7056
|
+
strategy: selection.strategy,
|
|
7057
|
+
stateRootDir: selection.stateRootDir ?? daemonRootDir,
|
|
7058
|
+
apiUrl: selection.apiUrl,
|
|
6737
7059
|
mode: opts.modeLabel,
|
|
6738
7060
|
agent: identity.agent,
|
|
6739
7061
|
teamId,
|
|
@@ -6756,6 +7078,8 @@ async function runPolling(opts) {
|
|
|
6756
7078
|
agent: ctx.agent,
|
|
6757
7079
|
agentName: identity.agent,
|
|
6758
7080
|
profile,
|
|
7081
|
+
stateRootDir: selection.stateRootDir,
|
|
7082
|
+
workspaceExplicit: selection.workspaceExplicit,
|
|
6759
7083
|
prerequisiteEnv: cfg.profilePrerequisiteEnv,
|
|
6760
7084
|
runtimeAdapter,
|
|
6761
7085
|
runtimeInstanceId,
|
|
@@ -6787,6 +7111,9 @@ async function runPolling(opts) {
|
|
|
6787
7111
|
agent: ctx.agent,
|
|
6788
7112
|
endpoint: cfg.otelEndpoint,
|
|
6789
7113
|
resourceAttributes: {
|
|
7114
|
+
"moltnet.project.id": selection.projectId ?? "general",
|
|
7115
|
+
"moltnet.project.selection": selection.selectedBy,
|
|
7116
|
+
"moltnet.workspace.strategy": selection.strategy,
|
|
6790
7117
|
"moltnet.team.id": teamId,
|
|
6791
7118
|
"moltnet.agent.name": identity.agent,
|
|
6792
7119
|
"moltnet.credential.source": ctx.credentialSource,
|
|
@@ -6905,7 +7232,7 @@ async function runPolling(opts) {
|
|
|
6905
7232
|
try {
|
|
6906
7233
|
runtime = new AgentRuntime({
|
|
6907
7234
|
logger: rootLogger,
|
|
6908
|
-
source:
|
|
7235
|
+
source: createProjectPollingSource(selection, {
|
|
6909
7236
|
agent: ctx.agent,
|
|
6910
7237
|
teamId,
|
|
6911
7238
|
taskTypes: taskTypes.length > 0 ? taskTypes : void 0,
|
|
@@ -7027,7 +7354,7 @@ async function runPolling(opts) {
|
|
|
7027
7354
|
error: {
|
|
7028
7355
|
code: err instanceof ProducerContextResolutionError ? "producer_context_missing" : "execution_plan_failed",
|
|
7029
7356
|
message,
|
|
7030
|
-
retryable:
|
|
7357
|
+
retryable: err instanceof WorkspaceModeMismatchError
|
|
7031
7358
|
}
|
|
7032
7359
|
};
|
|
7033
7360
|
}
|
|
@@ -7227,6 +7554,7 @@ async function runOnce(argv, runtimeAdapter = defaultPiDaemonAdapter) {
|
|
|
7227
7554
|
args: argv,
|
|
7228
7555
|
options: {
|
|
7229
7556
|
...runtimeCommandOptionDefs(),
|
|
7557
|
+
...projectRunOptionDefs(),
|
|
7230
7558
|
"task-id": {
|
|
7231
7559
|
type: "string",
|
|
7232
7560
|
short: "t"
|
|
@@ -7264,6 +7592,39 @@ async function runOnce(argv, runtimeAdapter = defaultPiDaemonAdapter) {
|
|
|
7264
7592
|
return 1;
|
|
7265
7593
|
}
|
|
7266
7594
|
const cfg = loadConfig();
|
|
7595
|
+
let selection;
|
|
7596
|
+
try {
|
|
7597
|
+
const endpoint = await resolveSelectionApiUrl(identity.agent, {
|
|
7598
|
+
agentRootDir: values["agent-root"] ? resolve(process.cwd(), values["agent-root"]) : void 0,
|
|
7599
|
+
credentialSource: cfg.credentialSource,
|
|
7600
|
+
envApiUrl: cfg.apiUrl
|
|
7601
|
+
});
|
|
7602
|
+
selection = await resolveRunProjectSelection({
|
|
7603
|
+
agent: identity.agent,
|
|
7604
|
+
cwd: process.cwd(),
|
|
7605
|
+
apiUrl: endpoint,
|
|
7606
|
+
binding: values.binding,
|
|
7607
|
+
project: values.project,
|
|
7608
|
+
team: values.team,
|
|
7609
|
+
general: values.general,
|
|
7610
|
+
"config-file": values["config-file"],
|
|
7611
|
+
"state-dir": values["state-dir"],
|
|
7612
|
+
source: values.source,
|
|
7613
|
+
"workspace-strategy": values["workspace-strategy"]
|
|
7614
|
+
});
|
|
7615
|
+
} catch (error) {
|
|
7616
|
+
await logDaemonStartupFailure({
|
|
7617
|
+
serviceName: "agent-daemon.selection",
|
|
7618
|
+
level: cfg.logLevel || "info",
|
|
7619
|
+
gate: "project_selection",
|
|
7620
|
+
agent: identity.agent,
|
|
7621
|
+
credentialSource: cfg.credentialSource,
|
|
7622
|
+
error
|
|
7623
|
+
});
|
|
7624
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
7625
|
+
console.error(ONCE_HELP);
|
|
7626
|
+
return 1;
|
|
7627
|
+
}
|
|
7267
7628
|
const credentialSources = {
|
|
7268
7629
|
profileRequirements: cfg.profileCredentialRequirements,
|
|
7269
7630
|
bindings: cfg.credentialBindings
|
|
@@ -7277,13 +7638,14 @@ async function runOnce(argv, runtimeAdapter = defaultPiDaemonAdapter) {
|
|
|
7277
7638
|
agentRootDir: explicitAgentRootDir,
|
|
7278
7639
|
credentialSource: cfg.credentialSource,
|
|
7279
7640
|
envApiUrl: cfg.apiUrl,
|
|
7280
|
-
|
|
7641
|
+
projectApiUrl: selection.binding?.apiUrl,
|
|
7642
|
+
teamId: selection.teamId
|
|
7281
7643
|
});
|
|
7282
7644
|
gate = "authenticate_and_bind";
|
|
7283
7645
|
const whoami = await validateStartupBinding({
|
|
7284
7646
|
agent: resolvedContext.agent,
|
|
7285
7647
|
credentialTeamId: resolvedContext.credentialTeamId,
|
|
7286
|
-
teamId:
|
|
7648
|
+
teamId: selection.teamId,
|
|
7287
7649
|
expectedAgent: cfg.expectedAgent
|
|
7288
7650
|
});
|
|
7289
7651
|
gate = "resolve_signing_material";
|
|
@@ -7330,18 +7692,25 @@ async function runOnce(argv, runtimeAdapter = defaultPiDaemonAdapter) {
|
|
|
7330
7692
|
throw error;
|
|
7331
7693
|
}
|
|
7332
7694
|
})();
|
|
7333
|
-
const daemonRootDir = explicitAgentRootDir ?? process.cwd();
|
|
7334
|
-
const profile = await resolveRuntimeProfile({
|
|
7695
|
+
const daemonRootDir = selection.binding || values.source ? selection.source ?? process.cwd() : explicitAgentRootDir ?? process.cwd();
|
|
7696
|
+
const profile = applyProjectWorkspacePolicy(await resolveRuntimeProfile({
|
|
7335
7697
|
agent: ctx.agent,
|
|
7336
7698
|
profile: values.profile,
|
|
7337
|
-
teamId:
|
|
7699
|
+
teamId: selection.teamId,
|
|
7338
7700
|
cwd: daemonRootDir
|
|
7339
|
-
});
|
|
7701
|
+
}), selection);
|
|
7340
7702
|
const { logger, shutdown: shutdownLogger } = createRootLogger({
|
|
7341
7703
|
name: "agent-daemon.once",
|
|
7342
7704
|
level: cfg.logLevel || (identity.debug ? "debug" : "info")
|
|
7343
7705
|
});
|
|
7344
7706
|
const rootLogger = logger.child({
|
|
7707
|
+
projectId: selection.projectId,
|
|
7708
|
+
binding: selection.binding?.name,
|
|
7709
|
+
selectedBy: selection.selectedBy,
|
|
7710
|
+
source: daemonRootDir,
|
|
7711
|
+
strategy: selection.strategy,
|
|
7712
|
+
stateRootDir: selection.stateRootDir ?? daemonRootDir,
|
|
7713
|
+
apiUrl: selection.apiUrl,
|
|
7345
7714
|
mode: "once",
|
|
7346
7715
|
agent: identity.agent,
|
|
7347
7716
|
provider: profile.provider,
|
|
@@ -7365,6 +7734,8 @@ async function runOnce(argv, runtimeAdapter = defaultPiDaemonAdapter) {
|
|
|
7365
7734
|
agent: ctx.agent,
|
|
7366
7735
|
agentName: identity.agent,
|
|
7367
7736
|
profile,
|
|
7737
|
+
stateRootDir: selection.stateRootDir,
|
|
7738
|
+
workspaceExplicit: selection.workspaceExplicit,
|
|
7368
7739
|
prerequisiteEnv: cfg.profilePrerequisiteEnv,
|
|
7369
7740
|
runtimeAdapter,
|
|
7370
7741
|
runtimeInstanceId,
|
|
@@ -7382,6 +7753,9 @@ async function runOnce(argv, runtimeAdapter = defaultPiDaemonAdapter) {
|
|
|
7382
7753
|
agent: ctx.agent,
|
|
7383
7754
|
endpoint: cfg.otelEndpoint,
|
|
7384
7755
|
resourceAttributes: {
|
|
7756
|
+
"moltnet.project.id": selection.projectId ?? "general",
|
|
7757
|
+
"moltnet.project.selection": selection.selectedBy,
|
|
7758
|
+
"moltnet.workspace.strategy": selection.strategy,
|
|
7385
7759
|
"moltnet.task.id": taskId,
|
|
7386
7760
|
"moltnet.agent.name": identity.agent,
|
|
7387
7761
|
"moltnet.credential.source": ctx.credentialSource,
|
|
@@ -7576,7 +7950,7 @@ async function runOnce(argv, runtimeAdapter = defaultPiDaemonAdapter) {
|
|
|
7576
7950
|
});
|
|
7577
7951
|
runtime = new AgentRuntime({
|
|
7578
7952
|
logger: rootLogger,
|
|
7579
|
-
source:
|
|
7953
|
+
source: createProjectOnceSource(selection, {
|
|
7580
7954
|
agent: ctx.agent,
|
|
7581
7955
|
taskId,
|
|
7582
7956
|
teamId: profile.teamId,
|
|
@@ -7669,6 +8043,18 @@ function runPoll(argv, runtimeAdapter) {
|
|
|
7669
8043
|
//#region src/lib/provider-lock.ts
|
|
7670
8044
|
var DEFAULT_LOCK_TIMEOUT_MS = 3e4;
|
|
7671
8045
|
var LOCK_WAIT_WARNING_MS = 1e3;
|
|
8046
|
+
/**
|
|
8047
|
+
* When to warn that we are waiting on another process.
|
|
8048
|
+
*
|
|
8049
|
+
* A fixed 1s threshold is silently useless to a caller whose whole budget is
|
|
8050
|
+
* shorter than that: the timeout wins the race and the operation fails with no
|
|
8051
|
+
* word of *why* it failed, which is the one thing the caller needs. So the
|
|
8052
|
+
* threshold scales down with the budget, and never sits so close to the
|
|
8053
|
+
* deadline that whether it fires depends on timer drift.
|
|
8054
|
+
*/
|
|
8055
|
+
function warningThresholdMs(timeoutMs) {
|
|
8056
|
+
return Math.min(LOCK_WAIT_WARNING_MS, Math.floor(timeoutMs / 2));
|
|
8057
|
+
}
|
|
7672
8058
|
var ProviderLockError = class extends Error {
|
|
7673
8059
|
name = "ProviderLockError";
|
|
7674
8060
|
constructor(code, message, options) {
|
|
@@ -7697,6 +8083,7 @@ async function withNamedProviderLock(root, name, work, options) {
|
|
|
7697
8083
|
let compromised;
|
|
7698
8084
|
const startedAt = Date.now();
|
|
7699
8085
|
const timeoutMs = options.timeoutMs ?? DEFAULT_LOCK_TIMEOUT_MS;
|
|
8086
|
+
const warnAfterMs = warningThresholdMs(timeoutMs);
|
|
7700
8087
|
const lockfilePath = join(locksDir, `${name}.lock`);
|
|
7701
8088
|
let warned = false;
|
|
7702
8089
|
let release;
|
|
@@ -7704,7 +8091,7 @@ async function withNamedProviderLock(root, name, work, options) {
|
|
|
7704
8091
|
if (options.signal?.aborted) throw new ProviderLockError("lock_aborted", `provider lock acquisition was cancelled for "${name}"`, { cause: options.signal.reason });
|
|
7705
8092
|
const elapsedMs = Date.now() - startedAt;
|
|
7706
8093
|
if (elapsedMs >= timeoutMs) throw new ProviderLockError("lock_timeout", `timed out waiting for provider lock "${name}"`);
|
|
7707
|
-
if (!warned && elapsedMs >=
|
|
8094
|
+
if (!warned && elapsedMs >= warnAfterMs) {
|
|
7708
8095
|
warned = true;
|
|
7709
8096
|
options.logger?.warn({
|
|
7710
8097
|
code: "provider_lock_contended",
|
|
@@ -8887,29 +9274,97 @@ function registerLoopbackSecurity(app, options) {
|
|
|
8887
9274
|
});
|
|
8888
9275
|
}
|
|
8889
9276
|
//#endregion
|
|
8890
|
-
//#region
|
|
8891
|
-
|
|
8892
|
-
|
|
8893
|
-
|
|
8894
|
-
|
|
8895
|
-
|
|
8896
|
-
|
|
8897
|
-
|
|
8898
|
-
|
|
8899
|
-
function
|
|
8900
|
-
|
|
8901
|
-
const
|
|
8902
|
-
const
|
|
8903
|
-
|
|
9277
|
+
//#region src/lib/agent-server/connection-settings.ts
|
|
9278
|
+
var RELEASE_CONNECTION = {
|
|
9279
|
+
apiUrl: "https://api.themolt.net",
|
|
9280
|
+
issuer: "https://auth.themolt.net",
|
|
9281
|
+
publicUrl: "https://auth.themolt.net",
|
|
9282
|
+
nativeClientId: "moltnet-native",
|
|
9283
|
+
consoleClientId: "moltnet-console"
|
|
9284
|
+
};
|
|
9285
|
+
var KEYS = Object.keys(RELEASE_CONNECTION);
|
|
9286
|
+
function validateConnectionOverrides(value) {
|
|
9287
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("Connection settings must be an object");
|
|
9288
|
+
const result = {};
|
|
9289
|
+
for (const [key, raw] of Object.entries(value)) {
|
|
9290
|
+
if (!KEYS.includes(key) || typeof raw !== "string" || !raw.trim()) throw new Error(`Invalid connection setting: ${key}`);
|
|
9291
|
+
const text = raw.trim();
|
|
9292
|
+
if (key.endsWith("ClientId")) {
|
|
9293
|
+
if (text.length > 255 || /\s/u.test(text)) throw new Error("Client IDs must not contain whitespace");
|
|
9294
|
+
} else {
|
|
9295
|
+
const url = new URL(text);
|
|
9296
|
+
if (url.username || url.password || url.search || url.hash || url.protocol !== "https:" && !(url.protocol === "http:" && (key === "issuer" || isLoopbackHostname(url.hostname)))) throw new Error("Connection URLs require HTTPS (HTTP is allowed only on loopback)");
|
|
9297
|
+
}
|
|
9298
|
+
result[key] = text;
|
|
9299
|
+
}
|
|
9300
|
+
return result;
|
|
8904
9301
|
}
|
|
8905
|
-
/**
|
|
8906
|
-
|
|
8907
|
-
|
|
8908
|
-
|
|
8909
|
-
|
|
8910
|
-
|
|
8911
|
-
|
|
8912
|
-
|
|
9302
|
+
/** Local administration only. Never exposed through browser authorization. */
|
|
9303
|
+
var ConnectionSettingsStore = class {
|
|
9304
|
+
path;
|
|
9305
|
+
environment;
|
|
9306
|
+
constructor(root, environment = {}) {
|
|
9307
|
+
this.root = root;
|
|
9308
|
+
this.path = join(root, "connection-settings.json");
|
|
9309
|
+
this.environment = validateConnectionOverrides(environment);
|
|
9310
|
+
}
|
|
9311
|
+
view() {
|
|
9312
|
+
let overrides = {};
|
|
9313
|
+
try {
|
|
9314
|
+
overrides = validateConnectionOverrides(JSON.parse(readFileSync(this.path, "utf8")));
|
|
9315
|
+
} catch (error) {
|
|
9316
|
+
if (error.code !== "ENOENT") throw error;
|
|
9317
|
+
}
|
|
9318
|
+
return {
|
|
9319
|
+
defaults: RELEASE_CONNECTION,
|
|
9320
|
+
overrides,
|
|
9321
|
+
environment: this.environment,
|
|
9322
|
+
effective: {
|
|
9323
|
+
...RELEASE_CONNECTION,
|
|
9324
|
+
...overrides,
|
|
9325
|
+
...this.environment
|
|
9326
|
+
}
|
|
9327
|
+
};
|
|
9328
|
+
}
|
|
9329
|
+
stateRoot(settings = this.view().effective) {
|
|
9330
|
+
if (this.environment.apiUrl && this.environment.issuer) return this.root;
|
|
9331
|
+
return connectionStateRoot(this.root, settings);
|
|
9332
|
+
}
|
|
9333
|
+
save(value) {
|
|
9334
|
+
const overrides = validateConnectionOverrides(value);
|
|
9335
|
+
const current = this.view();
|
|
9336
|
+
for (const key of KEYS) {
|
|
9337
|
+
if (this.environment[key] !== void 0 && overrides[key] !== current.overrides[key]) throw new Error(`${key} is managed by the launch environment`);
|
|
9338
|
+
if (overrides[key] === RELEASE_CONNECTION[key]) delete overrides[key];
|
|
9339
|
+
}
|
|
9340
|
+
mkdirSync(this.root, {
|
|
9341
|
+
recursive: true,
|
|
9342
|
+
mode: 448
|
|
9343
|
+
});
|
|
9344
|
+
const effective = {
|
|
9345
|
+
...RELEASE_CONNECTION,
|
|
9346
|
+
...overrides,
|
|
9347
|
+
...this.environment
|
|
9348
|
+
};
|
|
9349
|
+
rmSync(join(this.stateRoot(effective), "operator.json"), { force: true });
|
|
9350
|
+
const temporary = `${this.path}.${randomUUID()}.tmp`;
|
|
9351
|
+
writeFileSync(temporary, JSON.stringify(overrides, null, 2) + "\n", {
|
|
9352
|
+
mode: 384,
|
|
9353
|
+
flag: "wx"
|
|
9354
|
+
});
|
|
9355
|
+
try {
|
|
9356
|
+
renameSync(temporary, this.path);
|
|
9357
|
+
} finally {
|
|
9358
|
+
rmSync(temporary, { force: true });
|
|
9359
|
+
}
|
|
9360
|
+
return this.view();
|
|
9361
|
+
}
|
|
9362
|
+
};
|
|
9363
|
+
/** A custom service gets its own identities, keys and runtime configuration. */
|
|
9364
|
+
function connectionStateRoot(root, settings) {
|
|
9365
|
+
const identity = [settings.apiUrl.replace(/\/$/u, ""), settings.issuer.replace(/\/$/u, "")];
|
|
9366
|
+
if (identity[0] === RELEASE_CONNECTION.apiUrl && identity[1] === RELEASE_CONNECTION.issuer) return root;
|
|
9367
|
+
return join(root, "environments", createHash("sha256").update(JSON.stringify(identity)).digest("hex"));
|
|
8913
9368
|
}
|
|
8914
9369
|
//#endregion
|
|
8915
9370
|
//#region src/lib/agent-server/lock.ts
|
|
@@ -8968,162 +9423,249 @@ async function withAgentServerLock(root, work, options) {
|
|
|
8968
9423
|
}
|
|
8969
9424
|
}
|
|
8970
9425
|
//#endregion
|
|
8971
|
-
//#region src/lib/agent-server/
|
|
9426
|
+
//#region src/lib/agent-server/native-grant.ts
|
|
8972
9427
|
/**
|
|
8973
|
-
*
|
|
8974
|
-
*
|
|
9428
|
+
* Environment variable the supervising desktop app uses to hand this server
|
|
9429
|
+
* process its control token.
|
|
8975
9430
|
*
|
|
8976
|
-
*
|
|
8977
|
-
*
|
|
8978
|
-
*
|
|
8979
|
-
|
|
8980
|
-
|
|
8981
|
-
|
|
8982
|
-
|
|
8983
|
-
|
|
8984
|
-
*
|
|
9431
|
+
* The parent generates the token, passes it here, and keeps it in native
|
|
9432
|
+
* memory — it is never written to disk, never printed to stdout (the desktop
|
|
9433
|
+
* app surfaces server output in its WebView), and never reaches the renderer.
|
|
9434
|
+
*/
|
|
9435
|
+
var NATIVE_TOKEN_ENV = "MOLTNET_AGENT_SERVER_NATIVE_TOKEN";
|
|
9436
|
+
/** 32 bytes of entropy, base64url-encoded, is 43 characters. */
|
|
9437
|
+
var MIN_TOKEN_LENGTH = 32;
|
|
9438
|
+
/**
|
|
9439
|
+
* Consume the supervisor's native token from the environment and grant it.
|
|
8985
9440
|
*
|
|
8986
|
-
*
|
|
8987
|
-
*
|
|
8988
|
-
*
|
|
8989
|
-
*
|
|
8990
|
-
*
|
|
8991
|
-
*
|
|
9441
|
+
* Consuming matters as much as granting: spawned run children inherit this
|
|
9442
|
+
* process environment, so a token left in place would hand every
|
|
9443
|
+
* task-executing agent control of the Agent Server supervising it. The
|
|
9444
|
+
* variable is deleted whether or not the value turns out to be usable.
|
|
9445
|
+
*
|
|
9446
|
+
* @returns whether a native grant was issued.
|
|
8992
9447
|
*/
|
|
8993
|
-
|
|
8994
|
-
|
|
8995
|
-
|
|
8996
|
-
|
|
8997
|
-
|
|
8998
|
-
|
|
8999
|
-
|
|
9448
|
+
function applyNativeClientGrant(options) {
|
|
9449
|
+
const { nativeGrant, env } = options;
|
|
9450
|
+
const token = env[NATIVE_TOKEN_ENV];
|
|
9451
|
+
delete env[NATIVE_TOKEN_ENV];
|
|
9452
|
+
if (typeof token !== "string" || token.length === 0) return false;
|
|
9453
|
+
if (token.length < MIN_TOKEN_LENGTH) throw new Error(`${NATIVE_TOKEN_ENV} must be at least 32 characters of unguessable entropy`);
|
|
9454
|
+
nativeGrant.grantNative(token);
|
|
9455
|
+
return true;
|
|
9456
|
+
}
|
|
9457
|
+
var NativeGrantError = class extends Error {
|
|
9458
|
+
code = "native_token_invalid";
|
|
9000
9459
|
};
|
|
9001
|
-
|
|
9002
|
-
|
|
9003
|
-
|
|
9004
|
-
|
|
9005
|
-
|
|
9006
|
-
const right = Buffer.from(b, "utf8");
|
|
9007
|
-
return left.length === right.length && timingSafeEqual(left, right);
|
|
9008
|
-
}
|
|
9009
|
-
var PairingService = class {
|
|
9010
|
-
pending = /* @__PURE__ */ new Map();
|
|
9011
|
-
paired = /* @__PURE__ */ new Map();
|
|
9012
|
-
constructor(options = {}) {
|
|
9013
|
-
this.options = options;
|
|
9460
|
+
var NativeGrantService = class {
|
|
9461
|
+
digest;
|
|
9462
|
+
grantNative(token) {
|
|
9463
|
+
if (!token) throw new NativeGrantError("Native token must not be empty");
|
|
9464
|
+
this.digest = createHash("sha256").update(token).digest();
|
|
9014
9465
|
}
|
|
9015
|
-
|
|
9016
|
-
|
|
9466
|
+
verify(origin, token) {
|
|
9467
|
+
const digest = createHash("sha256").update(token).digest();
|
|
9468
|
+
if (origin !== "moltnet-agent-desktop://native" || !this.digest || !timingSafeEqual(this.digest, digest)) throw new NativeGrantError("Native token is not valid");
|
|
9017
9469
|
}
|
|
9018
|
-
|
|
9019
|
-
|
|
9470
|
+
};
|
|
9471
|
+
//#endregion
|
|
9472
|
+
//#region src/lib/agent-server/operator-oauth.ts
|
|
9473
|
+
var InvalidOperatorGrantError = class extends Error {};
|
|
9474
|
+
var LOCAL_SCOPE = OPERATOR_OAUTH.localControlScope;
|
|
9475
|
+
/** Trusted native controller owns the verifier, callback and token exchange. */
|
|
9476
|
+
var OperatorOAuth = class {
|
|
9477
|
+
instance = randomUUID();
|
|
9478
|
+
keys;
|
|
9479
|
+
active = false;
|
|
9480
|
+
pending;
|
|
9481
|
+
operator;
|
|
9482
|
+
constructor(config, root, openBrowser = (url) => {
|
|
9483
|
+
const command = process.platform === "darwin" ? "open" : process.platform === "win32" ? "explorer.exe" : "xdg-open";
|
|
9484
|
+
return new Promise((resolve, reject) => {
|
|
9485
|
+
execFile(command, [url], (error) => {
|
|
9486
|
+
if (error) reject(/* @__PURE__ */ new Error("Could not open Console approval"));
|
|
9487
|
+
else resolve();
|
|
9488
|
+
});
|
|
9489
|
+
});
|
|
9490
|
+
}) {
|
|
9491
|
+
this.config = config;
|
|
9492
|
+
this.root = root;
|
|
9493
|
+
this.openBrowser = openBrowser;
|
|
9494
|
+
const endpoints = [
|
|
9495
|
+
config.authorizationUrl,
|
|
9496
|
+
config.tokenUrl,
|
|
9497
|
+
config.jwksUrl
|
|
9498
|
+
].map((value) => new URL(value));
|
|
9499
|
+
if (endpoints.some((url) => url.username || url.password || url.hash || url.protocol !== "https:" && !(url.protocol === "http:" && isLoopbackHostname(url.hostname))) || endpoints.some((url) => url.origin !== endpoints[0].origin)) throw new Error("OAuth endpoints must share a trusted secure origin");
|
|
9500
|
+
this.keys = createRemoteJWKSet(new URL(config.jwksUrl));
|
|
9501
|
+
try {
|
|
9502
|
+
const value = JSON.parse(readFileSync(join(root, "operator.json"), "utf8"));
|
|
9503
|
+
if (!value || typeof value !== "object" || !("issuer" in value) || !("subject" in value) || typeof value.issuer !== "string" || typeof value.subject !== "string") throw new Error("Invalid operator");
|
|
9504
|
+
this.operator = {
|
|
9505
|
+
issuer: value.issuer,
|
|
9506
|
+
subject: value.subject
|
|
9507
|
+
};
|
|
9508
|
+
} catch (error) {
|
|
9509
|
+
if (error.code !== "ENOENT") throw error;
|
|
9510
|
+
this.operator = null;
|
|
9511
|
+
}
|
|
9020
9512
|
}
|
|
9021
|
-
|
|
9022
|
-
|
|
9023
|
-
for (const [id, pairing] of this.pending) if (pairing.expiresAt <= now) this.pending.delete(id);
|
|
9513
|
+
cancel() {
|
|
9514
|
+
this.pending?.abort();
|
|
9024
9515
|
}
|
|
9025
|
-
|
|
9026
|
-
this.
|
|
9027
|
-
|
|
9028
|
-
this.
|
|
9029
|
-
|
|
9030
|
-
|
|
9031
|
-
expiresAt: this.now() + PENDING_TTL_MS,
|
|
9032
|
-
approved: false,
|
|
9033
|
-
bearerToken: null
|
|
9034
|
-
});
|
|
9516
|
+
removeOperator() {
|
|
9517
|
+
this.cancel();
|
|
9518
|
+
rmSync(join(this.root, "operator.json"), { force: true });
|
|
9519
|
+
this.operator = null;
|
|
9520
|
+
}
|
|
9521
|
+
metadata() {
|
|
9035
9522
|
return {
|
|
9036
|
-
|
|
9037
|
-
|
|
9523
|
+
protocolVersion: OPERATOR_OAUTH.protocolVersion,
|
|
9524
|
+
instance: this.instance,
|
|
9525
|
+
issuer: this.config.issuer,
|
|
9526
|
+
authorizationUrl: this.config.authorizationUrl,
|
|
9527
|
+
tokenUrl: this.config.tokenUrl,
|
|
9528
|
+
clientId: this.config.consoleClientId,
|
|
9529
|
+
operatorConfigured: !!this.operator
|
|
9038
9530
|
};
|
|
9039
9531
|
}
|
|
9040
|
-
|
|
9041
|
-
|
|
9042
|
-
|
|
9043
|
-
|
|
9532
|
+
async verify(token, scope, clientId) {
|
|
9533
|
+
const { payload } = await jwtVerify(token, this.keys, {
|
|
9534
|
+
algorithms: ["RS256"],
|
|
9535
|
+
issuer: this.config.issuer,
|
|
9536
|
+
audience: scope === LOCAL_SCOPE ? OPERATOR_OAUTH.localControlAudience : OPERATOR_OAUTH.provisioningAudience,
|
|
9537
|
+
requiredClaims: [
|
|
9538
|
+
"exp",
|
|
9539
|
+
"iat",
|
|
9540
|
+
"sub"
|
|
9541
|
+
],
|
|
9542
|
+
maxTokenAge: clientId === this.config.nativeClientId ? OPERATOR_OAUTH.nativeLifetimeSeconds : OPERATOR_OAUTH.consoleLifetimeSeconds
|
|
9543
|
+
});
|
|
9544
|
+
const claims = payload.ext;
|
|
9545
|
+
const scopes = typeof payload.scope === "string" ? payload.scope.split(" ") : payload.scp;
|
|
9546
|
+
if (!Array.isArray(scopes) || scopes.length !== 1 || scopes[0] !== scope || payload.client_id !== clientId || claims?.["moltnet:subject_type"] !== "human" || claims["moltnet:identity_id"] !== payload.sub || claims["moltnet:instance"] !== this.instance) throw new InvalidOperatorGrantError("Invalid operator grant");
|
|
9044
9547
|
return {
|
|
9045
|
-
|
|
9046
|
-
|
|
9548
|
+
issuer: payload.iss,
|
|
9549
|
+
subject: payload.sub,
|
|
9550
|
+
provisioning: claims["moltnet:provisioning"]
|
|
9047
9551
|
};
|
|
9048
9552
|
}
|
|
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
|
-
|
|
9553
|
+
async verifyBrowser(token) {
|
|
9554
|
+
const operator = await this.verify(token, LOCAL_SCOPE, this.config.consoleClientId);
|
|
9555
|
+
if (!this.operator || operator.issuer !== this.operator.issuer || operator.subject !== this.operator.subject) throw new InvalidOperatorGrantError("Native operator sign-in required");
|
|
9556
|
+
}
|
|
9557
|
+
async authorize(grant, signal) {
|
|
9558
|
+
if (this.active) throw new Error("An approval is already pending");
|
|
9559
|
+
this.active = true;
|
|
9560
|
+
const state = randomBytes(32).toString("base64url");
|
|
9561
|
+
const verifier = randomBytes(32).toString("base64url");
|
|
9562
|
+
const callback = `http://127.0.0.1:${this.config.callbackPort}/oauth/callback`;
|
|
9563
|
+
const scope = grant ? OPERATOR_OAUTH.provisioningScope : LOCAL_SCOPE;
|
|
9564
|
+
const controller = new AbortController();
|
|
9565
|
+
this.pending = controller;
|
|
9566
|
+
const timeout = setTimeout(() => controller.abort(), OPERATOR_OAUTH.nativeLifetimeSeconds * 1e3);
|
|
9567
|
+
const abort = () => controller.abort();
|
|
9568
|
+
signal?.addEventListener("abort", abort, { once: true });
|
|
9569
|
+
let server;
|
|
9570
|
+
try {
|
|
9571
|
+
if (signal?.aborted) throw new Error("Approval cancelled");
|
|
9572
|
+
const code = await new Promise((resolve, reject) => {
|
|
9573
|
+
let consumed = false;
|
|
9574
|
+
server = createServer((req, res) => {
|
|
9575
|
+
const url = new URL(req.url ?? "/", callback);
|
|
9576
|
+
res.setHeader("Cache-Control", "no-store");
|
|
9577
|
+
res.setHeader("Content-Security-Policy", "default-src 'none'; frame-ancestors 'none'");
|
|
9578
|
+
if (req.method !== "GET" || req.headers.host !== `127.0.0.1:${this.config.callbackPort}` || url.pathname !== "/oauth/callback" || url.searchParams.getAll("state").length !== 1 || url.searchParams.get("state") !== state || consumed) {
|
|
9579
|
+
res.writeHead(400);
|
|
9580
|
+
res.end("Invalid callback");
|
|
9581
|
+
return;
|
|
9582
|
+
}
|
|
9583
|
+
consumed = true;
|
|
9584
|
+
if (url.searchParams.has("error") || url.searchParams.getAll("code").length !== 1 || !url.searchParams.get("code")) {
|
|
9585
|
+
res.end("Approval cancelled. Return to Desktop.");
|
|
9586
|
+
reject(/* @__PURE__ */ new Error("Approval cancelled"));
|
|
9587
|
+
return;
|
|
9588
|
+
}
|
|
9589
|
+
res.end("Approval received. Return to Desktop.");
|
|
9590
|
+
resolve(url.searchParams.get("code"));
|
|
9591
|
+
});
|
|
9592
|
+
server.once("error", reject);
|
|
9593
|
+
controller.signal.addEventListener("abort", () => reject(/* @__PURE__ */ new Error("Approval cancelled")), { once: true });
|
|
9594
|
+
server.listen(this.config.callbackPort, "127.0.0.1", () => {
|
|
9595
|
+
const url = new URL(this.config.authorizationUrl);
|
|
9596
|
+
for (const [key, value] of Object.entries({
|
|
9597
|
+
client_id: this.config.nativeClientId,
|
|
9598
|
+
response_type: "code",
|
|
9599
|
+
scope,
|
|
9600
|
+
audience: grant ? OPERATOR_OAUTH.provisioningAudience : OPERATOR_OAUTH.localControlAudience,
|
|
9601
|
+
redirect_uri: callback,
|
|
9602
|
+
state,
|
|
9603
|
+
code_challenge_method: "S256",
|
|
9604
|
+
code_challenge: createHash("sha256").update(verifier).digest("base64url"),
|
|
9605
|
+
prompt: "consent",
|
|
9606
|
+
instance: this.instance,
|
|
9607
|
+
...grant ? { provisioning: JSON.stringify(grant) } : {}
|
|
9608
|
+
})) url.searchParams.set(key, value);
|
|
9609
|
+
try {
|
|
9610
|
+
Promise.resolve(this.openBrowser(url.href)).catch(() => reject(/* @__PURE__ */ new Error("Could not open Console approval")));
|
|
9611
|
+
} catch {
|
|
9612
|
+
reject(/* @__PURE__ */ new Error("Could not open Console approval"));
|
|
9613
|
+
}
|
|
9614
|
+
});
|
|
9615
|
+
});
|
|
9616
|
+
const response = await fetch(this.config.tokenUrl, {
|
|
9617
|
+
method: "POST",
|
|
9618
|
+
redirect: "error",
|
|
9619
|
+
signal: controller.signal,
|
|
9620
|
+
headers: { "content-type": "application/x-www-form-urlencoded" },
|
|
9621
|
+
body: new URLSearchParams({
|
|
9622
|
+
grant_type: "authorization_code",
|
|
9623
|
+
code,
|
|
9624
|
+
code_verifier: verifier,
|
|
9625
|
+
client_id: this.config.nativeClientId,
|
|
9626
|
+
redirect_uri: callback
|
|
9627
|
+
})
|
|
9628
|
+
});
|
|
9629
|
+
if (!response.ok) throw new Error("Approval exchange lost; request fresh approval");
|
|
9630
|
+
const tokens = await response.json();
|
|
9631
|
+
if (!tokens.access_token || tokens.refresh_token) throw new Error("Invalid approval token response");
|
|
9632
|
+
const operator = await this.verify(tokens.access_token, scope, this.config.nativeClientId);
|
|
9633
|
+
controller.signal.throwIfAborted();
|
|
9634
|
+
if (grant) {
|
|
9635
|
+
const actual = operator.provisioning;
|
|
9636
|
+
if (!actual || !Array.isArray(actual.scopes) || !actual.scopes.every((scope) => typeof scope === "string") || actual.agentId !== grant.agentId || actual.teamId !== grant.teamId || actual.operation !== grant.operation || actual.idempotencyKey !== grant.idempotencyKey || [...actual.scopes].sort().join(" ") !== [...grant.scopes].sort().join(" ")) throw new Error("Approval target differs from native request");
|
|
9637
|
+
}
|
|
9638
|
+
if (this.operator && (this.operator.issuer !== operator.issuer || this.operator.subject !== operator.subject)) throw new Error("Change the operator through native administration first");
|
|
9639
|
+
if (!this.operator) {
|
|
9640
|
+
try {
|
|
9641
|
+
writeFileSync(join(this.root, "operator.json"), JSON.stringify({
|
|
9642
|
+
issuer: operator.issuer,
|
|
9643
|
+
subject: operator.subject
|
|
9644
|
+
}), {
|
|
9645
|
+
mode: 384,
|
|
9646
|
+
flag: "wx"
|
|
9647
|
+
});
|
|
9648
|
+
} catch (error) {
|
|
9649
|
+
if (error.code !== "EEXIST") throw error;
|
|
9650
|
+
const pinned = JSON.parse(readFileSync(join(this.root, "operator.json"), "utf8"));
|
|
9651
|
+
if (!pinned || typeof pinned !== "object" || !("issuer" in pinned) || !("subject" in pinned) || pinned.issuer !== operator.issuer || pinned.subject !== operator.subject) throw new Error("Change the operator through native administration first");
|
|
9652
|
+
}
|
|
9653
|
+
this.operator = {
|
|
9654
|
+
issuer: operator.issuer,
|
|
9655
|
+
subject: operator.subject
|
|
9656
|
+
};
|
|
9657
|
+
}
|
|
9658
|
+
return tokens.access_token;
|
|
9659
|
+
} finally {
|
|
9660
|
+
clearTimeout(timeout);
|
|
9661
|
+
signal?.removeEventListener("abort", abort);
|
|
9662
|
+
server?.close();
|
|
9663
|
+
server?.closeAllConnections();
|
|
9664
|
+
this.active = false;
|
|
9665
|
+
this.pending = void 0;
|
|
9666
|
+
}
|
|
9074
9667
|
}
|
|
9075
9668
|
};
|
|
9076
|
-
function escapeHtml(value) {
|
|
9077
|
-
return value.replaceAll("&", "&").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
9669
|
//#endregion
|
|
9128
9670
|
//#region src/lib/agent-server/provider-login.ts
|
|
9129
9671
|
/**
|
|
@@ -9669,53 +10211,6 @@ async function attachExternalAgent(store, secretProviders, input, connectAgent =
|
|
|
9669
10211
|
releaseAlias();
|
|
9670
10212
|
}
|
|
9671
10213
|
}
|
|
9672
|
-
/** Load and authenticate the current config, then refresh its derived pin. */
|
|
9673
|
-
async function verifyAgentActivation(store, alias, managedSecretProviders, externalSecretProviders, connectAgent = connect, signal, teamId) {
|
|
9674
|
-
const activation = requireActivation(store, alias);
|
|
9675
|
-
teamId ??= activation.boundTeamId;
|
|
9676
|
-
const verified = activation.source === "managed" ? await verifyManagedActivation(store, activation, managedSecretProviders, connectAgent, signal, teamId) : await verifyExternalActivation(store, activation, externalSecretProviders, connectAgent, signal, teamId);
|
|
9677
|
-
assertSubjectMatches(verified.whoami, verified.config, "authenticated whoami", `agent "${activation.alias}" config`);
|
|
9678
|
-
if (verified.whoami.subjectId !== activation.subjectId) throw new AgentServerIdentityError("verification_failed", `authenticated whoami subject does not match agent "${activation.alias}" pinned activation`);
|
|
9679
|
-
const identity = identityFromConfig(verified.config);
|
|
9680
|
-
assertIdentityMatches(verified.whoami, identity, "authenticated whoami", `agent "${activation.alias}" config`);
|
|
9681
|
-
const boundTeamId = boundTeamIdFromWhoami(verified.whoami);
|
|
9682
|
-
if (!selectAgentKeyReference(verified.config, teamId)?.teamId && activation.boundTeamId !== boundTeamId) throw new AgentServerIdentityError("verification_failed", `authenticated whoami team binding does not match agent "${activation.alias}" pinned activation`);
|
|
9683
|
-
const refreshed = {
|
|
9684
|
-
...activation,
|
|
9685
|
-
...identity
|
|
9686
|
-
};
|
|
9687
|
-
if (activation.publicKey !== refreshed.publicKey || activation.fingerprint !== refreshed.fingerprint) {
|
|
9688
|
-
store.writeActivation(refreshed);
|
|
9689
|
-
process.stderr.write(`agent-server: refreshed the authenticated signing identity for ${JSON.stringify(activation.alias)}\n`);
|
|
9690
|
-
}
|
|
9691
|
-
return {
|
|
9692
|
-
activation: refreshed,
|
|
9693
|
-
config: verified.config,
|
|
9694
|
-
...boundTeamId ? { boundTeamId } : {}
|
|
9695
|
-
};
|
|
9696
|
-
}
|
|
9697
|
-
async function verifyManagedActivation(store, activation, secretProviders, connectAgent, signal, teamId) {
|
|
9698
|
-
const configPath = store.agentPath(activation.alias);
|
|
9699
|
-
const config = await readCurrentConfig(configPath);
|
|
9700
|
-
assertActivatedConfig(config, activation, configPath, requireConfigApiUrl(config, configPath), activation.apiUrl);
|
|
9701
|
-
return {
|
|
9702
|
-
config,
|
|
9703
|
-
whoami: await authenticateConfig(config, configPath, activation.apiUrl, secretProviders, connectAgent, signal, teamId)
|
|
9704
|
-
};
|
|
9705
|
-
}
|
|
9706
|
-
async function verifyExternalActivation(store, activation, secretProviders, connectAgent, signal, teamId) {
|
|
9707
|
-
const central = activation.configPath === store.agentPath(activation.alias);
|
|
9708
|
-
if (!central) externalAgentLocation(activation.configPath);
|
|
9709
|
-
assertTrustedConfigApiUrl(activation.configApiUrl);
|
|
9710
|
-
const config = await readCurrentConfig(activation.configPath);
|
|
9711
|
-
if (central && !hasAgentKeyConfiguration(config)) throw new AgentServerIdentityError("unsupported_credential", `central identity "${activation.alias}" needs a stored agent key before the Agent Server can run it`);
|
|
9712
|
-
assertActivatedConfig(config, activation, activation.configPath, requireTrustedConfigApiUrl(config, activation.configPath), activation.configApiUrl);
|
|
9713
|
-
const effectiveApiUrl = requireTrustedApiOverride(activation.apiUrl, activation.configApiUrl, activation.configPath);
|
|
9714
|
-
return {
|
|
9715
|
-
config,
|
|
9716
|
-
whoami: await authenticateConfig(config, activation.configPath, effectiveApiUrl, secretProviders, connectAgent, signal, teamId)
|
|
9717
|
-
};
|
|
9718
|
-
}
|
|
9719
10214
|
/** Never let request or activation metadata redirect persisted credentials. */
|
|
9720
10215
|
function requireTrustedApiOverride(override, configApiUrl, configPath) {
|
|
9721
10216
|
if (!override) return configApiUrl;
|
|
@@ -9849,10 +10344,133 @@ function publicAgentView(store, activation) {
|
|
|
9849
10344
|
function boundTeamIdFromWhoami(whoami) {
|
|
9850
10345
|
return whoami.credentialBinding?.bindingScope === "team" ? whoami.credentialBinding.boundTeamId ?? void 0 : void 0;
|
|
9851
10346
|
}
|
|
9852
|
-
function requireActivation(store, alias) {
|
|
9853
|
-
const activation = store.readActivation(alias);
|
|
9854
|
-
if (!activation) throw new AgentServerStoreError("not_found", `agent "${alias}" is not configured`);
|
|
9855
|
-
return activation;
|
|
10347
|
+
function requireActivation(store, alias) {
|
|
10348
|
+
const activation = store.readActivation(alias);
|
|
10349
|
+
if (!activation) throw new AgentServerStoreError("not_found", `agent "${alias}" is not configured`);
|
|
10350
|
+
return activation;
|
|
10351
|
+
}
|
|
10352
|
+
/** Enrollment may recover a central identity before its first online activation. */
|
|
10353
|
+
async function loadEnrollmentIdentity(store, alias) {
|
|
10354
|
+
if (store.readActivation(alias)) return loadAgentActivation(store, alias);
|
|
10355
|
+
const configPath = store.agentPath(alias);
|
|
10356
|
+
const config = await readCurrentConfig(configPath);
|
|
10357
|
+
if (!isCanonicalConfig(config)) throw new AgentServerIdentityError("verification_failed", "Enrollment requires a canonical agent identity");
|
|
10358
|
+
const apiUrl = requireTrustedConfigApiUrl(config, configPath);
|
|
10359
|
+
return {
|
|
10360
|
+
config,
|
|
10361
|
+
activation: {
|
|
10362
|
+
source: "external",
|
|
10363
|
+
alias,
|
|
10364
|
+
configPath,
|
|
10365
|
+
configApiUrl: apiUrl,
|
|
10366
|
+
apiUrl,
|
|
10367
|
+
subjectId: config.subject_id,
|
|
10368
|
+
...identityFromConfig(config),
|
|
10369
|
+
createdAt: config.registered_at
|
|
10370
|
+
}
|
|
10371
|
+
};
|
|
10372
|
+
}
|
|
10373
|
+
/** Resolve pinned local identity state without requiring a live API credential. */
|
|
10374
|
+
async function loadAgentActivation(store, alias) {
|
|
10375
|
+
const activation = requireActivation(store, alias);
|
|
10376
|
+
const configPath = activation.source === "managed" ? store.agentPath(alias) : activation.configPath;
|
|
10377
|
+
if (activation.source === "external" && configPath !== store.agentPath(alias)) externalAgentLocation(configPath);
|
|
10378
|
+
const config = await readCurrentConfig(configPath);
|
|
10379
|
+
const apiUrl = activation.source === "managed" ? activation.apiUrl : activation.configApiUrl;
|
|
10380
|
+
assertActivatedConfig(config, activation, configPath, requireTrustedConfigApiUrl(config, configPath), apiUrl);
|
|
10381
|
+
requireTrustedApiOverride(activation.apiUrl, apiUrl, configPath);
|
|
10382
|
+
return {
|
|
10383
|
+
activation,
|
|
10384
|
+
config
|
|
10385
|
+
};
|
|
10386
|
+
}
|
|
10387
|
+
//#endregion
|
|
10388
|
+
//#region src/lib/agent-server/team-credentials.ts
|
|
10389
|
+
var AGENT_SERVER_REQUIRED_SCOPES = [
|
|
10390
|
+
...DAEMON_MINIMUM_SCOPES,
|
|
10391
|
+
"team:read",
|
|
10392
|
+
"diary:read"
|
|
10393
|
+
];
|
|
10394
|
+
var TeamCredentialError = class extends Error {
|
|
10395
|
+
constructor(blocker) {
|
|
10396
|
+
super(blocker.message);
|
|
10397
|
+
this.blocker = blocker;
|
|
10398
|
+
}
|
|
10399
|
+
};
|
|
10400
|
+
function credentialBlocker(error) {
|
|
10401
|
+
return error instanceof TeamCredentialError ? error.blocker : {
|
|
10402
|
+
code: "agent_key_unavailable",
|
|
10403
|
+
message: "This team credential could not be verified or read its team resources.",
|
|
10404
|
+
remedy: "Check connectivity and team access, or renew this team credential."
|
|
10405
|
+
};
|
|
10406
|
+
}
|
|
10407
|
+
var snapshots = /* @__PURE__ */ new WeakMap();
|
|
10408
|
+
/** Capture is internal to the verifier; exported for injected verifier test doubles. */
|
|
10409
|
+
function captureTeamCredential(agent, snapshot) {
|
|
10410
|
+
snapshots.set(agent, snapshot);
|
|
10411
|
+
return agent;
|
|
10412
|
+
}
|
|
10413
|
+
function requireCredentialSnapshot(agent) {
|
|
10414
|
+
const snapshot = snapshots.get(agent);
|
|
10415
|
+
if (!snapshot) throw new Error("A verified team credential snapshot is required");
|
|
10416
|
+
return snapshot;
|
|
10417
|
+
}
|
|
10418
|
+
/** The only supervised credential path. No fallback reference or OAuth resolution. */
|
|
10419
|
+
async function verifyTeamActivation(store, alias, managed, external, connectImpl = connect, signal, teamId) {
|
|
10420
|
+
const activated = await loadAgentActivation(store, alias);
|
|
10421
|
+
const { config, activation } = activated;
|
|
10422
|
+
const reference = teamId ? config.agent_key_refs?.[teamId] : void 0;
|
|
10423
|
+
if (!teamId || !reference) throw new TeamCredentialError({
|
|
10424
|
+
code: "agent_key_missing",
|
|
10425
|
+
message: "No credential is indexed for this team.",
|
|
10426
|
+
remedy: "Enroll into this team or explicitly index its existing team-bound credential."
|
|
10427
|
+
});
|
|
10428
|
+
let agentKey;
|
|
10429
|
+
try {
|
|
10430
|
+
const resolved = await resolveAgentKey({
|
|
10431
|
+
...config,
|
|
10432
|
+
agent_key_ref: void 0,
|
|
10433
|
+
agent_key_refs: { [teamId]: reference }
|
|
10434
|
+
}, activation.source === "managed" ? managed : external, teamId);
|
|
10435
|
+
if (!resolved) throw new Error("Missing selected key");
|
|
10436
|
+
agentKey = resolved;
|
|
10437
|
+
} catch {
|
|
10438
|
+
throw new TeamCredentialError({
|
|
10439
|
+
code: "agent_key_unavailable",
|
|
10440
|
+
message: "The selected team credential is unavailable.",
|
|
10441
|
+
remedy: "Repair its secret provider or renew this team credential."
|
|
10442
|
+
});
|
|
10443
|
+
}
|
|
10444
|
+
const client = await connectImpl({
|
|
10445
|
+
agentKey,
|
|
10446
|
+
apiUrl: activation.apiUrl ?? (activation.source === "external" ? activation.configApiUrl : void 0),
|
|
10447
|
+
signal
|
|
10448
|
+
});
|
|
10449
|
+
const whoami = await client.agents.whoami({ signal });
|
|
10450
|
+
if (whoami.subjectType !== "agent" || whoami.subjectId !== activation.subjectId || whoami.publicKey !== config.keys.public_key || whoami.fingerprint !== config.keys.fingerprint || whoami.publicKey !== activation.publicKey || whoami.fingerprint !== activation.fingerprint || whoami.credentialBinding?.bindingScope !== "team" || whoami.credentialBinding.boundTeamId !== teamId) throw new TeamCredentialError({
|
|
10451
|
+
code: "agent_key_binding_invalid",
|
|
10452
|
+
message: "The selected credential does not match this identity and team.",
|
|
10453
|
+
remedy: "Renew the selected team credential."
|
|
10454
|
+
});
|
|
10455
|
+
const metadata = {
|
|
10456
|
+
keyId: whoami.credentialBinding.keyId,
|
|
10457
|
+
...Object.hasOwn(whoami.credentialBinding, "expiresAt") ? { expiresAt: whoami.credentialBinding.expiresAt } : {},
|
|
10458
|
+
scopes: [...whoami.scopes ?? []],
|
|
10459
|
+
verifiedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
10460
|
+
};
|
|
10461
|
+
store.writeCredentialMetadata(alias, teamId, metadata);
|
|
10462
|
+
const missing = AGENT_SERVER_REQUIRED_SCOPES.filter((scope) => !metadata.scopes.includes(scope));
|
|
10463
|
+
if (missing.length) throw new TeamCredentialError({
|
|
10464
|
+
code: "agent_key_scopes_insufficient",
|
|
10465
|
+
message: `This credential lacks ${missing.join(", ")}.`,
|
|
10466
|
+
remedy: "Renew through Console approval with the required desktop scopes."
|
|
10467
|
+
});
|
|
10468
|
+
activated.boundTeamId = teamId;
|
|
10469
|
+
return captureTeamCredential(activated, {
|
|
10470
|
+
agentKey,
|
|
10471
|
+
client,
|
|
10472
|
+
metadata
|
|
10473
|
+
});
|
|
9856
10474
|
}
|
|
9857
10475
|
//#endregion
|
|
9858
10476
|
//#region src/lib/agent-server/runs.ts
|
|
@@ -9893,6 +10511,19 @@ var INHERITED_MOLTNET_ENV_NAMES = new Set([
|
|
|
9893
10511
|
"MOLTNET_SIGNER_URL",
|
|
9894
10512
|
"MOLTNET_TRACE_IDLE_POLLING"
|
|
9895
10513
|
]);
|
|
10514
|
+
/** The runtime kind bundled with the agent; needs no registration. */
|
|
10515
|
+
var BUILT_IN_RUNTIME_KIND = "gondolin_pi";
|
|
10516
|
+
/** Persist only process status; worker stderr can contain provider secrets. */
|
|
10517
|
+
function describeFailure(code, signal) {
|
|
10518
|
+
if (signal) return {
|
|
10519
|
+
code: "run_signalled",
|
|
10520
|
+
message: `The worker was stopped by ${signal}.`
|
|
10521
|
+
};
|
|
10522
|
+
return {
|
|
10523
|
+
code: "run_failed",
|
|
10524
|
+
message: `The worker exited with code ${code ?? "unknown"}. Open the log for detail.`
|
|
10525
|
+
};
|
|
10526
|
+
}
|
|
9896
10527
|
var AgentServerRunError = class extends Error {
|
|
9897
10528
|
name = "AgentServerRunError";
|
|
9898
10529
|
constructor(code, message) {
|
|
@@ -9975,23 +10606,16 @@ var RunManager = class {
|
|
|
9975
10606
|
String(runtimeSettings.warmRetentionSec),
|
|
9976
10607
|
...target.extraArgs
|
|
9977
10608
|
];
|
|
10609
|
+
env["MOLTNET_AGENT_KEY"] = requireCredentialSnapshot(agent).agentKey;
|
|
10610
|
+
env["MOLTNET_API_URL"] = activation.apiUrl ?? (activation.source === "external" ? activation.configApiUrl : "");
|
|
9978
10611
|
if (activation.source === "managed") {
|
|
9979
|
-
|
|
9980
|
-
if (!reference || !config.keys.private_key_ref) throw new AgentServerRunError("invalid_spec", `managed config for "${activation.alias}" is missing canonical secret references`);
|
|
9981
|
-
env["MOLTNET_API_URL"] = activation.apiUrl;
|
|
9982
|
-
env["MOLTNET_AGENT_KEY_REF"] = formatSecretReferenceString(reference);
|
|
10612
|
+
if (!config.keys.private_key_ref) throw new AgentServerRunError("invalid_spec", "The managed signing key reference is missing");
|
|
9983
10613
|
env["MOLTNET_PRIVATE_KEY_REF"] = formatSecretReferenceString(config.keys.private_key_ref);
|
|
9984
10614
|
env["MOLTNET_SECRET_ROOT"] = this.store.secretsDir;
|
|
9985
|
-
} else {
|
|
9986
|
-
env["
|
|
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
|
-
}
|
|
10615
|
+
} else try {
|
|
10616
|
+
env["MOLTNET_PRIVATE_KEY"] = await resolveIdentitySeed(config, this.options.externalSecretProviders);
|
|
10617
|
+
} catch {
|
|
10618
|
+
throw new AgentServerRunError("invalid_spec", "The selected signing key could not be projected");
|
|
9995
10619
|
}
|
|
9996
10620
|
env["MOLTNET_EXPECTED_SUBJECT_ID"] = activation.subjectId;
|
|
9997
10621
|
env["MOLTNET_EXPECTED_SUBJECT_TYPE"] = "agent";
|
|
@@ -10025,9 +10649,9 @@ var RunManager = class {
|
|
|
10025
10649
|
}
|
|
10026
10650
|
async startReserved(spec, signal) {
|
|
10027
10651
|
this.assertStartOpen(signal);
|
|
10028
|
-
const agent = await (this.options.verifyActivationImpl ??
|
|
10029
|
-
if (cause instanceof
|
|
10030
|
-
throw
|
|
10652
|
+
const agent = await (this.options.verifyActivationImpl ?? verifyTeamActivation)(this.store, spec.agent, this.options.secretProviders, this.options.externalSecretProviders, void 0, signal, spec.teamId).catch((cause) => {
|
|
10653
|
+
if (cause instanceof TeamCredentialError || cause instanceof AgentServerStoreError) throw cause;
|
|
10654
|
+
throw new AgentServerIdentityError("verification_failed", `Cannot start agent "${spec.agent}" for team "${spec.teamId}": credential verification failed. Check the selected team key and activation.`);
|
|
10031
10655
|
});
|
|
10032
10656
|
this.assertStartOpen(signal);
|
|
10033
10657
|
if (agent.boundTeamId && agent.boundTeamId !== spec.teamId) throw new AgentServerRunError("invalid_spec", `agent "${spec.agent}" has a key bound to team ${agent.boundTeamId}; start the run with that team, or create a new agent with an enrollment token from team ${spec.teamId}`);
|
|
@@ -10104,7 +10728,8 @@ var RunManager = class {
|
|
|
10104
10728
|
id,
|
|
10105
10729
|
status: "running",
|
|
10106
10730
|
pid: child.pid,
|
|
10107
|
-
startedAt: (this.options.now?.() ?? /* @__PURE__ */ new Date()).toISOString()
|
|
10731
|
+
startedAt: (this.options.now?.() ?? /* @__PURE__ */ new Date()).toISOString(),
|
|
10732
|
+
credential: requireCredentialSnapshot(agent).metadata
|
|
10108
10733
|
};
|
|
10109
10734
|
child.once("exit", (code, signal) => {
|
|
10110
10735
|
const activeRun = this.active.get(id);
|
|
@@ -10119,7 +10744,8 @@ var RunManager = class {
|
|
|
10119
10744
|
});
|
|
10120
10745
|
this.persistRunCompletion(id, spec.agent, {
|
|
10121
10746
|
status,
|
|
10122
|
-
exitCode: code
|
|
10747
|
+
exitCode: code,
|
|
10748
|
+
...status === "failed" ? { lastError: describeFailure(code, signal) } : {}
|
|
10123
10749
|
});
|
|
10124
10750
|
});
|
|
10125
10751
|
child.once("error", (error) => {
|
|
@@ -10130,7 +10756,13 @@ var RunManager = class {
|
|
|
10130
10756
|
transition: "spawn_failed",
|
|
10131
10757
|
...safeRunError(error)
|
|
10132
10758
|
});
|
|
10133
|
-
this.persistRunCompletion(id, spec.agent, {
|
|
10759
|
+
this.persistRunCompletion(id, spec.agent, {
|
|
10760
|
+
status: "failed",
|
|
10761
|
+
lastError: {
|
|
10762
|
+
code: "spawn_failed",
|
|
10763
|
+
message: `The worker could not be started: ${error.message}`
|
|
10764
|
+
}
|
|
10765
|
+
});
|
|
10134
10766
|
});
|
|
10135
10767
|
this.store.writeRun(record);
|
|
10136
10768
|
this.log("info", "agent server run started", {
|
|
@@ -10157,7 +10789,7 @@ var RunManager = class {
|
|
|
10157
10789
|
}
|
|
10158
10790
|
async resolveRuntimeModule(spec, activated, cwd) {
|
|
10159
10791
|
const profiles = await resolveRuntimeProfiles({
|
|
10160
|
-
agent: await this.
|
|
10792
|
+
agent: await this.connectAgent(activated, spec.teamId),
|
|
10161
10793
|
profiles: spec.profiles,
|
|
10162
10794
|
teamId: spec.teamId,
|
|
10163
10795
|
cwd
|
|
@@ -10179,14 +10811,9 @@ var RunManager = class {
|
|
|
10179
10811
|
if (kind === "gondolin_pi") return void 0;
|
|
10180
10812
|
throw new AgentServerRunError("invalid_spec", `No local runtime is registered for profile kind "${kind}".`);
|
|
10181
10813
|
}
|
|
10182
|
-
|
|
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
|
-
});
|
|
10814
|
+
connectAgent(activated, teamId) {
|
|
10815
|
+
if (activated.boundTeamId !== teamId) throw new AgentServerRunError("invalid_spec", "Snapshot team mismatch");
|
|
10816
|
+
return Promise.resolve(requireCredentialSnapshot(activated).client);
|
|
10190
10817
|
}
|
|
10191
10818
|
stop(id) {
|
|
10192
10819
|
const record = this.store.readRun(id);
|
|
@@ -10211,6 +10838,22 @@ var RunManager = class {
|
|
|
10211
10838
|
if (!record) throw new AgentServerStoreError("not_found", `run "${id}" was not found`);
|
|
10212
10839
|
return record;
|
|
10213
10840
|
}
|
|
10841
|
+
async listAsync(limit) {
|
|
10842
|
+
const activeIds = new Set(this.active.keys());
|
|
10843
|
+
const records = await this.store.listRunsAsync(limit + activeIds.size, [...activeIds]);
|
|
10844
|
+
return [...records.filter((record) => activeIds.has(record.id)), ...records.filter((record) => !activeIds.has(record.id)).slice(0, limit)];
|
|
10845
|
+
}
|
|
10846
|
+
/** Freeze new starts only after local configuration has been persisted. */
|
|
10847
|
+
prepareServerRestart(persist) {
|
|
10848
|
+
if (this.closing || this.starting > 0 || this.active.size > 0) throw new AgentServerRunError("invalid_spec", "Stop running or starting work before changing connection settings");
|
|
10849
|
+
this.closing = true;
|
|
10850
|
+
try {
|
|
10851
|
+
return persist();
|
|
10852
|
+
} catch (error) {
|
|
10853
|
+
this.closing = false;
|
|
10854
|
+
throw error;
|
|
10855
|
+
}
|
|
10856
|
+
}
|
|
10214
10857
|
list(limit = Number.POSITIVE_INFINITY) {
|
|
10215
10858
|
if (!Number.isFinite(limit)) return this.store.listRuns();
|
|
10216
10859
|
const active = [...this.active.keys()].map((id) => this.store.readRun(id)).filter((record) => record !== null).sort((a, b) => b.startedAt.localeCompare(a.startedAt));
|
|
@@ -10467,6 +11110,21 @@ var LOCKFILE_NAMES = [
|
|
|
10467
11110
|
];
|
|
10468
11111
|
/** Local, operator-owned allowlist for executable daemon runtime modules. */
|
|
10469
11112
|
var RuntimeRegistry = class {
|
|
11113
|
+
displayDigests = /* @__PURE__ */ new Map();
|
|
11114
|
+
displayHash(path) {
|
|
11115
|
+
const stat = statSync(path);
|
|
11116
|
+
const key = String(path);
|
|
11117
|
+
const stamp = `${stat.dev}:${stat.ino}:${stat.size}:${stat.mtimeMs}:${stat.ctimeMs}`;
|
|
11118
|
+
const previous = this.displayDigests.get(key);
|
|
11119
|
+
if (previous?.stamp === stamp) return previous.hash;
|
|
11120
|
+
const hash = hashPath(path);
|
|
11121
|
+
if (this.displayDigests.size >= 128) this.displayDigests.clear();
|
|
11122
|
+
this.displayDigests.set(key, {
|
|
11123
|
+
stamp,
|
|
11124
|
+
hash
|
|
11125
|
+
});
|
|
11126
|
+
return hash;
|
|
11127
|
+
}
|
|
10470
11128
|
constructor(root) {
|
|
10471
11129
|
this.root = root;
|
|
10472
11130
|
}
|
|
@@ -10489,7 +11147,7 @@ var RuntimeRegistry = class {
|
|
|
10489
11147
|
const adapter = await loadDaemonRuntimeAdapter(specifier, { cwd });
|
|
10490
11148
|
if (adapter.runtimeKind !== kind) throw new Error(`Runtime module provides "${adapter.runtimeKind}", not registered kind "${kind}".`);
|
|
10491
11149
|
if (!moduleUrl.startsWith("file:")) throw new Error("Runtime registration must resolve to a local file URL.");
|
|
10492
|
-
const entryHash =
|
|
11150
|
+
const entryHash = hashPath(new URL(moduleUrl));
|
|
10493
11151
|
const lockfilePath = isPackageSpecifier(specifier) ? findLockfile(cwd) : void 0;
|
|
10494
11152
|
if (isPackageSpecifier(specifier) && !lockfilePath) throw new Error("Package runtime registration requires a pnpm, npm, Yarn, or Bun lockfile in the current project or a parent directory.");
|
|
10495
11153
|
const entry = {
|
|
@@ -10515,18 +11173,16 @@ var RuntimeRegistry = class {
|
|
|
10515
11173
|
writeRegistry(this.path, next);
|
|
10516
11174
|
return true;
|
|
10517
11175
|
}
|
|
10518
|
-
resolve(kind) {
|
|
11176
|
+
resolve(kind, options = {}) {
|
|
10519
11177
|
kind = assertStoreName("runtime kind", kind);
|
|
10520
11178
|
const entry = this.list().find((candidate) => candidate.kind === kind);
|
|
10521
11179
|
if (!entry) return void 0;
|
|
10522
|
-
|
|
10523
|
-
if (
|
|
11180
|
+
const digest = (path) => options.forDisplay ? this.displayHash(path) : hashPath(path);
|
|
11181
|
+
if (digest(new URL(entry.moduleUrl)) !== entry.entryHash) throw new Error(`Registered runtime "${kind}" has changed; re-register it before starting a run.`);
|
|
11182
|
+
if (entry.lockfilePath && (!existsSync(entry.lockfilePath) || digest(entry.lockfilePath) !== entry.lockfileHash)) throw new Error(`Registered runtime "${kind}" lockfile has changed; re-register it before starting a run.`);
|
|
10524
11183
|
return entry;
|
|
10525
11184
|
}
|
|
10526
11185
|
};
|
|
10527
|
-
function hashFile(url) {
|
|
10528
|
-
return createHash("sha256").update(readFileSync(url)).digest("hex");
|
|
10529
|
-
}
|
|
10530
11186
|
function hashPath(path) {
|
|
10531
11187
|
return createHash("sha256").update(readFileSync(path)).digest("hex");
|
|
10532
11188
|
}
|
|
@@ -10556,6 +11212,229 @@ function writeRegistry(path, entries) {
|
|
|
10556
11212
|
renameSync(temp, path);
|
|
10557
11213
|
}
|
|
10558
11214
|
//#endregion
|
|
11215
|
+
//#region src/lib/agent-server/readiness.ts
|
|
11216
|
+
/**
|
|
11217
|
+
* Whether a runtime profile can execute on *this* machine.
|
|
11218
|
+
*
|
|
11219
|
+
* A profile is authored in Console against a team; whether it can run depends
|
|
11220
|
+
* on local facts only the server knows — which provider keys are configured
|
|
11221
|
+
* here and which runtime kinds this machine can produce.
|
|
11222
|
+
*
|
|
11223
|
+
* The prerequisite comparison itself is **not** reimplemented here. Run start
|
|
11224
|
+
* calls `validateRuntimeProfilePrerequisites`, and the catalogue calls the same
|
|
11225
|
+
* function, so the composer cannot promise a run that startup would reject for
|
|
11226
|
+
* a reason the two evaluated differently.
|
|
11227
|
+
*/
|
|
11228
|
+
function deriveProfileReadiness(profile, machine) {
|
|
11229
|
+
const blockers = [];
|
|
11230
|
+
const env = {};
|
|
11231
|
+
for (const [name, configured] of machine.providerEnv) if (configured) env[name] = "configured";
|
|
11232
|
+
try {
|
|
11233
|
+
validateRuntimeProfilePrerequisites(profile, env, {
|
|
11234
|
+
tools: machine.inventory?.tools ?? profile.requiredTools,
|
|
11235
|
+
executables: machine.inventory?.executables ?? profile.requiredExecutables
|
|
11236
|
+
});
|
|
11237
|
+
} catch (error) {
|
|
11238
|
+
if (!(error instanceof RuntimeProfilePrerequisiteError)) throw error;
|
|
11239
|
+
for (const name of error.missingEnv) blockers.push({
|
|
11240
|
+
code: "env_missing",
|
|
11241
|
+
message: `${name} is not configured on this machine.`,
|
|
11242
|
+
remedy: "Add the key under Providers, then reopen this run."
|
|
11243
|
+
});
|
|
11244
|
+
for (const name of error.missingTools) blockers.push({
|
|
11245
|
+
code: "tool_missing",
|
|
11246
|
+
message: `The runtime does not provide the tool ${name}.`,
|
|
11247
|
+
remedy: `Use a profile whose runtime provides ${name}, or change the profile in Console.`
|
|
11248
|
+
});
|
|
11249
|
+
for (const name of error.missingExecutables) blockers.push({
|
|
11250
|
+
code: "executable_missing",
|
|
11251
|
+
message: `The runtime does not provide the executable ${name}.`,
|
|
11252
|
+
remedy: `Use a runtime that ships ${name}, or drop the requirement in Console.`
|
|
11253
|
+
});
|
|
11254
|
+
}
|
|
11255
|
+
if (!machine.runtimeKinds.has(profile.runtimeKind)) blockers.push({
|
|
11256
|
+
code: "runtime_unregistered",
|
|
11257
|
+
message: `Runtime kind ${profile.runtimeKind} is not available on this machine.`,
|
|
11258
|
+
remedy: "Register the runtime under Runtimes, then reopen this run."
|
|
11259
|
+
});
|
|
11260
|
+
return {
|
|
11261
|
+
ready: blockers.length === 0,
|
|
11262
|
+
blockers
|
|
11263
|
+
};
|
|
11264
|
+
}
|
|
11265
|
+
//#endregion
|
|
11266
|
+
//#region src/lib/agent-server/catalogue.ts
|
|
11267
|
+
async function buildCatalogue(options) {
|
|
11268
|
+
const { agent, machine, identityDefault } = options;
|
|
11269
|
+
const entries = await Promise.all(agent.teamIds.map(async (teamId) => {
|
|
11270
|
+
try {
|
|
11271
|
+
const result = await agent.readTeam(teamId);
|
|
11272
|
+
if (result.team.id !== teamId) throw new Error("Team response mismatch");
|
|
11273
|
+
const diaries = result.diaries.filter((diary) => diary.teamId === teamId).map(({ id, name }) => ({
|
|
11274
|
+
id,
|
|
11275
|
+
name
|
|
11276
|
+
}));
|
|
11277
|
+
return {
|
|
11278
|
+
team: {
|
|
11279
|
+
teamId,
|
|
11280
|
+
teamName: result.team.name,
|
|
11281
|
+
available: true,
|
|
11282
|
+
blockers: [],
|
|
11283
|
+
credential: result.credential,
|
|
11284
|
+
diaries,
|
|
11285
|
+
defaultDiaryId: resolveDefaultDiary(teamId, diaries, identityDefault)
|
|
11286
|
+
},
|
|
11287
|
+
profiles: result.profiles.filter((profile) => profile.teamId === teamId).map((profile) => ({
|
|
11288
|
+
...profile,
|
|
11289
|
+
...deriveProfileReadiness(profile, machine)
|
|
11290
|
+
}))
|
|
11291
|
+
};
|
|
11292
|
+
} catch (error) {
|
|
11293
|
+
return {
|
|
11294
|
+
team: {
|
|
11295
|
+
teamId,
|
|
11296
|
+
teamName: teamId,
|
|
11297
|
+
available: false,
|
|
11298
|
+
blockers: [credentialBlocker(error)],
|
|
11299
|
+
credential: agent.lastVerified(teamId),
|
|
11300
|
+
diaries: [],
|
|
11301
|
+
defaultDiaryId: null
|
|
11302
|
+
},
|
|
11303
|
+
profiles: []
|
|
11304
|
+
};
|
|
11305
|
+
}
|
|
11306
|
+
}));
|
|
11307
|
+
const teams = entries.map(({ team }) => team);
|
|
11308
|
+
const available = teams.filter((team) => team.available);
|
|
11309
|
+
return {
|
|
11310
|
+
teams,
|
|
11311
|
+
defaultTeamId: available.find((team) => team.teamId === identityDefault.teamId)?.teamId ?? available[0]?.teamId ?? null,
|
|
11312
|
+
profiles: entries.flatMap((entry) => entry.profiles)
|
|
11313
|
+
};
|
|
11314
|
+
}
|
|
11315
|
+
function resolveDefaultDiary(teamId, diaries, identityDefault) {
|
|
11316
|
+
const bound = diaries.find((diary) => diary.id === identityDefault.diaryId);
|
|
11317
|
+
if (bound && identityDefault.teamId === teamId) return bound.id;
|
|
11318
|
+
return diaries.length === 1 ? diaries[0]?.id ?? null : null;
|
|
11319
|
+
}
|
|
11320
|
+
//#endregion
|
|
11321
|
+
//#region src/lib/agent-server/enrollment.ts
|
|
11322
|
+
/** Native callers receive metadata only; approval and storage stay local. */
|
|
11323
|
+
async function enrollIdentityTeam(options) {
|
|
11324
|
+
const apiUrl = new URL(options.apiUrl);
|
|
11325
|
+
if (apiUrl.username || apiUrl.password || apiUrl.hash || apiUrl.protocol !== "https:" && !(apiUrl.protocol === "http:" && isLoopbackHostname(apiUrl.hostname))) throw new Error("Provisioning requires a configured secure API endpoint");
|
|
11326
|
+
const { activation, config } = await loadEnrollmentIdentity(options.store, options.alias);
|
|
11327
|
+
const identityApiUrl = activation.apiUrl ?? config.endpoints?.api;
|
|
11328
|
+
if (!identityApiUrl) throw new Error("The identity has no API environment configured");
|
|
11329
|
+
if (new URL(identityApiUrl).href.replace(/\/$/u, "") !== apiUrl.href.replace(/\/$/u, "")) throw new Error("The identity belongs to another API environment. Select that environment in Server settings before enrollment.");
|
|
11330
|
+
const registry = activation.source === "managed" ? options.managed : options.external;
|
|
11331
|
+
const replacement = options.input.mode === "replace" ? { teamId: options.input.teamId } : void 0;
|
|
11332
|
+
const providerName = replacement ? config.agent_key_refs?.[replacement.teamId]?.provider : activation.source === "managed" ? "file" : config.keys.private_key_ref?.provider;
|
|
11333
|
+
const provider = providerName ? registry.get(providerName) : void 0;
|
|
11334
|
+
if (!provider?.capabilities.write) throw new Error("Enrollment requires a writable identity secret provider");
|
|
11335
|
+
const configPath = activation.source === "managed" ? options.store.agentPath(options.alias) : activation.configPath;
|
|
11336
|
+
try {
|
|
11337
|
+
const result = await enrollTeam({
|
|
11338
|
+
idempotencyKey: options.input.idempotencyKey,
|
|
11339
|
+
provisioningContext: {
|
|
11340
|
+
teamId: options.input.teamId,
|
|
11341
|
+
operation: replacement ? "renew" : "enroll",
|
|
11342
|
+
scopes: [...AGENT_SERVER_REQUIRED_SCOPES]
|
|
11343
|
+
},
|
|
11344
|
+
replacement,
|
|
11345
|
+
provision: async () => {
|
|
11346
|
+
const grant = {
|
|
11347
|
+
agentId: config.subject_id,
|
|
11348
|
+
teamId: options.input.teamId,
|
|
11349
|
+
operation: replacement ? "renew" : "enroll",
|
|
11350
|
+
scopes: [...AGENT_SERVER_REQUIRED_SCOPES],
|
|
11351
|
+
idempotencyKey: options.input.idempotencyKey
|
|
11352
|
+
};
|
|
11353
|
+
let token;
|
|
11354
|
+
let agentProof;
|
|
11355
|
+
try {
|
|
11356
|
+
token = await options.oauth.authorize(grant, options.signal);
|
|
11357
|
+
agentProof = replacement ? void 0 : await signBytes(Buffer.from(enrollmentProofMessage({
|
|
11358
|
+
accessToken: token,
|
|
11359
|
+
grant
|
|
11360
|
+
})).toString("base64"), dirname(configPath), registry);
|
|
11361
|
+
} catch (error) {
|
|
11362
|
+
throw new ProvisioningNotStartedError(error);
|
|
11363
|
+
}
|
|
11364
|
+
const response = await fetch(new URL("/oauth2/provision", options.apiUrl), {
|
|
11365
|
+
method: "POST",
|
|
11366
|
+
redirect: "error",
|
|
11367
|
+
signal: options.signal ? AbortSignal.any([options.signal, AbortSignal.timeout(3e4)]) : AbortSignal.timeout(3e4),
|
|
11368
|
+
headers: {
|
|
11369
|
+
authorization: `Bearer ${token}`,
|
|
11370
|
+
"content-type": "application/json"
|
|
11371
|
+
},
|
|
11372
|
+
body: JSON.stringify(agentProof ? { agentProof } : {})
|
|
11373
|
+
});
|
|
11374
|
+
if (!response.ok) throw new Error("Provisioning unavailable; inspect recovery before fresh approval");
|
|
11375
|
+
const agentKey = await response.json();
|
|
11376
|
+
return {
|
|
11377
|
+
teamId: options.input.teamId,
|
|
11378
|
+
role: "member",
|
|
11379
|
+
agentKey
|
|
11380
|
+
};
|
|
11381
|
+
},
|
|
11382
|
+
configDir: dirname(configPath),
|
|
11383
|
+
secretProvider: provider,
|
|
11384
|
+
apiUrl: options.apiUrl
|
|
11385
|
+
});
|
|
11386
|
+
if (!options.store.readActivation(options.alias)) options.store.writeActivation(activation);
|
|
11387
|
+
return {
|
|
11388
|
+
state: "persisted",
|
|
11389
|
+
teamId: result.teamId,
|
|
11390
|
+
keyId: result.key.id
|
|
11391
|
+
};
|
|
11392
|
+
} catch (error) {
|
|
11393
|
+
if (error instanceof ProvisioningNotStartedError) throw error;
|
|
11394
|
+
if (error instanceof CredentialPersistenceError || error instanceof EnrollmentRecoveryError) return {
|
|
11395
|
+
state: "recovery_required",
|
|
11396
|
+
secretCaptured: error.secretCaptured,
|
|
11397
|
+
...error.issuedKeyId ? { issuedKeyId: error.issuedKeyId } : {},
|
|
11398
|
+
...error.recoveryPath ? { recoveryId: basename(error.recoveryPath) } : {},
|
|
11399
|
+
message: error.secretCaptured ? "The credential was captured locally but persistence is incomplete. Recover the captured credential before retrying enrollment." : "No credential secret was captured. Approved team membership may already exist. Retained retry context identifies this issuance; inspect it before requesting fresh approval."
|
|
11400
|
+
};
|
|
11401
|
+
throw new Error("Team enrollment could not be completed", { cause: error });
|
|
11402
|
+
}
|
|
11403
|
+
}
|
|
11404
|
+
//#endregion
|
|
11405
|
+
//#region src/lib/agent-server/identity-binding.ts
|
|
11406
|
+
/**
|
|
11407
|
+
* The identity-wide team/diary binding, read from `<identityDir>/env`.
|
|
11408
|
+
*
|
|
11409
|
+
* This mirrors the Go CLI's `identityDefaultBinding` (`context_store.go:219`),
|
|
11410
|
+
* which is the fallback the CLI uses when a working directory has no
|
|
11411
|
+
* location-keyed binding in `contexts.json`.
|
|
11412
|
+
*
|
|
11413
|
+
* The desktop cannot use the CLI's *location* bindings at all — its composer
|
|
11414
|
+
* has no working directory to key on — but it can honour this identity-wide
|
|
11415
|
+
* default, so an operator who ran `moltnet context set` sees their familiar
|
|
11416
|
+
* team preselected rather than an arbitrary first entry.
|
|
11417
|
+
*
|
|
11418
|
+
* Like the CLI, a half-filled pair is treated as no binding: a team without a
|
|
11419
|
+
* diary is exactly the state `validateContextBinding` rejects.
|
|
11420
|
+
*/
|
|
11421
|
+
function readIdentityDefaultBinding(identityDir) {
|
|
11422
|
+
let contents;
|
|
11423
|
+
try {
|
|
11424
|
+
contents = readFileSync(join(identityDir, "env"), "utf8");
|
|
11425
|
+
} catch {
|
|
11426
|
+
return {};
|
|
11427
|
+
}
|
|
11428
|
+
const env = parseEnv(contents);
|
|
11429
|
+
const teamId = env["MOLTNET_TEAM_ID"]?.trim();
|
|
11430
|
+
const diaryId = env["MOLTNET_DIARY_ID"]?.trim();
|
|
11431
|
+
if (!teamId || !diaryId) return {};
|
|
11432
|
+
return {
|
|
11433
|
+
teamId,
|
|
11434
|
+
diaryId
|
|
11435
|
+
};
|
|
11436
|
+
}
|
|
11437
|
+
//#endregion
|
|
10559
11438
|
//#region src/lib/agent-server/protocol.ts
|
|
10560
11439
|
var DateTime = Type.String({ format: "date-time" });
|
|
10561
11440
|
var StringList = Type.Array(Type.String());
|
|
@@ -10601,6 +11480,63 @@ var AgentServerProviderSchema = Type.Object({
|
|
|
10601
11480
|
models: ProviderModelList,
|
|
10602
11481
|
hasApiKey: Type.Boolean()
|
|
10603
11482
|
}, { $id: "AgentServerProvider" });
|
|
11483
|
+
var CredentialMetadataSchema = Type.Object({
|
|
11484
|
+
keyId: Type.String(),
|
|
11485
|
+
expiresAt: Type.Optional(Type.Union([DateTime, Type.Null()])),
|
|
11486
|
+
verifiedAt: DateTime,
|
|
11487
|
+
scopes: StringList
|
|
11488
|
+
});
|
|
11489
|
+
var AgentServerCatalogueTeamSchema = Type.Object({
|
|
11490
|
+
teamId: Type.String(),
|
|
11491
|
+
teamName: Type.String(),
|
|
11492
|
+
available: Type.Boolean(),
|
|
11493
|
+
credential: Type.Optional(CredentialMetadataSchema),
|
|
11494
|
+
blockers: Type.Array(Type.Object({
|
|
11495
|
+
code: Type.String(),
|
|
11496
|
+
message: Type.String(),
|
|
11497
|
+
remedy: Type.String()
|
|
11498
|
+
})),
|
|
11499
|
+
diaries: Type.Array(Type.Object({
|
|
11500
|
+
id: Type.String(),
|
|
11501
|
+
name: Type.String()
|
|
11502
|
+
})),
|
|
11503
|
+
defaultDiaryId: Type.Union([Type.String(), Type.Null()])
|
|
11504
|
+
}, { $id: "AgentServerCatalogueTeam" });
|
|
11505
|
+
/**
|
|
11506
|
+
* Composed from the canonical `RuntimeProfile` schema rather than restated, so
|
|
11507
|
+
* the wire contract cannot drift from the profile the API serves — and so the
|
|
11508
|
+
* constrained fields keep their real unions instead of degrading to `string`.
|
|
11509
|
+
*/
|
|
11510
|
+
var AgentServerCatalogueProfileSchema = Type.Intersect([Type.Pick(RuntimeProfile, [
|
|
11511
|
+
"id",
|
|
11512
|
+
"name",
|
|
11513
|
+
"teamId",
|
|
11514
|
+
"description",
|
|
11515
|
+
"provider",
|
|
11516
|
+
"model",
|
|
11517
|
+
"runtimeKind",
|
|
11518
|
+
"toolEnforcement",
|
|
11519
|
+
"defaultWorkspaceMode",
|
|
11520
|
+
"maxTurns",
|
|
11521
|
+
"revision",
|
|
11522
|
+
"definitionCid",
|
|
11523
|
+
"requiredEnv",
|
|
11524
|
+
"requiredTools",
|
|
11525
|
+
"requiredExecutables"
|
|
11526
|
+
]), Type.Object({
|
|
11527
|
+
ready: Type.Boolean(),
|
|
11528
|
+
blockers: Type.Array(Type.Object({
|
|
11529
|
+
code: Type.String(),
|
|
11530
|
+
message: Type.String(),
|
|
11531
|
+
remedy: Type.String()
|
|
11532
|
+
}))
|
|
11533
|
+
})], { $id: "AgentServerCatalogueProfile" });
|
|
11534
|
+
var AgentServerCatalogueSchema = Type.Object({
|
|
11535
|
+
teams: Type.Array(schemaRef(AgentServerCatalogueTeamSchema)),
|
|
11536
|
+
defaultTeamId: Type.Union([Type.String(), Type.Null()]),
|
|
11537
|
+
profiles: Type.Array(schemaRef(AgentServerCatalogueProfileSchema))
|
|
11538
|
+
}, { $id: "AgentServerCatalogue" });
|
|
11539
|
+
var CatalogueQuerySchema = Type.Object({ identity: Type.String({ minLength: 1 }) });
|
|
10604
11540
|
var AgentServerRunRecordSchema = Type.Object({
|
|
10605
11541
|
id: Type.String(),
|
|
10606
11542
|
agent: Type.String(),
|
|
@@ -10617,6 +11553,11 @@ var AgentServerRunRecordSchema = Type.Object({
|
|
|
10617
11553
|
]),
|
|
10618
11554
|
pid: Type.Optional(Type.Number()),
|
|
10619
11555
|
exitCode: Type.Optional(Type.Union([Type.Number(), Type.Null()])),
|
|
11556
|
+
credential: Type.Optional(CredentialMetadataSchema),
|
|
11557
|
+
lastError: Type.Optional(Type.Object({
|
|
11558
|
+
code: Type.String(),
|
|
11559
|
+
message: Type.String()
|
|
11560
|
+
})),
|
|
10620
11561
|
startedAt: DateTime,
|
|
10621
11562
|
endedAt: Type.Optional(DateTime)
|
|
10622
11563
|
}, { $id: "AgentServerRunRecord" });
|
|
@@ -10653,12 +11594,6 @@ var AgentServerStatusSchema = Type.Object({
|
|
|
10653
11594
|
warmRetentionSec: Type.Integer({ minimum: 0 })
|
|
10654
11595
|
})
|
|
10655
11596
|
}, { $id: "AgentServerStatus" });
|
|
10656
|
-
var PairingStartedSchema = Type.Object({
|
|
10657
|
-
pairingId: Type.String(),
|
|
10658
|
-
approvalPath: Type.String()
|
|
10659
|
-
}, { $id: "PairingStarted" });
|
|
10660
|
-
var PairingClaimedSchema = Type.Object({ token: Type.String() }, { $id: "PairingClaimed" });
|
|
10661
|
-
var PairingParamsSchema = Type.Object({ pairingId: Type.String() });
|
|
10662
11597
|
var ProviderParamsSchema = Type.Object({ providerId: Type.String() });
|
|
10663
11598
|
var AgentParamsSchema = Type.Object({ agentName: Type.String() });
|
|
10664
11599
|
var RunParamsSchema = Type.Object({ runId: Type.String() });
|
|
@@ -10704,19 +11639,20 @@ var AGENT_SERVER_SCHEMAS = [
|
|
|
10704
11639
|
AgentServerIdentitySchema,
|
|
10705
11640
|
AgentServerTaskTypeSchema,
|
|
10706
11641
|
AgentServerProviderSchema,
|
|
11642
|
+
AgentServerCatalogueTeamSchema,
|
|
11643
|
+
AgentServerCatalogueProfileSchema,
|
|
11644
|
+
AgentServerCatalogueSchema,
|
|
10707
11645
|
AgentServerRunRecordSchema,
|
|
10708
11646
|
AgentServerRunSchema,
|
|
10709
11647
|
AgentServerSubscriptionSchema,
|
|
10710
11648
|
AgentServerSubscriptionLoginSchema,
|
|
10711
11649
|
AgentServerStatusSchema,
|
|
10712
|
-
PairingStartedSchema,
|
|
10713
|
-
PairingClaimedSchema,
|
|
10714
11650
|
ReconcileAgentResultSchema,
|
|
10715
11651
|
DiscoverModelsSchema,
|
|
10716
11652
|
CancelledSubscriptionSchema,
|
|
10717
11653
|
LogStreamSchema
|
|
10718
11654
|
];
|
|
10719
|
-
var
|
|
11655
|
+
var localControlSecurity = [{ agentServerToken: [] }];
|
|
10720
11656
|
var problemResponse = { default: schemaRef(AgentServerProblemSchema) };
|
|
10721
11657
|
var AgentServerRouteSchemas = {
|
|
10722
11658
|
health: {
|
|
@@ -10724,27 +11660,10 @@ var AgentServerRouteSchemas = {
|
|
|
10724
11660
|
tags: ["system"],
|
|
10725
11661
|
response: { 200: schemaRef(AgentServerHealthSchema) }
|
|
10726
11662
|
},
|
|
10727
|
-
startPairing: {
|
|
10728
|
-
operationId: "startAgentServerPairing",
|
|
10729
|
-
tags: ["pairing"],
|
|
10730
|
-
response: {
|
|
10731
|
-
201: schemaRef(PairingStartedSchema),
|
|
10732
|
-
...problemResponse
|
|
10733
|
-
}
|
|
10734
|
-
},
|
|
10735
|
-
claimPairing: {
|
|
10736
|
-
operationId: "claimAgentServerPairing",
|
|
10737
|
-
tags: ["pairing"],
|
|
10738
|
-
params: PairingParamsSchema,
|
|
10739
|
-
response: {
|
|
10740
|
-
200: schemaRef(PairingClaimedSchema),
|
|
10741
|
-
...problemResponse
|
|
10742
|
-
}
|
|
10743
|
-
},
|
|
10744
11663
|
status: {
|
|
10745
11664
|
operationId: "getAgentServerStatus",
|
|
10746
11665
|
tags: ["system"],
|
|
10747
|
-
security:
|
|
11666
|
+
security: localControlSecurity,
|
|
10748
11667
|
response: {
|
|
10749
11668
|
200: schemaRef(AgentServerStatusSchema),
|
|
10750
11669
|
...problemResponse
|
|
@@ -10753,7 +11672,7 @@ var AgentServerRouteSchemas = {
|
|
|
10753
11672
|
listAgents: {
|
|
10754
11673
|
operationId: "listAgentServerAgents",
|
|
10755
11674
|
tags: ["agents"],
|
|
10756
|
-
security:
|
|
11675
|
+
security: localControlSecurity,
|
|
10757
11676
|
response: {
|
|
10758
11677
|
200: Type.Array(schemaRef(AgentServerAgentSchema)),
|
|
10759
11678
|
...problemResponse
|
|
@@ -10762,17 +11681,44 @@ var AgentServerRouteSchemas = {
|
|
|
10762
11681
|
createAgent: {
|
|
10763
11682
|
operationId: "createAgentServerAgent",
|
|
10764
11683
|
tags: ["agents"],
|
|
10765
|
-
security:
|
|
11684
|
+
security: localControlSecurity,
|
|
10766
11685
|
body: CreateAgentSchema,
|
|
10767
11686
|
response: {
|
|
10768
11687
|
201: schemaRef(AgentServerAgentSchema),
|
|
10769
11688
|
...problemResponse
|
|
10770
11689
|
}
|
|
10771
11690
|
},
|
|
11691
|
+
enrollTeam: {
|
|
11692
|
+
operationId: "enrollAgentServerTeam",
|
|
11693
|
+
tags: ["agents"],
|
|
11694
|
+
security: localControlSecurity,
|
|
11695
|
+
params: AgentParamsSchema,
|
|
11696
|
+
body: Type.Intersect([Type.Object({
|
|
11697
|
+
teamId: Type.String({ format: "uuid" }),
|
|
11698
|
+
idempotencyKey: Type.String({
|
|
11699
|
+
minLength: 1,
|
|
11700
|
+
maxLength: 256
|
|
11701
|
+
})
|
|
11702
|
+
}), Type.Union([Type.Object({ mode: Type.Literal("enroll") }), Type.Object({ mode: Type.Literal("replace") })])]),
|
|
11703
|
+
response: {
|
|
11704
|
+
200: Type.Union([Type.Object({
|
|
11705
|
+
state: Type.Literal("persisted"),
|
|
11706
|
+
teamId: Type.String(),
|
|
11707
|
+
keyId: Type.String()
|
|
11708
|
+
}), Type.Object({
|
|
11709
|
+
state: Type.Literal("recovery_required"),
|
|
11710
|
+
secretCaptured: Type.Boolean(),
|
|
11711
|
+
issuedKeyId: Type.Optional(Type.String()),
|
|
11712
|
+
recoveryId: Type.String(),
|
|
11713
|
+
message: Type.String()
|
|
11714
|
+
})]),
|
|
11715
|
+
...problemResponse
|
|
11716
|
+
}
|
|
11717
|
+
},
|
|
10772
11718
|
reconcileAgent: {
|
|
10773
11719
|
operationId: "reconcileAgentServerAgent",
|
|
10774
11720
|
tags: ["agents"],
|
|
10775
|
-
security:
|
|
11721
|
+
security: localControlSecurity,
|
|
10776
11722
|
params: AgentParamsSchema,
|
|
10777
11723
|
body: ReconcileAgentSchema,
|
|
10778
11724
|
response: {
|
|
@@ -10783,7 +11729,7 @@ var AgentServerRouteSchemas = {
|
|
|
10783
11729
|
listProviders: {
|
|
10784
11730
|
operationId: "listAgentServerProviders",
|
|
10785
11731
|
tags: ["providers"],
|
|
10786
|
-
security:
|
|
11732
|
+
security: localControlSecurity,
|
|
10787
11733
|
response: {
|
|
10788
11734
|
200: Type.Record(Type.String(), schemaRef(AgentServerProviderSchema)),
|
|
10789
11735
|
...problemResponse
|
|
@@ -10792,7 +11738,7 @@ var AgentServerRouteSchemas = {
|
|
|
10792
11738
|
discoverModels: {
|
|
10793
11739
|
operationId: "discoverAgentServerProviderModels",
|
|
10794
11740
|
tags: ["providers"],
|
|
10795
|
-
security:
|
|
11741
|
+
security: localControlSecurity,
|
|
10796
11742
|
params: ProviderParamsSchema,
|
|
10797
11743
|
response: {
|
|
10798
11744
|
200: schemaRef(DiscoverModelsSchema),
|
|
@@ -10802,7 +11748,7 @@ var AgentServerRouteSchemas = {
|
|
|
10802
11748
|
putProvider: {
|
|
10803
11749
|
operationId: "putAgentServerProvider",
|
|
10804
11750
|
tags: ["providers"],
|
|
10805
|
-
security:
|
|
11751
|
+
security: localControlSecurity,
|
|
10806
11752
|
params: ProviderParamsSchema,
|
|
10807
11753
|
body: PutProviderSchema,
|
|
10808
11754
|
response: {
|
|
@@ -10813,7 +11759,7 @@ var AgentServerRouteSchemas = {
|
|
|
10813
11759
|
deleteProvider: {
|
|
10814
11760
|
operationId: "deleteAgentServerProvider",
|
|
10815
11761
|
tags: ["providers"],
|
|
10816
|
-
security:
|
|
11762
|
+
security: localControlSecurity,
|
|
10817
11763
|
params: ProviderParamsSchema,
|
|
10818
11764
|
response: {
|
|
10819
11765
|
204: Type.Any(),
|
|
@@ -10823,7 +11769,7 @@ var AgentServerRouteSchemas = {
|
|
|
10823
11769
|
listSubscriptions: {
|
|
10824
11770
|
operationId: "listAgentServerSubscriptions",
|
|
10825
11771
|
tags: ["subscriptions"],
|
|
10826
|
-
security:
|
|
11772
|
+
security: localControlSecurity,
|
|
10827
11773
|
response: {
|
|
10828
11774
|
200: Type.Array(schemaRef(AgentServerSubscriptionSchema)),
|
|
10829
11775
|
...problemResponse
|
|
@@ -10832,7 +11778,7 @@ var AgentServerRouteSchemas = {
|
|
|
10832
11778
|
startSubscriptionLogin: {
|
|
10833
11779
|
operationId: "startAgentServerSubscriptionLogin",
|
|
10834
11780
|
tags: ["subscriptions"],
|
|
10835
|
-
security:
|
|
11781
|
+
security: localControlSecurity,
|
|
10836
11782
|
params: ProviderParamsSchema,
|
|
10837
11783
|
response: {
|
|
10838
11784
|
201: schemaRef(AgentServerSubscriptionLoginSchema),
|
|
@@ -10842,7 +11788,7 @@ var AgentServerRouteSchemas = {
|
|
|
10842
11788
|
getSubscriptionLogin: {
|
|
10843
11789
|
operationId: "getAgentServerSubscriptionLogin",
|
|
10844
11790
|
tags: ["subscriptions"],
|
|
10845
|
-
security:
|
|
11791
|
+
security: localControlSecurity,
|
|
10846
11792
|
params: ProviderParamsSchema,
|
|
10847
11793
|
response: {
|
|
10848
11794
|
200: schemaRef(AgentServerSubscriptionLoginSchema),
|
|
@@ -10852,17 +11798,27 @@ var AgentServerRouteSchemas = {
|
|
|
10852
11798
|
cancelSubscriptionLogin: {
|
|
10853
11799
|
operationId: "cancelAgentServerSubscriptionLogin",
|
|
10854
11800
|
tags: ["subscriptions"],
|
|
10855
|
-
security:
|
|
11801
|
+
security: localControlSecurity,
|
|
10856
11802
|
params: ProviderParamsSchema,
|
|
10857
11803
|
response: {
|
|
10858
11804
|
200: schemaRef(CancelledSubscriptionSchema),
|
|
10859
11805
|
...problemResponse
|
|
10860
11806
|
}
|
|
10861
11807
|
},
|
|
11808
|
+
catalogue: {
|
|
11809
|
+
operationId: "getAgentServerCatalogue",
|
|
11810
|
+
tags: ["catalogue"],
|
|
11811
|
+
security: localControlSecurity,
|
|
11812
|
+
querystring: CatalogueQuerySchema,
|
|
11813
|
+
response: {
|
|
11814
|
+
200: schemaRef(AgentServerCatalogueSchema),
|
|
11815
|
+
...problemResponse
|
|
11816
|
+
}
|
|
11817
|
+
},
|
|
10862
11818
|
listRuns: {
|
|
10863
11819
|
operationId: "listAgentServerRuns",
|
|
10864
11820
|
tags: ["runs"],
|
|
10865
|
-
security:
|
|
11821
|
+
security: localControlSecurity,
|
|
10866
11822
|
response: {
|
|
10867
11823
|
200: Type.Array(schemaRef(AgentServerRunSchema)),
|
|
10868
11824
|
...problemResponse
|
|
@@ -10871,7 +11827,7 @@ var AgentServerRouteSchemas = {
|
|
|
10871
11827
|
startRun: {
|
|
10872
11828
|
operationId: "startAgentServerRun",
|
|
10873
11829
|
tags: ["runs"],
|
|
10874
|
-
security:
|
|
11830
|
+
security: localControlSecurity,
|
|
10875
11831
|
body: StartRunSchema,
|
|
10876
11832
|
response: {
|
|
10877
11833
|
201: schemaRef(AgentServerRunSchema),
|
|
@@ -10881,7 +11837,7 @@ var AgentServerRouteSchemas = {
|
|
|
10881
11837
|
stopRun: {
|
|
10882
11838
|
operationId: "stopAgentServerRun",
|
|
10883
11839
|
tags: ["runs"],
|
|
10884
|
-
security:
|
|
11840
|
+
security: localControlSecurity,
|
|
10885
11841
|
params: RunParamsSchema,
|
|
10886
11842
|
response: {
|
|
10887
11843
|
200: schemaRef(AgentServerRunRecordSchema),
|
|
@@ -10891,7 +11847,7 @@ var AgentServerRouteSchemas = {
|
|
|
10891
11847
|
streamRunLogs: {
|
|
10892
11848
|
operationId: "streamAgentServerRunLogs",
|
|
10893
11849
|
tags: ["runs"],
|
|
10894
|
-
security:
|
|
11850
|
+
security: localControlSecurity,
|
|
10895
11851
|
params: RunParamsSchema,
|
|
10896
11852
|
response: {
|
|
10897
11853
|
200: schemaRef(LogStreamSchema),
|
|
@@ -10906,9 +11862,8 @@ var AgentServerRouteSchemas = {
|
|
|
10906
11862
|
* loopback-companion security profile (#2066): loopback Host enforcement,
|
|
10907
11863
|
* exact-origin CORS, Fetch-Metadata guards, strict JSON parsing.
|
|
10908
11864
|
*
|
|
10909
|
-
*
|
|
10910
|
-
*
|
|
10911
|
-
* origin-bound token issued by the one-click pairing ceremony.
|
|
11865
|
+
* Control routes require a native process grant or an OAuth token bound to
|
|
11866
|
+
* the native operator and this server instance. Origin checks apply to both.
|
|
10912
11867
|
*/
|
|
10913
11868
|
var AGENT_SERVER_TOKEN_HEADER = "x-moltnet-agent-server-token";
|
|
10914
11869
|
var BODY_LIMIT = 64 * 1024;
|
|
@@ -11024,7 +11979,9 @@ function requestOperationSignal(request, shutdownSignal) {
|
|
|
11024
11979
|
return shutdownSignal ? AbortSignal.any([disconnected.signal, shutdownSignal]) : disconnected.signal;
|
|
11025
11980
|
}
|
|
11026
11981
|
function buildAgentServer(options) {
|
|
11027
|
-
const {
|
|
11982
|
+
const { nativeGrant } = options;
|
|
11983
|
+
const oauth = options.operatorOAuth;
|
|
11984
|
+
let restartRequired = false;
|
|
11028
11985
|
const fastifyOptions = {
|
|
11029
11986
|
bodyLimit: BODY_LIMIT,
|
|
11030
11987
|
...options.tls ? { https: options.tls } : {}
|
|
@@ -11035,8 +11992,9 @@ function buildAgentServer(options) {
|
|
|
11035
11992
|
}) : Fastify(fastifyOptions);
|
|
11036
11993
|
options.registerOpenApi?.(app);
|
|
11037
11994
|
for (const schema of AGENT_SERVER_SCHEMAS) app.addSchema(schema);
|
|
11995
|
+
const browserOrigins = new OriginAllowlist(options.allowedOrigins);
|
|
11038
11996
|
registerLoopbackSecurity(app, {
|
|
11039
|
-
|
|
11997
|
+
isOriginAllowed: (origin) => origin === "moltnet-agent-desktop://native" || browserOrigins.has(origin),
|
|
11040
11998
|
...options.selfOrigin ? { selfOrigins: [options.selfOrigin] } : {},
|
|
11041
11999
|
allowedHeaders: [AGENT_SERVER_TOKEN_HEADER],
|
|
11042
12000
|
methods: [
|
|
@@ -11047,40 +12005,185 @@ function buildAgentServer(options) {
|
|
|
11047
12005
|
"OPTIONS"
|
|
11048
12006
|
]
|
|
11049
12007
|
});
|
|
12008
|
+
let verificationWindow = Date.now();
|
|
12009
|
+
let verifications = 0;
|
|
12010
|
+
const browserVerification = /* @__PURE__ */ new WeakMap();
|
|
12011
|
+
function verifyBrowser(request, token) {
|
|
12012
|
+
const previous = browserVerification.get(request);
|
|
12013
|
+
if (previous) return previous;
|
|
12014
|
+
if (Date.now() - verificationWindow >= RATE_LIMIT_WINDOW_MS) {
|
|
12015
|
+
verificationWindow = Date.now();
|
|
12016
|
+
verifications = 0;
|
|
12017
|
+
}
|
|
12018
|
+
if (++verifications > RATE_LIMIT_MAX) throw new AgentServerHttpError(429, "rate_limited", "Too many authorization attempts");
|
|
12019
|
+
if (!oauth) throw new AgentServerHttpError(503, "oauth_unavailable", "Local OAuth is not configured");
|
|
12020
|
+
const pending = oauth.verifyBrowser(token);
|
|
12021
|
+
browserVerification.set(request, pending);
|
|
12022
|
+
return pending;
|
|
12023
|
+
}
|
|
11050
12024
|
app.register(rateLimit, {
|
|
11051
|
-
global:
|
|
12025
|
+
global: true,
|
|
11052
12026
|
max: options.rateLimitMax ?? RATE_LIMIT_MAX,
|
|
11053
12027
|
timeWindow: RATE_LIMIT_WINDOW_MS,
|
|
11054
12028
|
errorResponseBuilder: () => new AgentServerHttpError(429, "rate_limited", "Too many requests"),
|
|
11055
|
-
keyGenerator: (request) => {
|
|
12029
|
+
keyGenerator: async (request) => {
|
|
11056
12030
|
const origin = request.headers.origin;
|
|
11057
|
-
|
|
11058
|
-
|
|
11059
|
-
|
|
11060
|
-
|
|
11061
|
-
|
|
11062
|
-
|
|
11063
|
-
|
|
11064
|
-
|
|
11065
|
-
|
|
12031
|
+
if (!isConfiguredOrigin(origin, options)) return `ip:${request.ip}`;
|
|
12032
|
+
const presented = request.headers[AGENT_SERVER_TOKEN_HEADER];
|
|
12033
|
+
let authenticated = false;
|
|
12034
|
+
if (typeof presented === "string" && presented.length > 0) try {
|
|
12035
|
+
if (origin === "moltnet-agent-desktop://native") nativeGrant.verify(origin, presented);
|
|
12036
|
+
else {
|
|
12037
|
+
if (!oauth) return `unauth:${origin}:${request.ip}`;
|
|
12038
|
+
await verifyBrowser(request, presented);
|
|
12039
|
+
}
|
|
12040
|
+
authenticated = true;
|
|
12041
|
+
} catch (error) {
|
|
12042
|
+
if (error instanceof AgentServerHttpError && error.statusCode === 429) throw error;
|
|
12043
|
+
if (origin === "moltnet-agent-desktop://native" && !(error instanceof NativeGrantError)) throw error;
|
|
12044
|
+
}
|
|
12045
|
+
return authenticated ? `origin:${origin}` : `unauth:${origin}:${request.ip}`;
|
|
11066
12046
|
}
|
|
11067
12047
|
});
|
|
11068
|
-
const
|
|
12048
|
+
const requireAuthorizedOrigin = async (request) => {
|
|
12049
|
+
if (restartRequired) throw new AgentServerHttpError(409, "restart_required", "Restart the Agent Server to apply connection settings");
|
|
11069
12050
|
const origin = requireOriginHeader(request.headers);
|
|
11070
12051
|
const token = request.headers[AGENT_SERVER_TOKEN_HEADER];
|
|
11071
|
-
if (typeof token !== "string" || token.length === 0) throw new AgentServerHttpError(401, "
|
|
11072
|
-
|
|
12052
|
+
if (typeof token !== "string" || token.length === 0) throw new AgentServerHttpError(401, "authorization_required", "Local control token is required");
|
|
12053
|
+
if (origin === "moltnet-agent-desktop://native") nativeGrant.verify(origin, token);
|
|
12054
|
+
else try {
|
|
12055
|
+
if (!oauth) throw new AgentServerHttpError(503, "oauth_unavailable", "Local OAuth is not configured");
|
|
12056
|
+
const admission = browserVerification.get(request);
|
|
12057
|
+
browserVerification.delete(request);
|
|
12058
|
+
await (admission ?? oauth.verifyBrowser(token));
|
|
12059
|
+
} catch (error) {
|
|
12060
|
+
if (error instanceof AgentServerHttpError) throw error;
|
|
12061
|
+
const code = error && typeof error === "object" && "code" in error ? error.code : void 0;
|
|
12062
|
+
const rejected = error instanceof InvalidOperatorGrantError || typeof code === "string" && [
|
|
12063
|
+
"ERR_JWT_EXPIRED",
|
|
12064
|
+
"ERR_JWT_CLAIM_VALIDATION_FAILED",
|
|
12065
|
+
"ERR_JWS_SIGNATURE_VERIFICATION_FAILED",
|
|
12066
|
+
"ERR_JWS_INVALID",
|
|
12067
|
+
"ERR_JWT_INVALID",
|
|
12068
|
+
"ERR_JOSE_ALG_NOT_ALLOWED",
|
|
12069
|
+
"ERR_JWKS_NO_MATCHING_KEY"
|
|
12070
|
+
].includes(code);
|
|
12071
|
+
request.log.warn({
|
|
12072
|
+
stage: "local-control-authorization",
|
|
12073
|
+
outcome: rejected ? "rejected" : "unavailable",
|
|
12074
|
+
code: typeof code === "string" ? code : void 0
|
|
12075
|
+
}, "Local control authorization failed");
|
|
12076
|
+
if (!rejected) throw new AgentServerHttpError(503, "authorization_unavailable", "Local authorization is unavailable. Check Server settings or retry shortly.");
|
|
12077
|
+
throw new AgentServerHttpError(401, "authorization_required", "Sign in to authorize local control");
|
|
12078
|
+
}
|
|
11073
12079
|
return origin;
|
|
11074
12080
|
};
|
|
11075
12081
|
app.after(() => {
|
|
11076
|
-
app.addHook("onRequest", app.rateLimit());
|
|
11077
12082
|
app.get("/health", { schema: AgentServerRouteSchemas.health }, async () => ({ status: "ok" }));
|
|
11078
|
-
|
|
11079
|
-
|
|
11080
|
-
|
|
11081
|
-
|
|
11082
|
-
|
|
11083
|
-
|
|
12083
|
+
app.get("/v1/native/connection-settings", { schema: { hide: true } }, async (request) => {
|
|
12084
|
+
if (await requireAuthorizedOrigin(request) !== "moltnet-agent-desktop://native" || !options.connectionSettings) throw new AgentServerHttpError(403, "native_required", "Native administration required");
|
|
12085
|
+
return options.connectionSettings.view();
|
|
12086
|
+
});
|
|
12087
|
+
app.post("/v1/native/connection-settings", { schema: { hide: true } }, async (request) => {
|
|
12088
|
+
if (await requireAuthorizedOrigin(request) !== "moltnet-agent-desktop://native" || !options.connectionSettings) throw new AgentServerHttpError(403, "native_required", "Native administration required");
|
|
12089
|
+
try {
|
|
12090
|
+
const settings = options.runs.prepareServerRestart(() => options.connectionSettings.save(request.body));
|
|
12091
|
+
oauth?.cancel();
|
|
12092
|
+
restartRequired = true;
|
|
12093
|
+
return settings;
|
|
12094
|
+
} catch (error) {
|
|
12095
|
+
throw new AgentServerHttpError(400, "invalid_connection_settings", error instanceof Error ? error.message : "Invalid connection settings");
|
|
12096
|
+
}
|
|
12097
|
+
});
|
|
12098
|
+
app.get("/oauth/metadata", { schema: {
|
|
12099
|
+
operationId: "getAgentServerOAuthMetadata",
|
|
12100
|
+
tags: ["operator"],
|
|
12101
|
+
response: { 200: {
|
|
12102
|
+
type: "object",
|
|
12103
|
+
required: [
|
|
12104
|
+
"protocolVersion",
|
|
12105
|
+
"instance",
|
|
12106
|
+
"issuer",
|
|
12107
|
+
"authorizationUrl",
|
|
12108
|
+
"tokenUrl",
|
|
12109
|
+
"clientId",
|
|
12110
|
+
"operatorConfigured"
|
|
12111
|
+
],
|
|
12112
|
+
properties: {
|
|
12113
|
+
protocolVersion: {
|
|
12114
|
+
type: "integer",
|
|
12115
|
+
const: OPERATOR_OAUTH.protocolVersion
|
|
12116
|
+
},
|
|
12117
|
+
instance: {
|
|
12118
|
+
type: "string",
|
|
12119
|
+
format: "uuid"
|
|
12120
|
+
},
|
|
12121
|
+
issuer: { type: "string" },
|
|
12122
|
+
authorizationUrl: { type: "string" },
|
|
12123
|
+
tokenUrl: { type: "string" },
|
|
12124
|
+
clientId: { type: "string" },
|
|
12125
|
+
operatorConfigured: { type: "boolean" }
|
|
12126
|
+
}
|
|
12127
|
+
} }
|
|
12128
|
+
} }, async () => {
|
|
12129
|
+
if (!oauth) throw new AgentServerHttpError(503, "oauth_unavailable", "Local OAuth is not configured");
|
|
12130
|
+
return oauth.metadata();
|
|
12131
|
+
});
|
|
12132
|
+
app.post("/v1/operator/sign-in", { schema: {
|
|
12133
|
+
operationId: "signInAgentServerOperator",
|
|
12134
|
+
response: { 200: {
|
|
12135
|
+
type: "object",
|
|
12136
|
+
properties: { state: { type: "string" } },
|
|
12137
|
+
required: ["state"]
|
|
12138
|
+
} }
|
|
12139
|
+
} }, async (request) => {
|
|
12140
|
+
if (await requireAuthorizedOrigin(request) !== "moltnet-agent-desktop://native" || !oauth) throw new AgentServerHttpError(403, "native_required", "Native administration required");
|
|
12141
|
+
await oauth.authorize(void 0, requestOperationSignal(request, options.shutdownSignal));
|
|
12142
|
+
return { state: "authorized" };
|
|
12143
|
+
});
|
|
12144
|
+
app.post("/v1/operator/cancel", { schema: {
|
|
12145
|
+
operationId: "cancelAgentServerOperatorApproval",
|
|
12146
|
+
tags: ["operator"],
|
|
12147
|
+
security: [{ agentServerToken: [] }],
|
|
12148
|
+
response: { 200: {
|
|
12149
|
+
type: "object",
|
|
12150
|
+
properties: { state: {
|
|
12151
|
+
type: "string",
|
|
12152
|
+
const: "cancelled"
|
|
12153
|
+
} },
|
|
12154
|
+
required: ["state"]
|
|
12155
|
+
} }
|
|
12156
|
+
} }, async (request) => {
|
|
12157
|
+
if (await requireAuthorizedOrigin(request) !== "moltnet-agent-desktop://native" || !oauth) throw new AgentServerHttpError(403, "native_required", "Native administration required");
|
|
12158
|
+
oauth.cancel();
|
|
12159
|
+
return { state: "cancelled" };
|
|
12160
|
+
});
|
|
12161
|
+
app.delete("/v1/operator", { schema: {
|
|
12162
|
+
operationId: "removeAgentServerOperator",
|
|
12163
|
+
tags: ["operator"],
|
|
12164
|
+
security: [{ agentServerToken: [] }],
|
|
12165
|
+
response: { 200: {
|
|
12166
|
+
type: "object",
|
|
12167
|
+
properties: { state: {
|
|
12168
|
+
type: "string",
|
|
12169
|
+
const: "removed"
|
|
12170
|
+
} },
|
|
12171
|
+
required: ["state"]
|
|
12172
|
+
} }
|
|
12173
|
+
} }, async (request) => {
|
|
12174
|
+
if (await requireAuthorizedOrigin(request) !== "moltnet-agent-desktop://native" || !oauth) throw new AgentServerHttpError(403, "native_required", "Native administration required");
|
|
12175
|
+
oauth.removeOperator();
|
|
12176
|
+
return { state: "removed" };
|
|
12177
|
+
});
|
|
12178
|
+
registerStatusRoute(app, options, requireAuthorizedOrigin);
|
|
12179
|
+
registerAgentRoutes(app, options, requireAuthorizedOrigin);
|
|
12180
|
+
registerProviderRoutes(app, options, requireAuthorizedOrigin);
|
|
12181
|
+
registerSubscriptionRoutes(app, options, requireAuthorizedOrigin);
|
|
12182
|
+
registerRunRoutes(app, options, requireAuthorizedOrigin);
|
|
12183
|
+
registerCatalogueRoute(app, options, requireAuthorizedOrigin);
|
|
12184
|
+
});
|
|
12185
|
+
app.addHook("preClose", async () => {
|
|
12186
|
+
options.operatorOAuth?.cancel();
|
|
11084
12187
|
});
|
|
11085
12188
|
app.addHook("onClose", () => {
|
|
11086
12189
|
options.subscriptions.close();
|
|
@@ -11104,41 +12207,65 @@ function buildAgentServer(options) {
|
|
|
11104
12207
|
});
|
|
11105
12208
|
return app;
|
|
11106
12209
|
}
|
|
11107
|
-
function
|
|
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);
|
|
12210
|
+
function registerCatalogueRoute(app, options, requireAuthorizedOrigin) {
|
|
12211
|
+
app.get("/v1/catalogue", {
|
|
12212
|
+
schema: AgentServerRouteSchemas.catalogue,
|
|
12213
|
+
attachValidation: true
|
|
12214
|
+
}, async (request) => {
|
|
12215
|
+
await requireAuthorizedOrigin(request);
|
|
12216
|
+
const { identity } = request.query ?? {};
|
|
12217
|
+
if (!identity || identity.trim().length === 0) throw new AgentServerHttpError(400, "invalid_query", "\"identity\" is required");
|
|
12218
|
+
const alias = identity.trim();
|
|
12219
|
+
requireActivation(options.store, alias);
|
|
12220
|
+
return buildCatalogue({
|
|
12221
|
+
agent: await (options.catalogueAgentFor ? options.catalogueAgentFor(alias) : defaultCatalogueAgent(options, alias)),
|
|
12222
|
+
machine: machineCapabilities(options),
|
|
12223
|
+
identityDefault: readIdentityDefaultBinding(options.store.identityDir(alias))
|
|
12224
|
+
});
|
|
11136
12225
|
});
|
|
11137
12226
|
}
|
|
11138
|
-
|
|
12227
|
+
/** Resolve and verify each indexed team independently with its exact key. */
|
|
12228
|
+
async function defaultCatalogueAgent(options, alias) {
|
|
12229
|
+
const { config } = await loadAgentActivation(options.store, alias);
|
|
12230
|
+
return {
|
|
12231
|
+
teamIds: Object.keys(config.agent_key_refs ?? {}),
|
|
12232
|
+
lastVerified: (teamId) => requireActivation(options.store, alias).credentialHealth?.[teamId],
|
|
12233
|
+
readTeam: async (teamId) => {
|
|
12234
|
+
const { client, metadata } = requireCredentialSnapshot(await verifyTeamActivation(options.store, alias, options.secretProviders, options.externalSecretProviders, void 0, options.shutdownSignal, teamId));
|
|
12235
|
+
const [team, diaries, profiles] = await Promise.all([
|
|
12236
|
+
client.teams.get(teamId),
|
|
12237
|
+
client.diaries.list(),
|
|
12238
|
+
client.runtimeProfiles.list({ teamId })
|
|
12239
|
+
]);
|
|
12240
|
+
return {
|
|
12241
|
+
team,
|
|
12242
|
+
diaries: diaries.items,
|
|
12243
|
+
profiles: profiles.items,
|
|
12244
|
+
credential: metadata
|
|
12245
|
+
};
|
|
12246
|
+
}
|
|
12247
|
+
};
|
|
12248
|
+
}
|
|
12249
|
+
/** What this machine can execute right now: provider keys and runtime kinds. */
|
|
12250
|
+
function machineCapabilities(options) {
|
|
12251
|
+
const providerEnv = /* @__PURE__ */ new Map();
|
|
12252
|
+
for (const provider of Object.values(options.providers.list())) {
|
|
12253
|
+
const configured = providerEnv.get(provider.envName) === true;
|
|
12254
|
+
providerEnv.set(provider.envName, configured || provider.hasApiKey);
|
|
12255
|
+
}
|
|
12256
|
+
const runtimeKinds = new Set([BUILT_IN_RUNTIME_KIND]);
|
|
12257
|
+
for (const entry of options.runtimeRegistry?.list() ?? []) try {
|
|
12258
|
+
if (options.runtimeRegistry?.resolve(entry.kind, { forDisplay: true })) runtimeKinds.add(entry.kind);
|
|
12259
|
+
} catch {}
|
|
12260
|
+
return {
|
|
12261
|
+
providerEnv,
|
|
12262
|
+
runtimeKinds
|
|
12263
|
+
};
|
|
12264
|
+
}
|
|
12265
|
+
function registerStatusRoute(app, options, requireAuthorizedOrigin) {
|
|
11139
12266
|
const { store, runs } = options;
|
|
11140
12267
|
app.get("/v1/status", { schema: AgentServerRouteSchemas.status }, async (request) => {
|
|
11141
|
-
|
|
12268
|
+
await requireAuthorizedOrigin(request);
|
|
11142
12269
|
const selected = selectedIdentity(store, options.activeIdentity);
|
|
11143
12270
|
return {
|
|
11144
12271
|
version: options.version,
|
|
@@ -11148,22 +12275,22 @@ function registerStatusRoute(app, options, requirePairedOrigin) {
|
|
|
11148
12275
|
identities: identityViews(store),
|
|
11149
12276
|
...selected ? { selectedIdentity: selected } : {},
|
|
11150
12277
|
providers: options.providers.list(),
|
|
11151
|
-
runs: runViews(runs),
|
|
12278
|
+
runs: await runViews(runs),
|
|
11152
12279
|
runtimeSettings: options.runtimeSettings ?? DEFAULT_LOCAL_OPERATIONAL_SETTINGS
|
|
11153
12280
|
};
|
|
11154
12281
|
});
|
|
11155
12282
|
}
|
|
11156
|
-
function registerAgentRoutes(app, options,
|
|
12283
|
+
function registerAgentRoutes(app, options, requireAuthorizedOrigin) {
|
|
11157
12284
|
const { store } = options;
|
|
11158
12285
|
app.get("/v1/agents", { schema: AgentServerRouteSchemas.listAgents }, async (request) => {
|
|
11159
|
-
|
|
12286
|
+
await requireAuthorizedOrigin(request);
|
|
11160
12287
|
return store.listActivations().map((activation) => publicAgentView(store, activation));
|
|
11161
12288
|
});
|
|
11162
12289
|
app.post("/v1/agents", {
|
|
11163
12290
|
schema: AgentServerRouteSchemas.createAgent,
|
|
11164
12291
|
attachValidation: true
|
|
11165
12292
|
}, async (request, reply) => {
|
|
11166
|
-
|
|
12293
|
+
await requireAuthorizedOrigin(request);
|
|
11167
12294
|
const body = requireBody(request);
|
|
11168
12295
|
const signal = requestOperationSignal(request, options.shutdownSignal);
|
|
11169
12296
|
const kind = requireString(body, "kind");
|
|
@@ -11189,11 +12316,26 @@ function registerAgentRoutes(app, options, requirePairedOrigin) {
|
|
|
11189
12316
|
}
|
|
11190
12317
|
throw new AgentServerHttpError(400, "invalid_body", "\"kind\" must be \"managed\" or \"external\"");
|
|
11191
12318
|
});
|
|
12319
|
+
app.post("/v1/agents/:agentName/teams", { schema: AgentServerRouteSchemas.enrollTeam }, async (request) => {
|
|
12320
|
+
await requireAuthorizedOrigin(request);
|
|
12321
|
+
const { agentName } = request.params;
|
|
12322
|
+
if (request.headers.origin !== "moltnet-agent-desktop://native" || !options.operatorOAuth || !options.operatorApiUrl) throw new AgentServerHttpError(403, "native_required", "Native OAuth enrollment required");
|
|
12323
|
+
return enrollIdentityTeam({
|
|
12324
|
+
oauth: options.operatorOAuth,
|
|
12325
|
+
apiUrl: options.operatorApiUrl,
|
|
12326
|
+
store,
|
|
12327
|
+
alias: agentName,
|
|
12328
|
+
managed: options.secretProviders,
|
|
12329
|
+
external: options.externalSecretProviders,
|
|
12330
|
+
input: request.body,
|
|
12331
|
+
signal: requestOperationSignal(request, options.shutdownSignal)
|
|
12332
|
+
});
|
|
12333
|
+
});
|
|
11192
12334
|
app.post("/v1/agents/:agentName/reconcile", {
|
|
11193
12335
|
schema: AgentServerRouteSchemas.reconcileAgent,
|
|
11194
12336
|
attachValidation: true
|
|
11195
12337
|
}, async (request) => {
|
|
11196
|
-
|
|
12338
|
+
await requireAuthorizedOrigin(request);
|
|
11197
12339
|
const { agentName } = request.params;
|
|
11198
12340
|
const action = requireString(requireBody(request), "action");
|
|
11199
12341
|
if (action !== "resume" && action !== "abandon") throw new AgentServerHttpError(400, "invalid_body", "\"action\" must be \"resume\" or \"abandon\"");
|
|
@@ -11217,13 +12359,13 @@ function identityViews(store) {
|
|
|
11217
12359
|
hasAgentKey: hasAgentKeyConfiguration(store.readAgentConfig(alias) ?? {})
|
|
11218
12360
|
}));
|
|
11219
12361
|
}
|
|
11220
|
-
function registerProviderRoutes(app, options,
|
|
12362
|
+
function registerProviderRoutes(app, options, requireAuthorizedOrigin) {
|
|
11221
12363
|
app.get("/v1/providers", { schema: AgentServerRouteSchemas.listProviders }, async (request) => {
|
|
11222
|
-
|
|
12364
|
+
await requireAuthorizedOrigin(request);
|
|
11223
12365
|
return options.providers.list();
|
|
11224
12366
|
});
|
|
11225
12367
|
app.post("/v1/providers/:providerId/discover-models", { schema: AgentServerRouteSchemas.discoverModels }, async (request) => {
|
|
11226
|
-
|
|
12368
|
+
await requireAuthorizedOrigin(request);
|
|
11227
12369
|
const { providerId } = request.params;
|
|
11228
12370
|
return options.providers.discover(providerId, { signal: requestOperationSignal(request, options.shutdownSignal) });
|
|
11229
12371
|
});
|
|
@@ -11231,7 +12373,7 @@ function registerProviderRoutes(app, options, requirePairedOrigin) {
|
|
|
11231
12373
|
schema: AgentServerRouteSchemas.putProvider,
|
|
11232
12374
|
attachValidation: true
|
|
11233
12375
|
}, async (request, reply) => {
|
|
11234
|
-
|
|
12376
|
+
await requireAuthorizedOrigin(request);
|
|
11235
12377
|
const { providerId } = request.params;
|
|
11236
12378
|
const body = requireBody(request);
|
|
11237
12379
|
const entry = await options.providers.set(providerId, {
|
|
@@ -11247,7 +12389,7 @@ function registerProviderRoutes(app, options, requirePairedOrigin) {
|
|
|
11247
12389
|
schema: AgentServerRouteSchemas.deleteProvider,
|
|
11248
12390
|
attachValidation: true
|
|
11249
12391
|
}, async (request, reply) => {
|
|
11250
|
-
|
|
12392
|
+
await requireAuthorizedOrigin(request);
|
|
11251
12393
|
const { providerId } = request.params;
|
|
11252
12394
|
try {
|
|
11253
12395
|
await options.providers.remove(providerId);
|
|
@@ -11258,45 +12400,45 @@ function registerProviderRoutes(app, options, requirePairedOrigin) {
|
|
|
11258
12400
|
return reply.code(204).send(null);
|
|
11259
12401
|
});
|
|
11260
12402
|
}
|
|
11261
|
-
function runViews(runs) {
|
|
11262
|
-
return runs.
|
|
12403
|
+
async function runViews(runs) {
|
|
12404
|
+
return (await runs.listAsync(RUN_HISTORY_LIMIT)).map((record) => ({
|
|
11263
12405
|
...record,
|
|
11264
12406
|
active: runs.isActive(record.id)
|
|
11265
12407
|
}));
|
|
11266
12408
|
}
|
|
11267
|
-
function registerSubscriptionRoutes(app, options,
|
|
12409
|
+
function registerSubscriptionRoutes(app, options, requireAuthorizedOrigin) {
|
|
11268
12410
|
app.get("/v1/subscriptions", { schema: AgentServerRouteSchemas.listSubscriptions }, async (request) => {
|
|
11269
|
-
|
|
12411
|
+
await requireAuthorizedOrigin(request);
|
|
11270
12412
|
return options.subscriptions.list();
|
|
11271
12413
|
});
|
|
11272
12414
|
app.post("/v1/subscriptions/:providerId/login", { schema: AgentServerRouteSchemas.startSubscriptionLogin }, async (request, reply) => {
|
|
11273
|
-
|
|
12415
|
+
await requireAuthorizedOrigin(request);
|
|
11274
12416
|
const { providerId } = request.params;
|
|
11275
12417
|
const login = await options.subscriptions.start(providerId);
|
|
11276
12418
|
return reply.code(201).send(login);
|
|
11277
12419
|
});
|
|
11278
12420
|
app.get("/v1/subscriptions/:providerId/login", { schema: AgentServerRouteSchemas.getSubscriptionLogin }, async (request) => {
|
|
11279
|
-
|
|
12421
|
+
await requireAuthorizedOrigin(request);
|
|
11280
12422
|
const { providerId } = request.params;
|
|
11281
12423
|
return options.subscriptions.status(providerId);
|
|
11282
12424
|
});
|
|
11283
12425
|
app.delete("/v1/subscriptions/:providerId/login", { schema: AgentServerRouteSchemas.cancelSubscriptionLogin }, async (request) => {
|
|
11284
|
-
|
|
12426
|
+
await requireAuthorizedOrigin(request);
|
|
11285
12427
|
const { providerId } = request.params;
|
|
11286
12428
|
return options.subscriptions.cancel(providerId);
|
|
11287
12429
|
});
|
|
11288
12430
|
}
|
|
11289
|
-
function registerRunRoutes(app, options,
|
|
12431
|
+
function registerRunRoutes(app, options, requireAuthorizedOrigin) {
|
|
11290
12432
|
const { runs } = options;
|
|
11291
12433
|
app.get("/v1/runs", { schema: AgentServerRouteSchemas.listRuns }, async (request) => {
|
|
11292
|
-
|
|
12434
|
+
await requireAuthorizedOrigin(request);
|
|
11293
12435
|
return runViews(runs);
|
|
11294
12436
|
});
|
|
11295
12437
|
app.post("/v1/runs", {
|
|
11296
12438
|
schema: AgentServerRouteSchemas.startRun,
|
|
11297
12439
|
attachValidation: true
|
|
11298
12440
|
}, async (request, reply) => {
|
|
11299
|
-
|
|
12441
|
+
await requireAuthorizedOrigin(request);
|
|
11300
12442
|
const body = requireBody(request);
|
|
11301
12443
|
const diaryId = optionalString(body, "diaryId");
|
|
11302
12444
|
const record = await runs.start({
|
|
@@ -11313,17 +12455,57 @@ function registerRunRoutes(app, options, requirePairedOrigin) {
|
|
|
11313
12455
|
});
|
|
11314
12456
|
});
|
|
11315
12457
|
app.delete("/v1/runs/:runId", { schema: AgentServerRouteSchemas.stopRun }, async (request) => {
|
|
11316
|
-
|
|
12458
|
+
await requireAuthorizedOrigin(request);
|
|
11317
12459
|
const { runId } = request.params;
|
|
11318
12460
|
return runs.stop(runId);
|
|
11319
12461
|
});
|
|
11320
|
-
registerRunLogRoute(app, options,
|
|
12462
|
+
registerRunLogRoute(app, options, requireAuthorizedOrigin);
|
|
11321
12463
|
}
|
|
11322
|
-
function registerRunLogRoute(app, options,
|
|
12464
|
+
function registerRunLogRoute(app, options, requireAuthorizedOrigin) {
|
|
11323
12465
|
const { runs, store } = options;
|
|
12466
|
+
app.get("/v1/runs/:runId/logs/snapshot", { schema: {
|
|
12467
|
+
operationId: "getAgentServerRunLogSnapshot",
|
|
12468
|
+
tags: ["runs"],
|
|
12469
|
+
security: [{ agentServerToken: [] }],
|
|
12470
|
+
params: {
|
|
12471
|
+
type: "object",
|
|
12472
|
+
required: ["runId"],
|
|
12473
|
+
properties: { runId: {
|
|
12474
|
+
type: "string",
|
|
12475
|
+
minLength: 1
|
|
12476
|
+
} }
|
|
12477
|
+
},
|
|
12478
|
+
response: { 200: {
|
|
12479
|
+
type: "object",
|
|
12480
|
+
required: ["lines"],
|
|
12481
|
+
properties: { lines: {
|
|
12482
|
+
type: "array",
|
|
12483
|
+
items: { type: "string" }
|
|
12484
|
+
} }
|
|
12485
|
+
} }
|
|
12486
|
+
} }, async (request) => {
|
|
12487
|
+
await requireAuthorizedOrigin(request);
|
|
12488
|
+
const { runId } = request.params;
|
|
12489
|
+
const record = runs.status(runId);
|
|
12490
|
+
const handle = await open(store.resolveRunLogPath(record.id), constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
12491
|
+
try {
|
|
12492
|
+
const state = {
|
|
12493
|
+
offset: 0,
|
|
12494
|
+
fragment: ""
|
|
12495
|
+
};
|
|
12496
|
+
const { lines, omitted } = await readAgentServerLogDelta(handle, state);
|
|
12497
|
+
return { lines: [
|
|
12498
|
+
...omitted ? ["[older log output omitted]"] : [],
|
|
12499
|
+
...lines,
|
|
12500
|
+
...state.fragment ? [state.fragment] : []
|
|
12501
|
+
] };
|
|
12502
|
+
} finally {
|
|
12503
|
+
await handle.close();
|
|
12504
|
+
}
|
|
12505
|
+
});
|
|
11324
12506
|
let openStreams = 0;
|
|
11325
12507
|
app.get("/v1/runs/:runId/logs", { schema: AgentServerRouteSchemas.streamRunLogs }, async (request, reply) => {
|
|
11326
|
-
|
|
12508
|
+
await requireAuthorizedOrigin(request);
|
|
11327
12509
|
const { runId } = request.params;
|
|
11328
12510
|
const record = runs.status(runId);
|
|
11329
12511
|
store.resolveRunLogPath(record.id);
|
|
@@ -11366,6 +12548,7 @@ function registerRunLogRoute(app, options, requirePairedOrigin) {
|
|
|
11366
12548
|
});
|
|
11367
12549
|
};
|
|
11368
12550
|
const push = async () => {
|
|
12551
|
+
if (request.headers.origin !== "moltnet-agent-desktop://native") await requireAuthorizedOrigin(request);
|
|
11369
12552
|
const handle = await open(store.resolveRunLogPath(record.id), constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
11370
12553
|
try {
|
|
11371
12554
|
const { lines, omitted } = await readAgentServerLogDelta(handle, readState);
|
|
@@ -11414,9 +12597,14 @@ function corsHeadersFor(request, options) {
|
|
|
11414
12597
|
return {};
|
|
11415
12598
|
}
|
|
11416
12599
|
function isConfiguredOrigin(origin, options) {
|
|
11417
|
-
return typeof origin === "string" && (options.allowedOrigins.includes(origin) || origin === options.selfOrigin);
|
|
12600
|
+
return typeof origin === "string" && (origin === "moltnet-agent-desktop://native" || options.allowedOrigins.includes(origin) || origin === options.selfOrigin);
|
|
11418
12601
|
}
|
|
11419
12602
|
function normalizeAgentServerError(error) {
|
|
12603
|
+
if (error instanceof TeamCredentialError) return {
|
|
12604
|
+
statusCode: 400,
|
|
12605
|
+
code: error.blocker.code,
|
|
12606
|
+
message: error.blocker.message
|
|
12607
|
+
};
|
|
11420
12608
|
if (error instanceof AgentServerHttpError) return {
|
|
11421
12609
|
statusCode: error.statusCode,
|
|
11422
12610
|
code: error.code,
|
|
@@ -11427,8 +12615,8 @@ function normalizeAgentServerError(error) {
|
|
|
11427
12615
|
code: error.kind,
|
|
11428
12616
|
message: error.message
|
|
11429
12617
|
};
|
|
11430
|
-
if (error instanceof
|
|
11431
|
-
statusCode:
|
|
12618
|
+
if (error instanceof NativeGrantError) return {
|
|
12619
|
+
statusCode: 401,
|
|
11432
12620
|
code: error.code,
|
|
11433
12621
|
message: error.message
|
|
11434
12622
|
};
|
|
@@ -11487,28 +12675,6 @@ function pemPrivateKey(key) {
|
|
|
11487
12675
|
type: "pkcs8"
|
|
11488
12676
|
}).toString());
|
|
11489
12677
|
}
|
|
11490
|
-
async function importCaKeyPair(pem) {
|
|
11491
|
-
const privateKey = createPrivateKey(pem);
|
|
11492
|
-
const privateDer = privateKey.export({
|
|
11493
|
-
format: "der",
|
|
11494
|
-
type: "pkcs8"
|
|
11495
|
-
});
|
|
11496
|
-
const publicDer = createPublicKey(privateKey).export({
|
|
11497
|
-
format: "der",
|
|
11498
|
-
type: "spki"
|
|
11499
|
-
});
|
|
11500
|
-
const [privateCryptoKey, publicCryptoKey] = await Promise.all([webcrypto.subtle.importKey("pkcs8", privateDer, {
|
|
11501
|
-
name: "ECDSA",
|
|
11502
|
-
namedCurve: "P-256"
|
|
11503
|
-
}, false, ["sign"]), webcrypto.subtle.importKey("spki", publicDer, {
|
|
11504
|
-
name: "ECDSA",
|
|
11505
|
-
namedCurve: "P-256"
|
|
11506
|
-
}, false, ["verify"])]);
|
|
11507
|
-
return {
|
|
11508
|
-
privateKey: privateCryptoKey,
|
|
11509
|
-
publicKey: publicCryptoKey
|
|
11510
|
-
};
|
|
11511
|
-
}
|
|
11512
12678
|
async function localTlsMaterialFromDirectory(dir) {
|
|
11513
12679
|
try {
|
|
11514
12680
|
const [key, cert, ca] = await Promise.all([
|
|
@@ -11517,7 +12683,8 @@ async function localTlsMaterialFromDirectory(dir) {
|
|
|
11517
12683
|
readFile(join(dir, "local-ca.pem"), "utf8")
|
|
11518
12684
|
]);
|
|
11519
12685
|
const parsed = new X509Certificate(cert);
|
|
11520
|
-
|
|
12686
|
+
const authority = new X509Certificate(ca);
|
|
12687
|
+
if (Date.parse(parsed.validTo) - Date.now() > RENEW_BEFORE_MS && Date.parse(authority.validTo) - Date.now() > RENEW_BEFORE_MS && authority.keyUsage?.includes("1.3.6.1.5.5.7.3.1") && parsed.checkIP("127.0.0.1") === "127.0.0.1" && parsed.verify(authority.publicKey) && parsed.publicKey.equals(createPublicKey(key))) return {
|
|
11521
12688
|
key,
|
|
11522
12689
|
cert,
|
|
11523
12690
|
ca,
|
|
@@ -11526,6 +12693,17 @@ async function localTlsMaterialFromDirectory(dir) {
|
|
|
11526
12693
|
} catch {}
|
|
11527
12694
|
return null;
|
|
11528
12695
|
}
|
|
12696
|
+
/** Read-only inspection for native status polling. */
|
|
12697
|
+
async function inspectLocalTlsMaterial(root) {
|
|
12698
|
+
if (await hasStoredSigningKey(join(root, "tls"))) return null;
|
|
12699
|
+
return localTlsMaterialFromDirectory(join(root, "tls"));
|
|
12700
|
+
}
|
|
12701
|
+
async function hasStoredSigningKey(dir) {
|
|
12702
|
+
return stat(join(dir, "local-ca-key.pem")).then(() => true, (error) => {
|
|
12703
|
+
if (error.code === "ENOENT") return false;
|
|
12704
|
+
throw error;
|
|
12705
|
+
});
|
|
12706
|
+
}
|
|
11529
12707
|
/** Creates a per-user CA and loopback-only leaf certificate under a 0700 directory. */
|
|
11530
12708
|
async function ensureLocalTlsMaterial(root) {
|
|
11531
12709
|
const dir = join(root, "tls");
|
|
@@ -11533,27 +12711,25 @@ async function ensureLocalTlsMaterial(root) {
|
|
|
11533
12711
|
recursive: true,
|
|
11534
12712
|
mode: 448
|
|
11535
12713
|
});
|
|
12714
|
+
const previousSigningKey = join(dir, "local-ca-key.pem");
|
|
12715
|
+
const hasPreviousSigningKey = await hasStoredSigningKey(dir);
|
|
11536
12716
|
const existing = await localTlsMaterialFromDirectory(dir);
|
|
11537
|
-
if (existing) return existing;
|
|
11538
|
-
|
|
11539
|
-
|
|
11540
|
-
|
|
11541
|
-
|
|
11542
|
-
|
|
11543
|
-
|
|
11544
|
-
|
|
11545
|
-
caKeys
|
|
11546
|
-
|
|
11547
|
-
|
|
11548
|
-
|
|
11549
|
-
|
|
11550
|
-
|
|
11551
|
-
|
|
11552
|
-
|
|
11553
|
-
extensions: [new BasicConstraintsExtension(true, void 0, true), new KeyUsagesExtension(KeyUsageFlags.keyCertSign | KeyUsageFlags.cRLSign, true)]
|
|
11554
|
-
})).toString("pem");
|
|
11555
|
-
caKey = await pemPrivateKey(caKeys.privateKey);
|
|
11556
|
-
}
|
|
12717
|
+
if (existing && !hasPreviousSigningKey) return existing;
|
|
12718
|
+
if (isMacos() && await isLocalCaTrusted(root)) await removeLocalCa(root);
|
|
12719
|
+
const caKeys = await webcrypto.subtle.generateKey({
|
|
12720
|
+
name: "ECDSA",
|
|
12721
|
+
namedCurve: "P-256"
|
|
12722
|
+
}, false, ["sign", "verify"]);
|
|
12723
|
+
const ca = (await X509CertificateGenerator.createSelfSigned({
|
|
12724
|
+
name: `CN=${CA_COMMON_NAME}`,
|
|
12725
|
+
keys: caKeys,
|
|
12726
|
+
notAfter: new Date(Date.now() + 366 * 24 * 60 * 60 * 1e3),
|
|
12727
|
+
extensions: [
|
|
12728
|
+
new BasicConstraintsExtension(true, 0, true),
|
|
12729
|
+
new KeyUsagesExtension(KeyUsageFlags.keyCertSign, true),
|
|
12730
|
+
new ExtendedKeyUsageExtension([ExtendedKeyUsage.serverAuth], true)
|
|
12731
|
+
]
|
|
12732
|
+
})).toString("pem");
|
|
11557
12733
|
const leafKeys = await webcrypto.subtle.generateKey({
|
|
11558
12734
|
name: "ECDSA",
|
|
11559
12735
|
namedCurve: "P-256"
|
|
@@ -11577,8 +12753,9 @@ async function ensureLocalTlsMaterial(root) {
|
|
|
11577
12753
|
const key = await pemPrivateKey(leafKeys.privateKey);
|
|
11578
12754
|
const cert = leafCert.toString("pem");
|
|
11579
12755
|
const writes = [writeFile(join(dir, "loopback-key.pem"), key, { mode: 384 }), writeFile(join(dir, "loopback-cert.pem"), cert, { mode: 384 })];
|
|
11580
|
-
|
|
12756
|
+
writes.push(writeFile(join(dir, "local-ca.pem"), ca, { mode: 384 }));
|
|
11581
12757
|
await Promise.all(writes);
|
|
12758
|
+
await rm(previousSigningKey, { force: true });
|
|
11582
12759
|
return {
|
|
11583
12760
|
key,
|
|
11584
12761
|
cert,
|
|
@@ -11588,20 +12765,23 @@ async function ensureLocalTlsMaterial(root) {
|
|
|
11588
12765
|
}
|
|
11589
12766
|
async function trustLocalCa(root) {
|
|
11590
12767
|
const caPath = join(root, "tls", "local-ca.pem");
|
|
11591
|
-
await execFileAsync("security", [
|
|
12768
|
+
await execFileAsync("/usr/bin/security", [
|
|
11592
12769
|
"add-trusted-cert",
|
|
11593
|
-
"-d",
|
|
11594
12770
|
"-r",
|
|
11595
12771
|
"trustRoot",
|
|
12772
|
+
"-p",
|
|
12773
|
+
"ssl",
|
|
12774
|
+
"-s",
|
|
12775
|
+
"127.0.0.1",
|
|
11596
12776
|
"-k",
|
|
11597
12777
|
loginKeychainPath(),
|
|
11598
12778
|
caPath
|
|
11599
12779
|
]);
|
|
11600
12780
|
}
|
|
11601
12781
|
async function isLocalCaTrusted(root) {
|
|
11602
|
-
const ca = await readFile(join(root, "tls", "local-ca.pem"), "utf8");
|
|
11603
12782
|
try {
|
|
11604
|
-
const
|
|
12783
|
+
const ca = await readFile(join(root, "tls", "local-ca.pem"), "utf8");
|
|
12784
|
+
const { stdout } = await execFileAsync("/usr/bin/security", [
|
|
11605
12785
|
"find-certificate",
|
|
11606
12786
|
"-a",
|
|
11607
12787
|
"-p",
|
|
@@ -11610,32 +12790,51 @@ async function isLocalCaTrusted(root) {
|
|
|
11610
12790
|
loginKeychainPath()
|
|
11611
12791
|
]);
|
|
11612
12792
|
return stdout.includes(ca.trim());
|
|
11613
|
-
} catch {
|
|
11614
|
-
return false;
|
|
12793
|
+
} catch (error) {
|
|
12794
|
+
if (error && typeof error === "object" && "code" in error && error.code === "ENOENT") return false;
|
|
12795
|
+
const stderr = error && typeof error === "object" && "stderr" in error ? error.stderr : void 0;
|
|
12796
|
+
if (typeof stderr === "string" && stderr.trim() === "security: SecKeychainSearchCopyNext: The specified item could not be found in the keychain.") return false;
|
|
12797
|
+
throw error;
|
|
11615
12798
|
}
|
|
11616
12799
|
}
|
|
11617
12800
|
async function removeLocalCa(root) {
|
|
11618
|
-
await
|
|
12801
|
+
const fingerprint = new X509Certificate(await readFile(join(root, "tls", "local-ca.pem"), "utf8")).fingerprint256.replaceAll(":", "");
|
|
12802
|
+
const caPath = join(root, "tls", "local-ca.pem");
|
|
12803
|
+
await removeTrustSettings([
|
|
12804
|
+
"remove-trusted-cert",
|
|
12805
|
+
"-d",
|
|
12806
|
+
caPath
|
|
12807
|
+
]);
|
|
12808
|
+
await removeTrustSettings(["remove-trusted-cert", caPath]);
|
|
12809
|
+
await execFileAsync("/usr/bin/security", [
|
|
11619
12810
|
"delete-certificate",
|
|
11620
12811
|
"-Z",
|
|
11621
|
-
|
|
12812
|
+
fingerprint,
|
|
11622
12813
|
loginKeychainPath()
|
|
11623
12814
|
]);
|
|
11624
12815
|
}
|
|
11625
12816
|
function isMacos() {
|
|
11626
12817
|
return process.platform === "darwin";
|
|
11627
12818
|
}
|
|
12819
|
+
async function removeTrustSettings(args) {
|
|
12820
|
+
try {
|
|
12821
|
+
await execFileAsync("/usr/bin/security", args, { env: { LC_ALL: "C" } });
|
|
12822
|
+
} catch (error) {
|
|
12823
|
+
const stderr = error && typeof error === "object" && "stderr" in error ? error.stderr : void 0;
|
|
12824
|
+
if (typeof stderr === "string" && ["SecTrustSettingsRemoveTrustSettings: No Trust Settings were found.", "SecTrustSettingsRemoveTrustSettings: The specified item could not be found in the keychain."].includes(stderr.trim())) return;
|
|
12825
|
+
throw error;
|
|
12826
|
+
}
|
|
12827
|
+
}
|
|
11628
12828
|
//#endregion
|
|
11629
12829
|
//#region src/cli/server.ts
|
|
11630
12830
|
/**
|
|
11631
12831
|
* `moltnet-agent server` — per-user loopback supervisor (#2061).
|
|
11632
12832
|
*
|
|
11633
|
-
* Starts nothing on its own: it binds 127.0.0.1 and waits for
|
|
12833
|
+
* Starts nothing on its own: it binds 127.0.0.1 and waits for an authorized
|
|
11634
12834
|
* Console origin to configure agents/providers and start/stop runs.
|
|
11635
12835
|
*/
|
|
11636
|
-
var DEFAULT_PORT =
|
|
12836
|
+
var DEFAULT_PORT = OPERATOR_OAUTH.serverPort;
|
|
11637
12837
|
var DEFAULT_ALLOWED_ORIGINS = "https://console.themolt.net";
|
|
11638
|
-
var DEFAULT_API_URL = "https://api.themolt.net";
|
|
11639
12838
|
var SHUTDOWN_TIMEOUT_MS = 15e3;
|
|
11640
12839
|
async function runAgentServer(argv) {
|
|
11641
12840
|
if (isHelpFlag(argv)) {
|
|
@@ -11664,8 +12863,14 @@ async function runAgentServer(argv) {
|
|
|
11664
12863
|
return 1;
|
|
11665
12864
|
}
|
|
11666
12865
|
const allowedOrigins = parseAllowedOrigins(values["allowed-origins"] ?? (envConfig.allowedOrigins || DEFAULT_ALLOWED_ORIGINS));
|
|
11667
|
-
const
|
|
11668
|
-
const
|
|
12866
|
+
const settingsRoot = values.root ?? resolveAgentServerRoot({ root: envConfig.root });
|
|
12867
|
+
const connectionSettings = new ConnectionSettingsStore(settingsRoot, {
|
|
12868
|
+
...envConfig.operatorOAuth,
|
|
12869
|
+
...values["api-url"] || envConfig.apiUrl ? { apiUrl: values["api-url"] || envConfig.apiUrl } : {}
|
|
12870
|
+
});
|
|
12871
|
+
const connection = connectionSettings.view().effective;
|
|
12872
|
+
const root = connectionSettings.stateRoot(connection);
|
|
12873
|
+
const defaultApiUrl = connection.apiUrl;
|
|
11669
12874
|
const runtimeSettings = parseLocalOperationalSettings(values);
|
|
11670
12875
|
const store = new AgentServerStore(root).ensure();
|
|
11671
12876
|
const { logger, shutdown: shutdownLogger } = createRootLogger({
|
|
@@ -11681,7 +12886,15 @@ async function runAgentServer(argv) {
|
|
|
11681
12886
|
});
|
|
11682
12887
|
const secretProviders = createNodeSecretProviderRegistry().register(secrets);
|
|
11683
12888
|
const externalSecretProviders = createNodeSecretProviderRegistry();
|
|
11684
|
-
const
|
|
12889
|
+
const nativeGrant = new NativeGrantService();
|
|
12890
|
+
const nativeClient = applyNativeClientGrant({
|
|
12891
|
+
nativeGrant,
|
|
12892
|
+
env: processEnvSnapshot()
|
|
12893
|
+
});
|
|
12894
|
+
if (Boolean(values.supervised) && !nativeClient) {
|
|
12895
|
+
console.error(`A supervised Agent Server requires ${NATIVE_TOKEN_ENV}. Start it from MoltNet Agent, or omit --supervised to run it to reconnect Console to an operator already established by Desktop.`);
|
|
12896
|
+
return 1;
|
|
12897
|
+
}
|
|
11685
12898
|
const shutdownController = new AbortController();
|
|
11686
12899
|
const subscriptions = await ProviderLoginService.create({
|
|
11687
12900
|
authPath: store.piAuthJsonPath,
|
|
@@ -11693,27 +12906,41 @@ async function runAgentServer(argv) {
|
|
|
11693
12906
|
secretProviders,
|
|
11694
12907
|
logger
|
|
11695
12908
|
});
|
|
12909
|
+
const runtimeRegistry = new RuntimeRegistry(store.root);
|
|
11696
12910
|
const runs = new RunManager({
|
|
11697
12911
|
store,
|
|
11698
12912
|
secretProviders,
|
|
11699
12913
|
externalSecretProviders,
|
|
11700
12914
|
baseEnv: processEnvSnapshot(),
|
|
11701
12915
|
logger,
|
|
11702
|
-
runtimeRegistry
|
|
12916
|
+
runtimeRegistry,
|
|
11703
12917
|
runtimeSettings
|
|
11704
12918
|
});
|
|
11705
|
-
const tls = isMacos() ? await ensureTrustedLocalTls(
|
|
12919
|
+
const tls = isMacos() ? await ensureTrustedLocalTls(settingsRoot) : void 0;
|
|
12920
|
+
const selfOrigin = `${tls ? "https" : "http"}://127.0.0.1:${port}`;
|
|
11706
12921
|
const app = buildAgentServer({
|
|
12922
|
+
operatorOAuth: new OperatorOAuth({
|
|
12923
|
+
issuer: connection.issuer,
|
|
12924
|
+
authorizationUrl: new URL("/oauth2/auth", connection.publicUrl).href,
|
|
12925
|
+
tokenUrl: new URL("/oauth2/token", connection.publicUrl).href,
|
|
12926
|
+
jwksUrl: new URL("/.well-known/jwks.json", connection.publicUrl).href,
|
|
12927
|
+
nativeClientId: connection.nativeClientId,
|
|
12928
|
+
consoleClientId: connection.consoleClientId,
|
|
12929
|
+
callbackPort: OPERATOR_OAUTH.callbackPort
|
|
12930
|
+
}, root),
|
|
12931
|
+
connectionSettings,
|
|
12932
|
+
operatorApiUrl: connection.apiUrl,
|
|
11707
12933
|
store,
|
|
11708
12934
|
secrets,
|
|
11709
12935
|
secretProviders,
|
|
11710
12936
|
externalSecretProviders,
|
|
11711
|
-
|
|
12937
|
+
nativeGrant,
|
|
11712
12938
|
runs,
|
|
11713
12939
|
subscriptions,
|
|
11714
12940
|
providers,
|
|
12941
|
+
runtimeRegistry,
|
|
11715
12942
|
allowedOrigins,
|
|
11716
|
-
selfOrigin
|
|
12943
|
+
selfOrigin,
|
|
11717
12944
|
...tls ? { tls: {
|
|
11718
12945
|
key: tls.key,
|
|
11719
12946
|
cert: tls.cert
|
|
@@ -11733,7 +12960,8 @@ async function runAgentServer(argv) {
|
|
|
11733
12960
|
console.error(`moltnet-agent server listening on ${address}`);
|
|
11734
12961
|
console.error(`config root: ${root}`);
|
|
11735
12962
|
console.error(`allowed origins: ${allowedOrigins.join(", ")}`);
|
|
11736
|
-
console.error("
|
|
12963
|
+
if (nativeClient) console.error("native desktop client: authorized");
|
|
12964
|
+
console.error("Sign in through Desktop, then connect from the Console \"Local runtime\" page.");
|
|
11737
12965
|
return await waitForAgentServerShutdown(runs, app, shutdownController, Boolean(values.supervised));
|
|
11738
12966
|
} catch (cause) {
|
|
11739
12967
|
await app.close().catch(() => void 0);
|
|
@@ -11788,15 +13016,15 @@ async function runTrustCommand(argv, defaultRoot) {
|
|
|
11788
13016
|
console.error("Local HTTPS trust setup is currently supported on macOS only.");
|
|
11789
13017
|
return 1;
|
|
11790
13018
|
}
|
|
11791
|
-
const material = await ensureLocalTlsMaterial(root);
|
|
11792
13019
|
if (statusRequested) {
|
|
11793
|
-
const
|
|
13020
|
+
const material = await inspectLocalTlsMaterial(root);
|
|
13021
|
+
const trusted = material !== null && await isLocalCaTrusted(root);
|
|
11794
13022
|
if (json) printTrustStatus({
|
|
11795
13023
|
supported: true,
|
|
11796
13024
|
trusted,
|
|
11797
|
-
fingerprint: material
|
|
13025
|
+
fingerprint: material?.fingerprint ?? null
|
|
11798
13026
|
});
|
|
11799
|
-
else console.log(trusted ? `MoltNet local CA ${material
|
|
13027
|
+
else console.log(trusted ? `MoltNet local CA ${material?.fingerprint ?? "(not prepared)"} is trusted.` : `MoltNet local CA ${material?.fingerprint ?? "(not prepared)"} is not trusted.`);
|
|
11800
13028
|
return 0;
|
|
11801
13029
|
}
|
|
11802
13030
|
if (json && !yes) {
|
|
@@ -11808,11 +13036,12 @@ async function runTrustCommand(argv, defaultRoot) {
|
|
|
11808
13036
|
if (json) printTrustStatus({
|
|
11809
13037
|
supported: true,
|
|
11810
13038
|
trusted: false,
|
|
11811
|
-
fingerprint:
|
|
13039
|
+
fingerprint: null
|
|
11812
13040
|
});
|
|
11813
13041
|
else console.log("Removed the MoltNet local CA from your login keychain.");
|
|
11814
13042
|
return 0;
|
|
11815
13043
|
}
|
|
13044
|
+
const material = await ensureLocalTlsMaterial(root);
|
|
11816
13045
|
if (yes) await trustLocalCa(root);
|
|
11817
13046
|
else await ensureTrustedLocalTls(root);
|
|
11818
13047
|
if (json) printTrustStatus({
|
|
@@ -12021,6 +13250,7 @@ async function runSyncSessions(argv) {
|
|
|
12021
13250
|
args: argv,
|
|
12022
13251
|
options: {
|
|
12023
13252
|
...identityOptionDefs(),
|
|
13253
|
+
...projectRunOptionDefs(),
|
|
12024
13254
|
team: { type: "string" },
|
|
12025
13255
|
"runtime-profile-id": { type: "string" },
|
|
12026
13256
|
state: { type: "string" },
|
|
@@ -12028,11 +13258,6 @@ async function runSyncSessions(argv) {
|
|
|
12028
13258
|
"dry-run": { type: "boolean" }
|
|
12029
13259
|
}
|
|
12030
13260
|
});
|
|
12031
|
-
if (!values.team) {
|
|
12032
|
-
console.error("Missing required flag: --team\n");
|
|
12033
|
-
console.error(SYNC_SESSIONS_HELP);
|
|
12034
|
-
return 1;
|
|
12035
|
-
}
|
|
12036
13261
|
let identity;
|
|
12037
13262
|
try {
|
|
12038
13263
|
identity = parseIdentityProcessOptions(values);
|
|
@@ -12049,18 +13274,38 @@ async function runSyncSessions(argv) {
|
|
|
12049
13274
|
const agentRootDir = resolve(process.cwd(), values["agent-root"] ?? process.cwd());
|
|
12050
13275
|
const explicitAgentRootDir = values["agent-root"] ? resolve(process.cwd(), values["agent-root"]) : void 0;
|
|
12051
13276
|
const cfg = loadConfig();
|
|
13277
|
+
let selection;
|
|
13278
|
+
try {
|
|
13279
|
+
const apiUrl = await resolveSelectionApiUrl(identity.agent, {
|
|
13280
|
+
agentRootDir: explicitAgentRootDir,
|
|
13281
|
+
credentialSource: cfg.credentialSource,
|
|
13282
|
+
envApiUrl: cfg.apiUrl
|
|
13283
|
+
});
|
|
13284
|
+
selection = await resolveRunProjectSelection({
|
|
13285
|
+
...values,
|
|
13286
|
+
agent: identity.agent,
|
|
13287
|
+
cwd: process.cwd(),
|
|
13288
|
+
apiUrl
|
|
13289
|
+
});
|
|
13290
|
+
if (!selection.teamId) throw new Error("Select --team or a binding with a team");
|
|
13291
|
+
} catch (error) {
|
|
13292
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
13293
|
+
console.error(SYNC_SESSIONS_HELP);
|
|
13294
|
+
return 1;
|
|
13295
|
+
}
|
|
12052
13296
|
const ctx = await resolveAgentContext(identity.agent, {
|
|
12053
13297
|
credentialSource: cfg.credentialSource,
|
|
12054
13298
|
envApiUrl: cfg.apiUrl,
|
|
12055
|
-
|
|
13299
|
+
projectApiUrl: selection.binding?.apiUrl,
|
|
13300
|
+
teamId: selection.teamId,
|
|
12056
13301
|
agentRootDir: explicitAgentRootDir
|
|
12057
13302
|
});
|
|
12058
13303
|
await validateStartupBinding({
|
|
12059
13304
|
agent: ctx.agent,
|
|
12060
|
-
teamId:
|
|
13305
|
+
teamId: selection.teamId,
|
|
12061
13306
|
credentialTeamId: ctx.credentialTeamId
|
|
12062
13307
|
});
|
|
12063
|
-
const stateDirs = ensureDaemonStateDirs(agentRootDir);
|
|
13308
|
+
const stateDirs = ensureDaemonStateDirs(selection.stateRootDir ?? (selection.binding || values.source ? selection.source ?? agentRootDir : agentRootDir));
|
|
12064
13309
|
const result = await syncRuntimeSessions({
|
|
12065
13310
|
runtimeSessionStore: createApiRuntimeSessionStore({ agent: ctx.agent }),
|
|
12066
13311
|
runtimeSlotStore: createApiRuntimeSlotStore({ agent: ctx.agent }),
|
|
@@ -12072,7 +13317,7 @@ async function runSyncSessions(argv) {
|
|
|
12072
13317
|
runtimeProfileId: values["runtime-profile-id"],
|
|
12073
13318
|
sessionRootDir: stateDirs.piSessionsDir,
|
|
12074
13319
|
state,
|
|
12075
|
-
teamId:
|
|
13320
|
+
teamId: selection.teamId
|
|
12076
13321
|
});
|
|
12077
13322
|
console.log(JSON.stringify(result, null, 2));
|
|
12078
13323
|
return result.failedUpload > 0 || result.unsafeSessionPath > 0 ? 1 : 0;
|
|
@@ -12243,7 +13488,7 @@ async function writeCache(cache) {
|
|
|
12243
13488
|
}
|
|
12244
13489
|
//#endregion
|
|
12245
13490
|
//#region src/version.ts
|
|
12246
|
-
var DAEMON_VERSION = "0.
|
|
13491
|
+
var DAEMON_VERSION = "0.61.0";
|
|
12247
13492
|
//#endregion
|
|
12248
13493
|
//#region src/cli.ts
|
|
12249
13494
|
async function runAgentDaemonCli(options) {
|