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