@themoltnet/agent-daemon 0.49.1 → 0.50.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/dist/cli.js +794 -362
- package/dist/main.js +1 -1
- package/package.json +14 -7
package/dist/cli.js
CHANGED
|
@@ -30,15 +30,15 @@ import { mkdir, open, realpath, stat } from "node:fs/promises";
|
|
|
30
30
|
import { pipeline } from "node:stream/promises";
|
|
31
31
|
import cors from "@fastify/cors";
|
|
32
32
|
import helmet from "@fastify/helmet";
|
|
33
|
-
import {
|
|
34
|
-
import {
|
|
33
|
+
import { lock, lockSync } from "proper-lockfile";
|
|
34
|
+
import { ModelRuntime, readStoredCredential } from "@earendil-works/pi-coding-agent";
|
|
35
35
|
import { Transform, Writable } from "node:stream";
|
|
36
36
|
import { writePiConfig } from "@themoltnet/pi-runtime/pi-config";
|
|
37
37
|
import { homedir } from "node:os";
|
|
38
|
-
import { lock } from "proper-lockfile";
|
|
39
38
|
import { StringDecoder } from "node:string_decoder";
|
|
40
39
|
import rateLimit from "@fastify/rate-limit";
|
|
41
40
|
import Fastify from "fastify";
|
|
41
|
+
import { isIP } from "node:net";
|
|
42
42
|
import { createGzip } from "node:zlib";
|
|
43
43
|
//#region ../../libs/tasks/src/rubric.ts
|
|
44
44
|
/**
|
|
@@ -800,7 +800,7 @@ Object.freeze(ALL_CREDENTIAL_SCOPES.filter((scope) => scope !== CREDENTIAL_SCOPE
|
|
|
800
800
|
].filter((scope) => scope !== CREDENTIAL_SCOPES.HumanProfile);
|
|
801
801
|
//#endregion
|
|
802
802
|
//#region ../../libs/models/src/preview-sign.ts
|
|
803
|
-
function schemaRef$
|
|
803
|
+
function schemaRef$2(schema, id) {
|
|
804
804
|
return Type.Unsafe(Type.Ref(id));
|
|
805
805
|
}
|
|
806
806
|
var PreviewSignBase64UrlSchema = Type.String({
|
|
@@ -825,8 +825,8 @@ var PreviewSignEs256PublicKeySchema = Type.Object({
|
|
|
825
825
|
kty: Type.Literal(2),
|
|
826
826
|
algorithm: Type.Literal(-7),
|
|
827
827
|
curve: Type.Literal(1),
|
|
828
|
-
x: schemaRef$
|
|
829
|
-
y: schemaRef$
|
|
828
|
+
x: schemaRef$2(PreviewSignSha256Base64UrlSchema, "PreviewSignSha256Base64Url"),
|
|
829
|
+
y: schemaRef$2(PreviewSignSha256Base64UrlSchema, "PreviewSignSha256Base64Url")
|
|
830
830
|
}, {
|
|
831
831
|
$id: "PreviewSignEs256PublicKey",
|
|
832
832
|
additionalProperties: false
|
|
@@ -835,8 +835,8 @@ var PreviewSignEcdhEsHkdf256PublicKeySchema = Type.Object({
|
|
|
835
835
|
kty: Type.Literal(2),
|
|
836
836
|
algorithm: Type.Literal(-25),
|
|
837
837
|
curve: Type.Literal(1),
|
|
838
|
-
x: schemaRef$
|
|
839
|
-
y: schemaRef$
|
|
838
|
+
x: schemaRef$2(PreviewSignSha256Base64UrlSchema, "PreviewSignSha256Base64Url"),
|
|
839
|
+
y: schemaRef$2(PreviewSignSha256Base64UrlSchema, "PreviewSignSha256Base64Url")
|
|
840
840
|
}, {
|
|
841
841
|
$id: "PreviewSignEcdhEsHkdf256PublicKey",
|
|
842
842
|
additionalProperties: false
|
|
@@ -845,8 +845,8 @@ var PreviewSignEsp256PublicKeySchema = Type.Object({
|
|
|
845
845
|
kty: Type.Literal(2),
|
|
846
846
|
algorithm: Type.Literal(-9),
|
|
847
847
|
curve: Type.Literal(1),
|
|
848
|
-
x: schemaRef$
|
|
849
|
-
y: schemaRef$
|
|
848
|
+
x: schemaRef$2(PreviewSignSha256Base64UrlSchema, "PreviewSignSha256Base64Url"),
|
|
849
|
+
y: schemaRef$2(PreviewSignSha256Base64UrlSchema, "PreviewSignSha256Base64Url")
|
|
850
850
|
}, {
|
|
851
851
|
$id: "PreviewSignEsp256PublicKey",
|
|
852
852
|
additionalProperties: false
|
|
@@ -855,18 +855,18 @@ var PreviewSignArkgSeedPublicKeySchema = Type.Object({
|
|
|
855
855
|
kty: Type.Literal(-65537),
|
|
856
856
|
algorithm: Type.Literal(-65700),
|
|
857
857
|
derivedAlgorithm: Type.Literal(-9),
|
|
858
|
-
blindingKey: schemaRef$
|
|
859
|
-
kemKey: schemaRef$
|
|
858
|
+
blindingKey: schemaRef$2(PreviewSignEs256PublicKeySchema, "PreviewSignEs256PublicKey"),
|
|
859
|
+
kemKey: schemaRef$2(PreviewSignEcdhEsHkdf256PublicKeySchema, "PreviewSignEcdhEsHkdf256PublicKey")
|
|
860
860
|
}, {
|
|
861
861
|
$id: "PreviewSignArkgSeedPublicKey",
|
|
862
862
|
additionalProperties: false
|
|
863
863
|
});
|
|
864
864
|
var PreviewSignPublicMaterialSchema = Type.Object({
|
|
865
865
|
version: Type.Literal(1),
|
|
866
|
-
outerCredentialId: schemaRef$
|
|
867
|
-
outerPublicKey: schemaRef$
|
|
868
|
-
previewKeyHandle: schemaRef$
|
|
869
|
-
seedPublicKey: schemaRef$
|
|
866
|
+
outerCredentialId: schemaRef$2(PreviewSignBase64UrlSchema, "PreviewSignBase64Url"),
|
|
867
|
+
outerPublicKey: schemaRef$2(PreviewSignEs256PublicKeySchema, "PreviewSignEs256PublicKey"),
|
|
868
|
+
previewKeyHandle: schemaRef$2(PreviewSignBase64UrlSchema, "PreviewSignBase64Url"),
|
|
869
|
+
seedPublicKey: schemaRef$2(PreviewSignArkgSeedPublicKeySchema, "PreviewSignArkgSeedPublicKey")
|
|
870
870
|
}, {
|
|
871
871
|
$id: "PreviewSignPublicMaterial",
|
|
872
872
|
additionalProperties: false
|
|
@@ -874,19 +874,19 @@ var PreviewSignPublicMaterialSchema = Type.Object({
|
|
|
874
874
|
var PreviewSignChallengeSchema = Type.Object({
|
|
875
875
|
verificationMethod: Type.Literal("human-hardware-previewsign"),
|
|
876
876
|
version: Type.Literal(1),
|
|
877
|
-
envelope: schemaRef$
|
|
878
|
-
digest: schemaRef$
|
|
879
|
-
additionalArguments: schemaRef$
|
|
880
|
-
outerCredentialId: schemaRef$
|
|
881
|
-
outerPublicKey: schemaRef$
|
|
882
|
-
previewKeyHandle: schemaRef$
|
|
877
|
+
envelope: schemaRef$2(PreviewSignBase64UrlSchema, "PreviewSignBase64Url"),
|
|
878
|
+
digest: schemaRef$2(PreviewSignSha256Base64UrlSchema, "PreviewSignSha256Base64Url"),
|
|
879
|
+
additionalArguments: schemaRef$2(PreviewSignBase64UrlSchema, "PreviewSignBase64Url"),
|
|
880
|
+
outerCredentialId: schemaRef$2(PreviewSignBase64UrlSchema, "PreviewSignBase64Url"),
|
|
881
|
+
outerPublicKey: schemaRef$2(PreviewSignEs256PublicKeySchema, "PreviewSignEs256PublicKey"),
|
|
882
|
+
previewKeyHandle: schemaRef$2(PreviewSignBase64UrlSchema, "PreviewSignBase64Url")
|
|
883
883
|
}, {
|
|
884
884
|
$id: "PreviewSignChallenge",
|
|
885
885
|
additionalProperties: false
|
|
886
886
|
});
|
|
887
887
|
var PreviewSignChallengeValueSchema = Type.Object({
|
|
888
888
|
verificationMethod: Type.Literal("human-hardware-previewsign"),
|
|
889
|
-
value: schemaRef$
|
|
889
|
+
value: schemaRef$2(PreviewSignChallengeSchema, "PreviewSignChallenge")
|
|
890
890
|
}, {
|
|
891
891
|
$id: "PreviewSignChallengeValue",
|
|
892
892
|
additionalProperties: false
|
|
@@ -894,14 +894,14 @@ var PreviewSignChallengeValueSchema = Type.Object({
|
|
|
894
894
|
var PreviewSignChallengeOperationSchema = Type.Union([Type.Literal("credential-registration"), Type.Literal("signing-request")], { $id: "PreviewSignChallengeOperation" });
|
|
895
895
|
var PreviewSignReceiptSchema = Type.Object({
|
|
896
896
|
version: Type.Literal(1),
|
|
897
|
-
signature: schemaRef$
|
|
897
|
+
signature: schemaRef$2(PreviewSignP256DerSignatureBase64UrlSchema, "PreviewSignP256DerSignatureBase64Url")
|
|
898
898
|
}, {
|
|
899
899
|
$id: "PreviewSignReceipt",
|
|
900
900
|
additionalProperties: false
|
|
901
901
|
});
|
|
902
902
|
var PreviewSignReceiptValueSchema = Type.Object({
|
|
903
903
|
verificationMethod: Type.Literal("human-hardware-previewsign"),
|
|
904
|
-
value: schemaRef$
|
|
904
|
+
value: schemaRef$2(PreviewSignReceiptSchema, "PreviewSignReceipt")
|
|
905
905
|
}, {
|
|
906
906
|
$id: "PreviewSignReceiptValue",
|
|
907
907
|
additionalProperties: false
|
|
@@ -1595,7 +1595,7 @@ Type.Union([
|
|
|
1595
1595
|
]);
|
|
1596
1596
|
//#endregion
|
|
1597
1597
|
//#region ../../libs/models/src/signer-protocol.ts
|
|
1598
|
-
function schemaRef(schema) {
|
|
1598
|
+
function schemaRef$1(schema) {
|
|
1599
1599
|
const id = schemaId(schema);
|
|
1600
1600
|
return Type.Ref(id);
|
|
1601
1601
|
}
|
|
@@ -1624,13 +1624,13 @@ var SignerProblemSchema = Type.Object({
|
|
|
1624
1624
|
$id: "SignerProblem",
|
|
1625
1625
|
additionalProperties: false
|
|
1626
1626
|
});
|
|
1627
|
-
var SignerCeremonyParamsSchema = Type.Object({ ceremonyId: Type.Unsafe(schemaRef(SignerBase64UrlSchema)) }, {
|
|
1627
|
+
var SignerCeremonyParamsSchema = Type.Object({ ceremonyId: Type.Unsafe(schemaRef$1(SignerBase64UrlSchema)) }, {
|
|
1628
1628
|
$id: "SignerCeremonyParams",
|
|
1629
1629
|
additionalProperties: false
|
|
1630
1630
|
});
|
|
1631
1631
|
var SignerSessionSchema = Type.Object({
|
|
1632
1632
|
version: Type.Literal(1),
|
|
1633
|
-
token: Type.Unsafe(schemaRef(SignerBase64UrlSchema)),
|
|
1633
|
+
token: Type.Unsafe(schemaRef$1(SignerBase64UrlSchema)),
|
|
1634
1634
|
expiresAt: Type.String()
|
|
1635
1635
|
}, {
|
|
1636
1636
|
$id: "SignerSession",
|
|
@@ -1643,25 +1643,25 @@ var SignerEnrollmentCeremonyRequestSchema = Type.Object({
|
|
|
1643
1643
|
minLength: 1,
|
|
1644
1644
|
maxLength: 255
|
|
1645
1645
|
}),
|
|
1646
|
-
teamId: Type.Unsafe(schemaRef(SignerUuidSchema))
|
|
1646
|
+
teamId: Type.Unsafe(schemaRef$1(SignerUuidSchema))
|
|
1647
1647
|
}, {
|
|
1648
1648
|
$id: "SignerEnrollmentCeremonyRequest",
|
|
1649
1649
|
additionalProperties: false
|
|
1650
1650
|
});
|
|
1651
1651
|
var SignerChallengeCeremonyRequestSchema = Type.Object({
|
|
1652
1652
|
version: Type.Literal(1),
|
|
1653
|
-
operation: Type.Unsafe(schemaRef(SignerChallengeOperationSchema)),
|
|
1654
|
-
resourceId: Type.Unsafe(schemaRef(SignerUuidSchema)),
|
|
1655
|
-
challenge: Type.Unsafe(schemaRef(SignerPreviewSignChallengeValueSchema))
|
|
1653
|
+
operation: Type.Unsafe(schemaRef$1(SignerChallengeOperationSchema)),
|
|
1654
|
+
resourceId: Type.Unsafe(schemaRef$1(SignerUuidSchema)),
|
|
1655
|
+
challenge: Type.Unsafe(schemaRef$1(SignerPreviewSignChallengeValueSchema))
|
|
1656
1656
|
}, {
|
|
1657
1657
|
$id: "SignerChallengeCeremonyRequest",
|
|
1658
1658
|
additionalProperties: false
|
|
1659
1659
|
});
|
|
1660
|
-
var SignerCeremonyRequestSchema = Type.Union([Type.Unsafe(schemaRef(SignerEnrollmentCeremonyRequestSchema)), Type.Unsafe(schemaRef(SignerChallengeCeremonyRequestSchema))], { $id: "SignerCeremonyRequest" });
|
|
1660
|
+
var SignerCeremonyRequestSchema = Type.Union([Type.Unsafe(schemaRef$1(SignerEnrollmentCeremonyRequestSchema)), Type.Unsafe(schemaRef$1(SignerChallengeCeremonyRequestSchema))], { $id: "SignerCeremonyRequest" });
|
|
1661
1661
|
var SignerCeremonySchema = Type.Object({
|
|
1662
1662
|
version: Type.Literal(1),
|
|
1663
|
-
id: Type.Unsafe(schemaRef(SignerBase64UrlSchema)),
|
|
1664
|
-
operation: Type.Unsafe(schemaRef(SignerOperationSchema)),
|
|
1663
|
+
id: Type.Unsafe(schemaRef$1(SignerBase64UrlSchema)),
|
|
1664
|
+
operation: Type.Unsafe(schemaRef$1(SignerOperationSchema)),
|
|
1665
1665
|
approvalUrl: Type.String(),
|
|
1666
1666
|
expiresAt: Type.String()
|
|
1667
1667
|
}, {
|
|
@@ -1671,7 +1671,7 @@ var SignerCeremonySchema = Type.Object({
|
|
|
1671
1671
|
var SignerPendingResultSchema = Type.Object({
|
|
1672
1672
|
version: Type.Literal(1),
|
|
1673
1673
|
status: Type.Literal("pending"),
|
|
1674
|
-
operation: Type.Unsafe(schemaRef(SignerOperationSchema))
|
|
1674
|
+
operation: Type.Unsafe(schemaRef$1(SignerOperationSchema))
|
|
1675
1675
|
}, {
|
|
1676
1676
|
$id: "SignerPendingResult",
|
|
1677
1677
|
additionalProperties: false
|
|
@@ -1680,7 +1680,7 @@ var SignerEnrollmentResultSchema = Type.Object({
|
|
|
1680
1680
|
version: Type.Literal(1),
|
|
1681
1681
|
status: Type.Literal("completed"),
|
|
1682
1682
|
operation: Type.Literal("credential-enrollment"),
|
|
1683
|
-
publicMaterial: Type.Unsafe(schemaRef(SignerPreviewSignPublicMaterialSchema))
|
|
1683
|
+
publicMaterial: Type.Unsafe(schemaRef$1(SignerPreviewSignPublicMaterialSchema))
|
|
1684
1684
|
}, {
|
|
1685
1685
|
$id: "SignerEnrollmentResult",
|
|
1686
1686
|
additionalProperties: false
|
|
@@ -1689,8 +1689,8 @@ var SignerReceiptSchema = PreviewSignReceiptValueSchema;
|
|
|
1689
1689
|
var SignerSignatureResultSchema = Type.Object({
|
|
1690
1690
|
version: Type.Literal(1),
|
|
1691
1691
|
status: Type.Literal("completed"),
|
|
1692
|
-
operation: Type.Unsafe(schemaRef(SignerChallengeOperationSchema)),
|
|
1693
|
-
receipt: Type.Unsafe(schemaRef(SignerReceiptSchema))
|
|
1692
|
+
operation: Type.Unsafe(schemaRef$1(SignerChallengeOperationSchema)),
|
|
1693
|
+
receipt: Type.Unsafe(schemaRef$1(SignerReceiptSchema))
|
|
1694
1694
|
}, {
|
|
1695
1695
|
$id: "SignerSignatureResult",
|
|
1696
1696
|
additionalProperties: false
|
|
@@ -1698,7 +1698,7 @@ var SignerSignatureResultSchema = Type.Object({
|
|
|
1698
1698
|
var SignerFailedResultSchema = Type.Object({
|
|
1699
1699
|
version: Type.Literal(1),
|
|
1700
1700
|
status: Type.Literal("failed"),
|
|
1701
|
-
operation: Type.Unsafe(schemaRef(SignerOperationSchema)),
|
|
1701
|
+
operation: Type.Unsafe(schemaRef$1(SignerOperationSchema)),
|
|
1702
1702
|
code: Type.String(),
|
|
1703
1703
|
message: Type.String()
|
|
1704
1704
|
}, {
|
|
@@ -1706,10 +1706,10 @@ var SignerFailedResultSchema = Type.Object({
|
|
|
1706
1706
|
additionalProperties: false
|
|
1707
1707
|
});
|
|
1708
1708
|
var SignerCeremonyResultSchema = Type.Union([
|
|
1709
|
-
Type.Unsafe(schemaRef(SignerPendingResultSchema)),
|
|
1710
|
-
Type.Unsafe(schemaRef(SignerEnrollmentResultSchema)),
|
|
1711
|
-
Type.Unsafe(schemaRef(SignerSignatureResultSchema)),
|
|
1712
|
-
Type.Unsafe(schemaRef(SignerFailedResultSchema))
|
|
1709
|
+
Type.Unsafe(schemaRef$1(SignerPendingResultSchema)),
|
|
1710
|
+
Type.Unsafe(schemaRef$1(SignerEnrollmentResultSchema)),
|
|
1711
|
+
Type.Unsafe(schemaRef$1(SignerSignatureResultSchema)),
|
|
1712
|
+
Type.Unsafe(schemaRef$1(SignerFailedResultSchema))
|
|
1713
1713
|
], { $id: "SignerCeremonyResult" });
|
|
1714
1714
|
({ ...previewSignSchemaContext }), schemaId(SignerUuidSchema), schemaId(SignerOperationSchema), schemaId(SignerProblemSchema), schemaId(SignerCeremonyParamsSchema), schemaId(SignerSessionSchema), schemaId(SignerEnrollmentCeremonyRequestSchema), schemaId(SignerChallengeCeremonyRequestSchema), schemaId(SignerCeremonyRequestSchema), schemaId(SignerCeremonySchema), schemaId(SignerPendingResultSchema), schemaId(SignerEnrollmentResultSchema), schemaId(SignerSignatureResultSchema), schemaId(SignerFailedResultSchema), schemaId(SignerCeremonyResultSchema);
|
|
1715
1715
|
//#endregion
|
|
@@ -3426,7 +3426,7 @@ Commands:
|
|
|
3426
3426
|
once Claim and execute one specific queued task by id, then exit.
|
|
3427
3427
|
drain Poll until the queue has nothing claimable, then exit.
|
|
3428
3428
|
Useful for batch eval runs and demos.
|
|
3429
|
-
|
|
3429
|
+
server Loopback supervisor for console-managed runs: pairing,
|
|
3430
3430
|
agent/provider config store, and start/stop of poll/drain
|
|
3431
3431
|
child processes. Binds 127.0.0.1 only.
|
|
3432
3432
|
sync-sessions
|
|
@@ -3561,8 +3561,8 @@ Example:
|
|
|
3561
3561
|
function isHelpFlag(args) {
|
|
3562
3562
|
return args.includes("--help") || args.includes("-h");
|
|
3563
3563
|
}
|
|
3564
|
-
var
|
|
3565
|
-
agent-daemon
|
|
3564
|
+
var AGENT_SERVER_HELP = `\
|
|
3565
|
+
agent-daemon server — loopback supervisor for console-managed runs.
|
|
3566
3566
|
|
|
3567
3567
|
Binds 127.0.0.1 only. A paired Console origin configures agents and
|
|
3568
3568
|
providers (secret references only) and starts/stops poll/drain runs as
|
|
@@ -3570,12 +3570,12 @@ child processes of this supervisor.
|
|
|
3570
3570
|
|
|
3571
3571
|
Options:
|
|
3572
3572
|
--port <n> Loopback port. Default: 17374.
|
|
3573
|
-
Env:
|
|
3573
|
+
Env: MOLTNET_AGENT_SERVER_PORT.
|
|
3574
3574
|
--allowed-origins <csv> Exact Console origins allowed to pair.
|
|
3575
3575
|
Default: https://console.themolt.net.
|
|
3576
|
-
Env:
|
|
3576
|
+
Env: MOLTNET_AGENT_SERVER_ALLOWED_ORIGINS.
|
|
3577
3577
|
--root <path> Config root. Default: ~/.config/moltnet
|
|
3578
|
-
(or
|
|
3578
|
+
(or MOLTNET_AGENT_SERVER_ROOT).
|
|
3579
3579
|
--api-url <url> Default MoltNet API for new managed agents.
|
|
3580
3580
|
Default: https://api.themolt.net.
|
|
3581
3581
|
`;
|
|
@@ -3658,7 +3658,7 @@ async function validateStartupBinding(options) {
|
|
|
3658
3658
|
const assessment = assessStartupBinding(whoami, options.teamId);
|
|
3659
3659
|
if (!assessment.ok) throw new Error(`Daemon startup validation failed: ${assessment.reason}`);
|
|
3660
3660
|
const expected = options.expectedIdentity;
|
|
3661
|
-
if (expected && !assessIdentityPin(whoami, expected).ok) throw new Error("Daemon startup validation failed: authenticated identity does not match the
|
|
3661
|
+
if (expected && !assessIdentityPin(whoami, expected).ok) throw new Error("Daemon startup validation failed: authenticated identity does not match the Agent Server activation.");
|
|
3662
3662
|
return whoami;
|
|
3663
3663
|
}
|
|
3664
3664
|
/**
|
|
@@ -3784,17 +3784,17 @@ function readBoolean(name, value) {
|
|
|
3784
3784
|
function activatePiCodingAgentDir(path) {
|
|
3785
3785
|
process.env["PI_CODING_AGENT_DIR"] = path;
|
|
3786
3786
|
}
|
|
3787
|
-
function
|
|
3787
|
+
function loadAgentServerEnvConfig() {
|
|
3788
3788
|
return {
|
|
3789
|
-
port: process.env["
|
|
3790
|
-
allowedOrigins: process.env["
|
|
3791
|
-
root: process.env["
|
|
3789
|
+
port: process.env["MOLTNET_AGENT_SERVER_PORT"] ?? "",
|
|
3790
|
+
allowedOrigins: process.env["MOLTNET_AGENT_SERVER_ALLOWED_ORIGINS"] ?? "",
|
|
3791
|
+
root: process.env["MOLTNET_AGENT_SERVER_ROOT"] ?? "",
|
|
3792
3792
|
xdgConfigHome: process.env["XDG_CONFIG_HOME"] ?? "",
|
|
3793
3793
|
apiUrl: process.env["MOLTNET_API_URL"] ?? "",
|
|
3794
3794
|
logLevel: process.env["LOG_LEVEL"] ?? ""
|
|
3795
3795
|
};
|
|
3796
3796
|
}
|
|
3797
|
-
/** Full process environment for spawned
|
|
3797
|
+
/** Full process environment for spawned Agent Server run children. */
|
|
3798
3798
|
function processEnvSnapshot() {
|
|
3799
3799
|
return process.env;
|
|
3800
3800
|
}
|
|
@@ -4859,7 +4859,7 @@ function executionIntentFromRuntimeProfile(input) {
|
|
|
4859
4859
|
* local configuration and logs its value-free decisions. It gates nothing
|
|
4860
4860
|
* yet — enforcement is a separate, later change.
|
|
4861
4861
|
*/
|
|
4862
|
-
var
|
|
4862
|
+
var DEFAULT_GOVERNANCE_OBAGENT_SERVER_TIMEOUT_MS = 2e3;
|
|
4863
4863
|
/**
|
|
4864
4864
|
* Two distinct provenance sources (#2022 review): requirements come from a
|
|
4865
4865
|
* profile-side/private input (`MOLTNET_PROFILE_CREDENTIAL_REQUIREMENTS`, a
|
|
@@ -4988,7 +4988,7 @@ async function observeGovernancePlan(input) {
|
|
|
4988
4988
|
* The observer is intentionally non-gating: failures and deadlines are logged
|
|
4989
4989
|
* with claim correlation and converted to `null`.
|
|
4990
4990
|
*/
|
|
4991
|
-
async function observeGovernancePlanSafely(input, timeoutMs =
|
|
4991
|
+
async function observeGovernancePlanSafely(input, timeoutMs = DEFAULT_GOVERNANCE_OBAGENT_SERVER_TIMEOUT_MS) {
|
|
4992
4992
|
let timeout;
|
|
4993
4993
|
try {
|
|
4994
4994
|
return await Promise.race([observeGovernancePlan(input), new Promise((_resolve, reject) => {
|
|
@@ -7056,6 +7056,8 @@ function requireOriginHeader(headers) {
|
|
|
7056
7056
|
//#endregion
|
|
7057
7057
|
//#region ../../libs/loopback-companion/src/fastify.ts
|
|
7058
7058
|
var CORS_PREFLIGHT_MAX_AGE_SECONDS = 600;
|
|
7059
|
+
var PRIVATE_NETWORK_REQUEST_HEADER = "access-control-request-private-network";
|
|
7060
|
+
var PRIVATE_NETWORK_ALLOW_HEADER = "access-control-allow-private-network";
|
|
7059
7061
|
/**
|
|
7060
7062
|
* Enforce that the `Host` header identifies loopback. Blocks DNS-rebinding
|
|
7061
7063
|
* setups where a public hostname resolves to 127.0.0.1: the browser then
|
|
@@ -7093,8 +7095,9 @@ function registerLoopbackSecurity(app, options) {
|
|
|
7093
7095
|
requireLoopbackHost(request);
|
|
7094
7096
|
done();
|
|
7095
7097
|
});
|
|
7096
|
-
app.addHook("onSend", async (
|
|
7098
|
+
app.addHook("onSend", async (request, reply, payload) => {
|
|
7097
7099
|
reply.header("cache-control", "no-store");
|
|
7100
|
+
if (request.method === "OPTIONS" && request.headers[PRIVATE_NETWORK_REQUEST_HEADER] === "true" && reply.getHeader("access-control-allow-origin") === request.headers.origin) reply.header(PRIVATE_NETWORK_ALLOW_HEADER, "true");
|
|
7098
7101
|
return payload;
|
|
7099
7102
|
});
|
|
7100
7103
|
app.removeContentTypeParser("application/json");
|
|
@@ -7170,9 +7173,65 @@ function rejectExplicitCrossSite(headers) {
|
|
|
7170
7173
|
if (headerValue(headers, "sec-fetch-site") === "cross-site") throw new LoopbackViolationError("cross_site_rejected", "Request must not originate cross-site");
|
|
7171
7174
|
}
|
|
7172
7175
|
//#endregion
|
|
7173
|
-
//#region src/lib/
|
|
7176
|
+
//#region src/lib/agent-server/lock.ts
|
|
7177
|
+
var AgentServerLockError = class extends Error {
|
|
7178
|
+
name = "AgentServerLockError";
|
|
7179
|
+
constructor(code, message, options) {
|
|
7180
|
+
super(message, options);
|
|
7181
|
+
this.code = code;
|
|
7182
|
+
}
|
|
7183
|
+
};
|
|
7184
|
+
/**
|
|
7185
|
+
* Acquire the per-root singleton lock. `proper-lockfile` uses an atomic lock
|
|
7186
|
+
* directory at exactly `<root>/agent-server.lock`, recovers stale owners, and keeps
|
|
7187
|
+
* the mtime fresh while the supervisor is alive.
|
|
7188
|
+
*/
|
|
7189
|
+
async function acquireAgentServerLock(root, options = {}) {
|
|
7190
|
+
const path = join(root, "agent-server.lock");
|
|
7191
|
+
let releaseLock;
|
|
7192
|
+
try {
|
|
7193
|
+
releaseLock = await lock(root, {
|
|
7194
|
+
lockfilePath: path,
|
|
7195
|
+
realpath: false,
|
|
7196
|
+
retries: 0,
|
|
7197
|
+
...options.staleMs === void 0 ? {} : { stale: options.staleMs },
|
|
7198
|
+
...options.updateMs === void 0 ? {} : { update: options.updateMs },
|
|
7199
|
+
onCompromised: (cause) => {
|
|
7200
|
+
const error = new AgentServerLockError("compromised", `Agent Server lock ${path} was compromised: ${cause.message}`, { cause });
|
|
7201
|
+
if (options.onCompromised) {
|
|
7202
|
+
options.onCompromised(error);
|
|
7203
|
+
return;
|
|
7204
|
+
}
|
|
7205
|
+
throw error;
|
|
7206
|
+
}
|
|
7207
|
+
});
|
|
7208
|
+
} catch (cause) {
|
|
7209
|
+
if (cause.code === "ELOCKED") throw new AgentServerLockError("held", `another moltnet-agent server process already owns ${path}`, { cause });
|
|
7210
|
+
throw new AgentServerLockError("failed", `could not acquire Agent Server lock ${path}: ${cause.message}`, { cause });
|
|
7211
|
+
}
|
|
7212
|
+
let released = false;
|
|
7213
|
+
return {
|
|
7214
|
+
path,
|
|
7215
|
+
async release() {
|
|
7216
|
+
if (released) return;
|
|
7217
|
+
released = true;
|
|
7218
|
+
await releaseLock();
|
|
7219
|
+
}
|
|
7220
|
+
};
|
|
7221
|
+
}
|
|
7222
|
+
/** Always release after normal completion or startup/runtime failure. */
|
|
7223
|
+
async function withAgentServerLock(root, work, options) {
|
|
7224
|
+
const held = await acquireAgentServerLock(root, options);
|
|
7225
|
+
try {
|
|
7226
|
+
return await work();
|
|
7227
|
+
} finally {
|
|
7228
|
+
await held.release();
|
|
7229
|
+
}
|
|
7230
|
+
}
|
|
7231
|
+
//#endregion
|
|
7232
|
+
//#region src/lib/agent-server/pairing.ts
|
|
7174
7233
|
/**
|
|
7175
|
-
* One-click pairing ceremony for the
|
|
7234
|
+
* One-click pairing ceremony for the agent server, mirroring the signer's
|
|
7176
7235
|
* session/ceremony pattern (#2062 design):
|
|
7177
7236
|
*
|
|
7178
7237
|
* 1. Console (allowed origin) POSTs `/v1/pairings` → pending pairing bound
|
|
@@ -7193,8 +7252,8 @@ function rejectExplicitCrossSite(headers) {
|
|
|
7193
7252
|
* supervisor process.
|
|
7194
7253
|
*/
|
|
7195
7254
|
var PENDING_TTL_MS = 600 * 1e3;
|
|
7196
|
-
var
|
|
7197
|
-
name = "
|
|
7255
|
+
var AgentServerPairingError = class extends Error {
|
|
7256
|
+
name = "AgentServerPairingError";
|
|
7198
7257
|
constructor(code, message) {
|
|
7199
7258
|
super(message);
|
|
7200
7259
|
this.code = code;
|
|
@@ -7242,7 +7301,7 @@ var PairingService = class {
|
|
|
7242
7301
|
/** Data for the local approval page. */
|
|
7243
7302
|
approval(pairingId) {
|
|
7244
7303
|
const pairing = this.require(pairingId);
|
|
7245
|
-
if (pairing.approved) throw new
|
|
7304
|
+
if (pairing.approved) throw new AgentServerPairingError("pairing_invalid", "Pairing is already approved");
|
|
7246
7305
|
return {
|
|
7247
7306
|
origin: pairing.origin,
|
|
7248
7307
|
confirmToken: pairing.confirmToken
|
|
@@ -7250,15 +7309,15 @@ var PairingService = class {
|
|
|
7250
7309
|
}
|
|
7251
7310
|
confirm(pairingId, confirmToken) {
|
|
7252
7311
|
const pairing = this.require(pairingId);
|
|
7253
|
-
if (pairing.approved || !safeEqual(pairing.confirmToken, confirmToken)) throw new
|
|
7312
|
+
if (pairing.approved || !safeEqual(pairing.confirmToken, confirmToken)) throw new AgentServerPairingError("pairing_invalid", "Confirmation token is not valid");
|
|
7254
7313
|
pairing.approved = true;
|
|
7255
7314
|
pairing.bearerToken = this.token();
|
|
7256
7315
|
return { origin: pairing.origin };
|
|
7257
7316
|
}
|
|
7258
7317
|
claim(pairingId, origin) {
|
|
7259
7318
|
const pairing = this.require(pairingId);
|
|
7260
|
-
if (pairing.origin !== origin) throw new
|
|
7261
|
-
if (!pairing.approved || !pairing.bearerToken) throw new
|
|
7319
|
+
if (pairing.origin !== origin) throw new AgentServerPairingError("pairing_origin_mismatch", "Pairing belongs to a different origin");
|
|
7320
|
+
if (!pairing.approved || !pairing.bearerToken) throw new AgentServerPairingError("pairing_not_approved", "Pairing has not been approved yet");
|
|
7262
7321
|
const token = pairing.bearerToken;
|
|
7263
7322
|
this.pending.delete(pairingId);
|
|
7264
7323
|
this.paired.set(origin, sha256Hex(token));
|
|
@@ -7266,12 +7325,12 @@ var PairingService = class {
|
|
|
7266
7325
|
}
|
|
7267
7326
|
verify(origin, token) {
|
|
7268
7327
|
const tokenHash = this.paired.get(origin);
|
|
7269
|
-
if (!tokenHash || !safeEqual(tokenHash, sha256Hex(token))) throw new
|
|
7328
|
+
if (!tokenHash || !safeEqual(tokenHash, sha256Hex(token))) throw new AgentServerPairingError("pairing_token_invalid", "Pairing token is not valid for this origin");
|
|
7270
7329
|
}
|
|
7271
7330
|
require(pairingId) {
|
|
7272
7331
|
this.sweep();
|
|
7273
7332
|
const pairing = this.pending.get(pairingId);
|
|
7274
|
-
if (!pairing) throw new
|
|
7333
|
+
if (!pairing) throw new AgentServerPairingError("pairing_not_found", "Pairing was not found or has expired");
|
|
7275
7334
|
return pairing;
|
|
7276
7335
|
}
|
|
7277
7336
|
};
|
|
@@ -7321,24 +7380,25 @@ function renderPairingResultPage(input) {
|
|
|
7321
7380
|
`;
|
|
7322
7381
|
}
|
|
7323
7382
|
//#endregion
|
|
7324
|
-
//#region src/lib/
|
|
7383
|
+
//#region src/lib/agent-server/provider-login.ts
|
|
7325
7384
|
/**
|
|
7326
|
-
* Subscription-provider OAuth brokering for
|
|
7385
|
+
* Subscription-provider OAuth brokering for Agent Server (#2061 slice 4).
|
|
7327
7386
|
*
|
|
7328
|
-
* The console clicks "Connect";
|
|
7329
|
-
* `
|
|
7387
|
+
* The console clicks "Connect"; agent server runs the Pi OAuth flow host-side via
|
|
7388
|
+
* `ModelRuntime.login()` (which owns persistence into the shared
|
|
7330
7389
|
* `pi/auth.json` and token rotation thereafter). The browser only ever sees
|
|
7331
7390
|
* the provider's authorize URL or device code — never tokens.
|
|
7332
7391
|
*
|
|
7333
|
-
* Provider ids come from
|
|
7334
|
-
*
|
|
7335
|
-
*
|
|
7392
|
+
* Provider ids come from Pi's model runtime, so Agent Server stays in lockstep
|
|
7393
|
+
* with supported subscription providers. GitHub Copilot is intentionally
|
|
7394
|
+
* excluded: MoltNet does not broker editor-seat subscription credentials.
|
|
7336
7395
|
*/
|
|
7337
7396
|
var LOGIN_TTL_MS = 600 * 1e3;
|
|
7338
7397
|
/** How long `start()` waits for the flow to surface a URL / device code. */
|
|
7339
7398
|
var START_INFO_TIMEOUT_MS = 5e3;
|
|
7340
|
-
var
|
|
7341
|
-
|
|
7399
|
+
var EXCLUDED_SUBSCRIPTION_PROVIDERS = new Set(["github-copilot"]);
|
|
7400
|
+
var AgentServerSubscriptionError = class extends Error {
|
|
7401
|
+
name = "AgentServerSubscriptionError";
|
|
7342
7402
|
constructor(code, message) {
|
|
7343
7403
|
super(message);
|
|
7344
7404
|
this.code = code;
|
|
@@ -7349,37 +7409,49 @@ var silentLogger = {
|
|
|
7349
7409
|
warn: () => void 0,
|
|
7350
7410
|
error: () => void 0
|
|
7351
7411
|
};
|
|
7352
|
-
var ProviderLoginService = class {
|
|
7412
|
+
var ProviderLoginService = class ProviderLoginService {
|
|
7353
7413
|
logins = /* @__PURE__ */ new Map();
|
|
7354
|
-
authStorage;
|
|
7355
7414
|
logger;
|
|
7356
7415
|
constructor(options) {
|
|
7357
7416
|
this.options = options;
|
|
7358
|
-
this.authStorage = options.authStorage ?? AuthStorage.create(this.options.authPath);
|
|
7359
7417
|
this.logger = options.logger ?? silentLogger;
|
|
7360
7418
|
}
|
|
7419
|
+
static async create(options) {
|
|
7420
|
+
const modelRuntime = await ModelRuntime.create({
|
|
7421
|
+
authPath: options.authPath,
|
|
7422
|
+
refreshOnCreate: false
|
|
7423
|
+
});
|
|
7424
|
+
return new ProviderLoginService({
|
|
7425
|
+
...options,
|
|
7426
|
+
modelRuntime
|
|
7427
|
+
});
|
|
7428
|
+
}
|
|
7361
7429
|
now() {
|
|
7362
7430
|
return this.options.now?.() ?? Date.now();
|
|
7363
7431
|
}
|
|
7364
7432
|
providers() {
|
|
7365
|
-
return this.options.listProviders
|
|
7433
|
+
return (this.options.listProviders ? this.options.listProviders() : this.runtime().getProviders().filter((provider) => provider.auth.oauth !== void 0).map((provider) => ({
|
|
7366
7434
|
id: provider.id,
|
|
7367
7435
|
name: provider.name
|
|
7368
|
-
}));
|
|
7436
|
+
}))).filter((provider) => !EXCLUDED_SUBSCRIPTION_PROVIDERS.has(provider.id));
|
|
7369
7437
|
}
|
|
7370
7438
|
connected(providerId) {
|
|
7371
7439
|
if (this.options.isConnected) return this.options.isConnected(providerId);
|
|
7372
7440
|
try {
|
|
7373
|
-
return (this.options.
|
|
7441
|
+
return readStoredCredential(providerId, this.options.authPath) !== void 0;
|
|
7374
7442
|
} catch (error) {
|
|
7375
7443
|
this.logger.warn({
|
|
7376
|
-
event: "
|
|
7444
|
+
event: "agent-server.subscription_auth_read_failed",
|
|
7377
7445
|
providerId,
|
|
7378
7446
|
...safeLoginError(error)
|
|
7379
7447
|
}, "Could not read subscription authentication state");
|
|
7380
7448
|
return false;
|
|
7381
7449
|
}
|
|
7382
7450
|
}
|
|
7451
|
+
runtime() {
|
|
7452
|
+
if (!this.options.modelRuntime) throw new Error("ProviderLoginService requires a model runtime when production adapters are not overridden");
|
|
7453
|
+
return this.options.modelRuntime;
|
|
7454
|
+
}
|
|
7383
7455
|
list() {
|
|
7384
7456
|
this.sweep();
|
|
7385
7457
|
return this.providers().map((provider) => ({
|
|
@@ -7390,40 +7462,42 @@ var ProviderLoginService = class {
|
|
|
7390
7462
|
sweep() {
|
|
7391
7463
|
const now = this.now();
|
|
7392
7464
|
for (const [id, login] of this.logins) if (login.startedAt + LOGIN_TTL_MS <= now) {
|
|
7393
|
-
this.invalidate(login, "expired");
|
|
7465
|
+
if (login.status === "pending") this.invalidate(login, "expired");
|
|
7394
7466
|
this.logins.delete(id);
|
|
7395
7467
|
}
|
|
7396
7468
|
}
|
|
7397
7469
|
restoreCredential(login) {
|
|
7398
7470
|
try {
|
|
7399
|
-
|
|
7400
|
-
|
|
7471
|
+
restoreStoredCredential(this.options.authPath, login.providerId, login.previousCredential);
|
|
7472
|
+
return true;
|
|
7401
7473
|
} catch (error) {
|
|
7402
7474
|
this.logger.error({
|
|
7403
|
-
event: "
|
|
7475
|
+
event: "agent-server.subscription_login_cleanup_failed",
|
|
7404
7476
|
operationId: login.operationId,
|
|
7405
7477
|
providerId: login.providerId,
|
|
7406
7478
|
...safeLoginError(error)
|
|
7407
7479
|
}, "Could not restore subscription credentials after an invalidated login");
|
|
7480
|
+
return false;
|
|
7408
7481
|
}
|
|
7409
7482
|
}
|
|
7410
7483
|
invalidate(login, transition) {
|
|
7411
|
-
if (login.invalidated) return;
|
|
7484
|
+
if (login.invalidated) return true;
|
|
7412
7485
|
login.invalidated = true;
|
|
7413
7486
|
login.abort.abort(/* @__PURE__ */ new Error(`subscription login ${transition}`));
|
|
7414
|
-
this.restoreCredential(login);
|
|
7487
|
+
const restored = this.restoreCredential(login);
|
|
7415
7488
|
login.infoArrived();
|
|
7416
7489
|
this.logger.info({
|
|
7417
|
-
event: "
|
|
7490
|
+
event: "agent-server.subscription_login_transition",
|
|
7418
7491
|
operationId: login.operationId,
|
|
7419
7492
|
providerId: login.providerId,
|
|
7420
7493
|
transition
|
|
7421
7494
|
}, "Subscription login invalidated");
|
|
7495
|
+
return restored;
|
|
7422
7496
|
}
|
|
7423
7497
|
status(providerId) {
|
|
7424
7498
|
this.sweep();
|
|
7425
7499
|
const login = this.logins.get(providerId);
|
|
7426
|
-
if (!login) throw new
|
|
7500
|
+
if (!login) throw new AgentServerSubscriptionError("login_not_found", `no login in progress for "${providerId}"`);
|
|
7427
7501
|
return snapshot(login);
|
|
7428
7502
|
}
|
|
7429
7503
|
/**
|
|
@@ -7433,7 +7507,7 @@ var ProviderLoginService = class {
|
|
|
7433
7507
|
*/
|
|
7434
7508
|
async start(providerId) {
|
|
7435
7509
|
this.sweep();
|
|
7436
|
-
if (!this.providers().some((provider) => provider.id === providerId)) throw new
|
|
7510
|
+
if (!this.providers().some((provider) => provider.id === providerId)) throw new AgentServerSubscriptionError("provider_unknown", `"${providerId}" is not a known subscription provider`);
|
|
7437
7511
|
const existing = this.logins.get(providerId);
|
|
7438
7512
|
if (existing && existing.status === "pending") return snapshot(existing);
|
|
7439
7513
|
let infoArrived = () => void 0;
|
|
@@ -7449,17 +7523,17 @@ var ProviderLoginService = class {
|
|
|
7449
7523
|
infoArrived,
|
|
7450
7524
|
abort,
|
|
7451
7525
|
invalidated: false,
|
|
7452
|
-
previousCredential: this.
|
|
7526
|
+
previousCredential: readStoredCredential(providerId, this.options.authPath)
|
|
7453
7527
|
};
|
|
7454
7528
|
this.logins.set(providerId, login);
|
|
7455
7529
|
this.logger.info({
|
|
7456
|
-
event: "
|
|
7530
|
+
event: "agent-server.subscription_login_transition",
|
|
7457
7531
|
operationId: login.operationId,
|
|
7458
7532
|
providerId,
|
|
7459
7533
|
transition: "started"
|
|
7460
7534
|
}, "Subscription login started");
|
|
7461
7535
|
const callbacks = createLoginCallbacks(login, this.logger);
|
|
7462
|
-
(this.options.runLogin ?? ((id, loginCallbacks) => this.
|
|
7536
|
+
(this.options.runLogin ?? ((id, loginCallbacks) => this.runtime().login(id, "oauth", toAuthInteraction(loginCallbacks)).then(() => void 0)))(providerId, callbacks).then(() => {
|
|
7463
7537
|
if (login.invalidated) {
|
|
7464
7538
|
this.restoreCredential(login);
|
|
7465
7539
|
return;
|
|
@@ -7468,7 +7542,7 @@ var ProviderLoginService = class {
|
|
|
7468
7542
|
login.status = "failed";
|
|
7469
7543
|
login.error = "Subscription sign-in completed, but credentials were not persisted. Start again to retry.";
|
|
7470
7544
|
this.logger.error({
|
|
7471
|
-
event: "
|
|
7545
|
+
event: "agent-server.subscription_login_transition",
|
|
7472
7546
|
operationId: login.operationId,
|
|
7473
7547
|
providerId,
|
|
7474
7548
|
transition: "persistence_failed"
|
|
@@ -7476,7 +7550,7 @@ var ProviderLoginService = class {
|
|
|
7476
7550
|
} else {
|
|
7477
7551
|
login.status = "completed";
|
|
7478
7552
|
this.logger.info({
|
|
7479
|
-
event: "
|
|
7553
|
+
event: "agent-server.subscription_login_transition",
|
|
7480
7554
|
operationId: login.operationId,
|
|
7481
7555
|
providerId,
|
|
7482
7556
|
transition: "completed"
|
|
@@ -7491,7 +7565,7 @@ var ProviderLoginService = class {
|
|
|
7491
7565
|
login.status = "failed";
|
|
7492
7566
|
login.error = publicLoginError(error);
|
|
7493
7567
|
this.logger.warn({
|
|
7494
|
-
event: "
|
|
7568
|
+
event: "agent-server.subscription_login_transition",
|
|
7495
7569
|
operationId: login.operationId,
|
|
7496
7570
|
providerId,
|
|
7497
7571
|
transition: "failed",
|
|
@@ -7499,16 +7573,24 @@ var ProviderLoginService = class {
|
|
|
7499
7573
|
}, "Subscription login failed");
|
|
7500
7574
|
login.infoArrived();
|
|
7501
7575
|
});
|
|
7502
|
-
await Promise.race([infoPromise, new Promise((resolvePromise) => {
|
|
7503
|
-
setTimeout(resolvePromise, START_INFO_TIMEOUT_MS).unref?.();
|
|
7504
|
-
})])
|
|
7576
|
+
if (!await Promise.race([infoPromise, new Promise((resolvePromise) => {
|
|
7577
|
+
setTimeout(() => resolvePromise(), START_INFO_TIMEOUT_MS).unref?.();
|
|
7578
|
+
})]).then(() => login.authUrl !== void 0 || login.userCode !== void 0 || login.status !== "pending")) {
|
|
7579
|
+
login.waitingForAuthorization = true;
|
|
7580
|
+
this.logger.warn({
|
|
7581
|
+
event: "serve.subscription_login_start_info_timeout",
|
|
7582
|
+
operationId: login.operationId,
|
|
7583
|
+
providerId,
|
|
7584
|
+
timeoutMs: START_INFO_TIMEOUT_MS
|
|
7585
|
+
}, "Subscription login is still waiting for authorization information");
|
|
7586
|
+
}
|
|
7505
7587
|
return snapshot(login);
|
|
7506
7588
|
}
|
|
7507
7589
|
/** Abort an in-flight login and forget it. */
|
|
7508
7590
|
cancel(providerId) {
|
|
7509
7591
|
const login = this.logins.get(providerId);
|
|
7510
|
-
if (!login) throw new
|
|
7511
|
-
this.invalidate(login, "cancelled");
|
|
7592
|
+
if (!login) throw new AgentServerSubscriptionError("login_not_found", `no login in progress for "${providerId}"`);
|
|
7593
|
+
if (!this.invalidate(login, "cancelled")) throw new AgentServerSubscriptionError("login_cleanup_failed", `could not safely cancel login for "${providerId}"; credential cleanup requires intervention`);
|
|
7512
7594
|
this.logins.delete(providerId);
|
|
7513
7595
|
return {
|
|
7514
7596
|
providerId,
|
|
@@ -7517,17 +7599,82 @@ var ProviderLoginService = class {
|
|
|
7517
7599
|
}
|
|
7518
7600
|
/** Abort every pending flow during supervisor shutdown. */
|
|
7519
7601
|
close() {
|
|
7520
|
-
for (const login of this.logins.values()) this.invalidate(login, "shutdown");
|
|
7602
|
+
for (const login of this.logins.values()) if (login.status === "pending") this.invalidate(login, "shutdown");
|
|
7521
7603
|
this.logins.clear();
|
|
7522
7604
|
}
|
|
7523
7605
|
};
|
|
7606
|
+
function toAuthInteraction(callbacks) {
|
|
7607
|
+
return {
|
|
7608
|
+
...callbacks.signal ? { signal: callbacks.signal } : {},
|
|
7609
|
+
notify: (event) => {
|
|
7610
|
+
switch (event.type) {
|
|
7611
|
+
case "auth_url":
|
|
7612
|
+
callbacks.onAuth({
|
|
7613
|
+
url: event.url,
|
|
7614
|
+
...event.instructions ? { instructions: event.instructions } : {}
|
|
7615
|
+
});
|
|
7616
|
+
break;
|
|
7617
|
+
case "device_code":
|
|
7618
|
+
callbacks.onDeviceCode({
|
|
7619
|
+
userCode: event.userCode,
|
|
7620
|
+
verificationUri: event.verificationUri
|
|
7621
|
+
});
|
|
7622
|
+
break;
|
|
7623
|
+
case "info":
|
|
7624
|
+
case "progress":
|
|
7625
|
+
callbacks.onProgress?.(event.message);
|
|
7626
|
+
break;
|
|
7627
|
+
}
|
|
7628
|
+
},
|
|
7629
|
+
prompt: async (prompt) => {
|
|
7630
|
+
if (prompt.type !== "select") return callbacks.onPrompt({ message: prompt.message });
|
|
7631
|
+
const selected = await callbacks.onSelect({
|
|
7632
|
+
message: prompt.message,
|
|
7633
|
+
options: prompt.options.map(({ id, label }) => ({
|
|
7634
|
+
id,
|
|
7635
|
+
label
|
|
7636
|
+
}))
|
|
7637
|
+
});
|
|
7638
|
+
if (!selected) throw new AgentServerSubscriptionError("login_unsupported_prompt", "This provider flow did not offer a supported sign-in method");
|
|
7639
|
+
return selected;
|
|
7640
|
+
}
|
|
7641
|
+
};
|
|
7642
|
+
}
|
|
7643
|
+
function restoreStoredCredential(authPath, providerId, credential) {
|
|
7644
|
+
mkdirSync(dirname(authPath), {
|
|
7645
|
+
recursive: true,
|
|
7646
|
+
mode: 448
|
|
7647
|
+
});
|
|
7648
|
+
try {
|
|
7649
|
+
writeFileSync(authPath, "{}\n", {
|
|
7650
|
+
flag: "wx",
|
|
7651
|
+
mode: 384
|
|
7652
|
+
});
|
|
7653
|
+
} catch (error) {
|
|
7654
|
+
if (error.code !== "EEXIST") throw error;
|
|
7655
|
+
}
|
|
7656
|
+
const release = lockSync(authPath, { realpath: false });
|
|
7657
|
+
const temp = `${authPath}.${randomBytes(6).toString("hex")}.tmp`;
|
|
7658
|
+
try {
|
|
7659
|
+
const parsed = JSON.parse(readFileSync(authPath, "utf8"));
|
|
7660
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error(`Pi credential store is not an object: ${authPath}`);
|
|
7661
|
+
const credentials = parsed;
|
|
7662
|
+
if (credential) credentials[providerId] = credential;
|
|
7663
|
+
else delete credentials[providerId];
|
|
7664
|
+
writeFileSync(temp, `${JSON.stringify(credentials, null, 2)}\n`, { mode: 384 });
|
|
7665
|
+
renameSync(temp, authPath);
|
|
7666
|
+
} finally {
|
|
7667
|
+
rmSync(temp, { force: true });
|
|
7668
|
+
release();
|
|
7669
|
+
}
|
|
7670
|
+
}
|
|
7524
7671
|
function createLoginCallbacks(login, logger) {
|
|
7525
7672
|
return {
|
|
7526
7673
|
onAuth: (info) => {
|
|
7527
7674
|
login.authUrl = info.url;
|
|
7528
7675
|
if (info.instructions) login.instructions = info.instructions;
|
|
7529
7676
|
logger.info({
|
|
7530
|
-
event: "
|
|
7677
|
+
event: "agent-server.subscription_login_transition",
|
|
7531
7678
|
operationId: login.operationId,
|
|
7532
7679
|
providerId: login.providerId,
|
|
7533
7680
|
transition: "authorization_ready"
|
|
@@ -7538,20 +7685,20 @@ function createLoginCallbacks(login, logger) {
|
|
|
7538
7685
|
login.userCode = info.userCode;
|
|
7539
7686
|
login.verificationUri = info.verificationUri;
|
|
7540
7687
|
logger.info({
|
|
7541
|
-
event: "
|
|
7688
|
+
event: "agent-server.subscription_login_transition",
|
|
7542
7689
|
operationId: login.operationId,
|
|
7543
7690
|
providerId: login.providerId,
|
|
7544
7691
|
transition: "device_code_ready"
|
|
7545
7692
|
}, "Subscription device code ready");
|
|
7546
7693
|
login.infoArrived();
|
|
7547
7694
|
},
|
|
7548
|
-
onPrompt: () => Promise.reject(new
|
|
7695
|
+
onPrompt: () => Promise.reject(new AgentServerSubscriptionError("login_unsupported_prompt", "This provider flow needs an interactive prompt; run `pi /login` in a terminal instead")),
|
|
7549
7696
|
onSelect: (prompt) => Promise.resolve((prompt.options.find((option) => /device/i.test(option.id)) ?? prompt.options[0])?.id),
|
|
7550
7697
|
signal: login.abort.signal
|
|
7551
7698
|
};
|
|
7552
7699
|
}
|
|
7553
7700
|
function publicLoginError(error) {
|
|
7554
|
-
if (error instanceof
|
|
7701
|
+
if (error instanceof AgentServerSubscriptionError) return error.message;
|
|
7555
7702
|
return "Subscription sign-in failed. Start again to retry.";
|
|
7556
7703
|
}
|
|
7557
7704
|
function safeLoginError(error) {
|
|
@@ -7561,7 +7708,7 @@ function safeLoginError(error) {
|
|
|
7561
7708
|
return result;
|
|
7562
7709
|
}
|
|
7563
7710
|
function snapshot(login) {
|
|
7564
|
-
const { providerId, status, authUrl, instructions, userCode, verificationUri, error } = login;
|
|
7711
|
+
const { providerId, status, authUrl, instructions, userCode, verificationUri, error, waitingForAuthorization } = login;
|
|
7565
7712
|
return {
|
|
7566
7713
|
providerId,
|
|
7567
7714
|
status,
|
|
@@ -7569,28 +7716,29 @@ function snapshot(login) {
|
|
|
7569
7716
|
...instructions ? { instructions } : {},
|
|
7570
7717
|
...userCode ? { userCode } : {},
|
|
7571
7718
|
...verificationUri ? { verificationUri } : {},
|
|
7572
|
-
...error ? { error } : {}
|
|
7719
|
+
...error ? { error } : {},
|
|
7720
|
+
...waitingForAuthorization ? { waitingForAuthorization } : {}
|
|
7573
7721
|
};
|
|
7574
7722
|
}
|
|
7575
7723
|
var NAME_RE = /^[a-z0-9][a-z0-9_-]{0,63}$/i;
|
|
7576
7724
|
var PROVIDER_ID_RE = /^[a-z0-9][a-z0-9-]{0,63}$/;
|
|
7577
7725
|
function assertStoreName(kind, value) {
|
|
7578
|
-
if (!NAME_RE.test(value)) throw new
|
|
7726
|
+
if (!NAME_RE.test(value)) throw new AgentServerStoreError("invalid_name", `${kind} must match ${NAME_RE.source}`);
|
|
7579
7727
|
return value;
|
|
7580
7728
|
}
|
|
7581
7729
|
function assertProviderId(value) {
|
|
7582
|
-
if (!PROVIDER_ID_RE.test(value)) throw new
|
|
7730
|
+
if (!PROVIDER_ID_RE.test(value)) throw new AgentServerStoreError("invalid_name", `provider id must match ${PROVIDER_ID_RE.source}`);
|
|
7583
7731
|
return value;
|
|
7584
7732
|
}
|
|
7585
|
-
var
|
|
7586
|
-
name = "
|
|
7733
|
+
var AgentServerStoreError = class extends Error {
|
|
7734
|
+
name = "AgentServerStoreError";
|
|
7587
7735
|
constructor(code, message, options) {
|
|
7588
7736
|
super(message, options);
|
|
7589
7737
|
this.code = code;
|
|
7590
7738
|
}
|
|
7591
7739
|
};
|
|
7592
|
-
/** `
|
|
7593
|
-
function
|
|
7740
|
+
/** `MOLTNET_AGENT_SERVER_ROOT` override, else `$XDG_CONFIG_HOME/moltnet`, else `~/.config/moltnet`. */
|
|
7741
|
+
function resolveAgentServerRoot(input) {
|
|
7594
7742
|
const override = input.root?.trim();
|
|
7595
7743
|
if (override) return override;
|
|
7596
7744
|
const xdg = input.xdgConfigHome?.trim();
|
|
@@ -7601,7 +7749,7 @@ function providerEnvName(providerId) {
|
|
|
7601
7749
|
}
|
|
7602
7750
|
function assertProviderEnvName(providerId, value) {
|
|
7603
7751
|
const expected = providerEnvName(providerId);
|
|
7604
|
-
if (value !== expected) throw new
|
|
7752
|
+
if (value !== expected) throw new AgentServerStoreError("invalid_state", `provider envName must be ${expected}`);
|
|
7605
7753
|
return value;
|
|
7606
7754
|
}
|
|
7607
7755
|
function readJson(path) {
|
|
@@ -7610,12 +7758,12 @@ function readJson(path) {
|
|
|
7610
7758
|
raw = readFileSync(path, "utf8");
|
|
7611
7759
|
} catch (cause) {
|
|
7612
7760
|
if (cause.code === "ENOENT") return null;
|
|
7613
|
-
throw new
|
|
7761
|
+
throw new AgentServerStoreError("io_error", `could not read state at ${path}`, { cause });
|
|
7614
7762
|
}
|
|
7615
7763
|
try {
|
|
7616
7764
|
return JSON.parse(raw);
|
|
7617
7765
|
} catch (cause) {
|
|
7618
|
-
throw new
|
|
7766
|
+
throw new AgentServerStoreError("invalid_state", `corrupt JSON at ${path}`, { cause });
|
|
7619
7767
|
}
|
|
7620
7768
|
}
|
|
7621
7769
|
function writeJsonAtomic(path, value) {
|
|
@@ -7630,7 +7778,7 @@ function writeJsonAtomic(path, value) {
|
|
|
7630
7778
|
throw cause;
|
|
7631
7779
|
}
|
|
7632
7780
|
}
|
|
7633
|
-
var
|
|
7781
|
+
var AgentServerStore = class {
|
|
7634
7782
|
root;
|
|
7635
7783
|
agentsDir;
|
|
7636
7784
|
runsDir;
|
|
@@ -7660,23 +7808,23 @@ var ServeStore = class {
|
|
|
7660
7808
|
});
|
|
7661
7809
|
return this;
|
|
7662
7810
|
}
|
|
7663
|
-
get
|
|
7664
|
-
return join(this.root, "
|
|
7811
|
+
get statePath() {
|
|
7812
|
+
return join(this.root, "agent-server.json");
|
|
7665
7813
|
}
|
|
7666
|
-
|
|
7667
|
-
const state = readJson(this.
|
|
7814
|
+
readAgentServerState() {
|
|
7815
|
+
const state = readJson(this.statePath);
|
|
7668
7816
|
if (!state) return {
|
|
7669
7817
|
version: 1,
|
|
7670
7818
|
pendingRegistrations: {},
|
|
7671
7819
|
activations: {}
|
|
7672
7820
|
};
|
|
7673
|
-
if (!isRecord$1(state) || state.version !== 1) throw new
|
|
7674
|
-
if ("pairedOrigins" in state) throw new
|
|
7675
|
-
if (!isRecord$1(state.pendingRegistrations) || !isRecord$1(state.activations)) throw new
|
|
7821
|
+
if (!isRecord$1(state) || state.version !== 1) throw new AgentServerStoreError("invalid_state", `agent-server.json version ${String(isRecord$1(state) ? state.version : void 0)} is not supported`);
|
|
7822
|
+
if ("pairedOrigins" in state) throw new AgentServerStoreError("invalid_state", "agent-server.json uses the obsolete pairing format; clear the unreleased agent server store and reconfigure it");
|
|
7823
|
+
if (!isRecord$1(state.pendingRegistrations) || !isRecord$1(state.activations)) throw new AgentServerStoreError("invalid_state", "agent-server.json is missing the version 1 activation map; clear the unreleased agent server store and reconfigure it");
|
|
7676
7824
|
for (const [alias, activation] of Object.entries(state.activations)) validateActivation(alias, activation);
|
|
7677
7825
|
for (const [alias, registration] of Object.entries(state.pendingRegistrations)) {
|
|
7678
7826
|
assertStoreName("agent name", alias);
|
|
7679
|
-
if (!isRecord$1(registration) || typeof registration.apiUrl !== "string" || registration.apiUrl.length === 0 || typeof registration.createdAt !== "string" || registration.createdAt.length === 0) throw new
|
|
7827
|
+
if (!isRecord$1(registration) || typeof registration.apiUrl !== "string" || registration.apiUrl.length === 0 || typeof registration.createdAt !== "string" || registration.createdAt.length === 0) throw new AgentServerStoreError("invalid_state", `pending registration "${alias}" is not valid`);
|
|
7680
7828
|
}
|
|
7681
7829
|
return {
|
|
7682
7830
|
version: 1,
|
|
@@ -7684,8 +7832,8 @@ var ServeStore = class {
|
|
|
7684
7832
|
activations: state.activations
|
|
7685
7833
|
};
|
|
7686
7834
|
}
|
|
7687
|
-
|
|
7688
|
-
writeJsonAtomic(this.
|
|
7835
|
+
writeAgentServerState(state) {
|
|
7836
|
+
writeJsonAtomic(this.statePath, state);
|
|
7689
7837
|
}
|
|
7690
7838
|
agentPath(name) {
|
|
7691
7839
|
return storeChildPath(this.agentsDir, "agent name", name, ".json");
|
|
@@ -7700,37 +7848,37 @@ var ServeStore = class {
|
|
|
7700
7848
|
rmSync(this.agentPath(alias), { force: true });
|
|
7701
7849
|
}
|
|
7702
7850
|
readActivation(alias) {
|
|
7703
|
-
return this.
|
|
7851
|
+
return this.readAgentServerState().activations[assertStoreName("agent name", alias)] ?? null;
|
|
7704
7852
|
}
|
|
7705
7853
|
hasPendingRegistration(alias) {
|
|
7706
|
-
return Boolean(this.
|
|
7854
|
+
return Boolean(this.readAgentServerState().pendingRegistrations[assertStoreName("agent name", alias)]);
|
|
7707
7855
|
}
|
|
7708
7856
|
reserveRegistration(alias, apiUrl) {
|
|
7709
7857
|
const name = assertStoreName("agent name", alias);
|
|
7710
|
-
const state = this.
|
|
7711
|
-
if (state.activations[name] || state.pendingRegistrations[name]) throw new
|
|
7858
|
+
const state = this.readAgentServerState();
|
|
7859
|
+
if (state.activations[name] || state.pendingRegistrations[name]) throw new AgentServerStoreError("already_exists", `agent "${name}" already exists in the agent server store`);
|
|
7712
7860
|
state.pendingRegistrations[name] = {
|
|
7713
7861
|
apiUrl,
|
|
7714
7862
|
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
7715
7863
|
};
|
|
7716
|
-
this.
|
|
7864
|
+
this.writeAgentServerState(state);
|
|
7717
7865
|
}
|
|
7718
7866
|
clearPendingRegistration(alias) {
|
|
7719
7867
|
const name = assertStoreName("agent name", alias);
|
|
7720
|
-
const state = this.
|
|
7868
|
+
const state = this.readAgentServerState();
|
|
7721
7869
|
delete state.pendingRegistrations[name];
|
|
7722
|
-
this.
|
|
7870
|
+
this.writeAgentServerState(state);
|
|
7723
7871
|
}
|
|
7724
7872
|
writeActivation(activation) {
|
|
7725
7873
|
const alias = assertStoreName("agent name", activation.alias);
|
|
7726
7874
|
validateActivation(alias, activation);
|
|
7727
|
-
const state = this.
|
|
7875
|
+
const state = this.readAgentServerState();
|
|
7728
7876
|
state.activations[alias] = activation;
|
|
7729
7877
|
if (activation.source === "managed") delete state.pendingRegistrations[alias];
|
|
7730
|
-
this.
|
|
7878
|
+
this.writeAgentServerState(state);
|
|
7731
7879
|
}
|
|
7732
7880
|
listActivations() {
|
|
7733
|
-
return Object.values(this.
|
|
7881
|
+
return Object.values(this.readAgentServerState().activations).sort((a, b) => a.alias.localeCompare(b.alias));
|
|
7734
7882
|
}
|
|
7735
7883
|
get providersPath() {
|
|
7736
7884
|
return join(this.root, "providers.json");
|
|
@@ -7760,17 +7908,17 @@ var ServeStore = class {
|
|
|
7760
7908
|
root = realpathSync(this.runsDir);
|
|
7761
7909
|
runDir = realpathSync(this.runDir(id));
|
|
7762
7910
|
} catch (cause) {
|
|
7763
|
-
throw new
|
|
7911
|
+
throw new AgentServerStoreError("io_error", "could not resolve run directory", { cause });
|
|
7764
7912
|
}
|
|
7765
|
-
if (!isStrictDescendant(root, runDir)) throw new
|
|
7913
|
+
if (!isStrictDescendant(root, runDir)) throw new AgentServerStoreError("invalid_state", "run directory escapes its store");
|
|
7766
7914
|
const logPath = join(runDir, "daemon.log");
|
|
7767
7915
|
try {
|
|
7768
|
-
if (lstatSync(logPath).isSymbolicLink()) throw new
|
|
7916
|
+
if (lstatSync(logPath).isSymbolicLink()) throw new AgentServerStoreError("invalid_state", "run log must not be a symbolic link");
|
|
7769
7917
|
const resolvedLog = realpathSync(logPath);
|
|
7770
|
-
if (!isStrictDescendant(runDir, resolvedLog)) throw new
|
|
7918
|
+
if (!isStrictDescendant(runDir, resolvedLog)) throw new AgentServerStoreError("invalid_state", "run log escapes its store");
|
|
7771
7919
|
} catch (cause) {
|
|
7772
|
-
if (cause instanceof
|
|
7773
|
-
if (cause.code !== "ENOENT") throw new
|
|
7920
|
+
if (cause instanceof AgentServerStoreError) throw cause;
|
|
7921
|
+
if (cause.code !== "ENOENT") throw new AgentServerStoreError("io_error", "could not resolve run log", { cause });
|
|
7774
7922
|
}
|
|
7775
7923
|
return logPath;
|
|
7776
7924
|
}
|
|
@@ -7857,7 +8005,7 @@ function storeChildPath(root, kind, value, suffix = "") {
|
|
|
7857
8005
|
const name = assertStoreName(kind, value);
|
|
7858
8006
|
const normalizedRoot = resolve(root);
|
|
7859
8007
|
const candidate = resolve(normalizedRoot, `${name}${suffix}`);
|
|
7860
|
-
if (!isStrictDescendant(normalizedRoot, candidate)) throw new
|
|
8008
|
+
if (!isStrictDescendant(normalizedRoot, candidate)) throw new AgentServerStoreError("invalid_name", `${kind} escapes its store`);
|
|
7861
8009
|
return candidate;
|
|
7862
8010
|
}
|
|
7863
8011
|
function isStrictDescendant(root, candidate) {
|
|
@@ -7866,7 +8014,7 @@ function isStrictDescendant(root, candidate) {
|
|
|
7866
8014
|
}
|
|
7867
8015
|
function validateActivation(alias, value) {
|
|
7868
8016
|
const invalid = () => {
|
|
7869
|
-
throw new
|
|
8017
|
+
throw new AgentServerStoreError("invalid_state", `activation "${alias}" is not a valid version 1 activation`);
|
|
7870
8018
|
};
|
|
7871
8019
|
if (!isRecord$1(value)) invalid();
|
|
7872
8020
|
const activation = value;
|
|
@@ -7884,20 +8032,20 @@ function validateActivation(alias, value) {
|
|
|
7884
8032
|
if (activation.source !== "external" || typeof activation.configPath !== "string" || activation.configPath.length === 0 || typeof activation.configApiUrl !== "string" || activation.configApiUrl.length === 0 || activation.apiUrl !== void 0 && typeof activation.apiUrl !== "string") invalid();
|
|
7885
8033
|
}
|
|
7886
8034
|
//#endregion
|
|
7887
|
-
//#region src/lib/
|
|
8035
|
+
//#region src/lib/agent-server/identity.ts
|
|
7888
8036
|
/**
|
|
7889
|
-
*
|
|
8037
|
+
* AgentServer identity activation (#2061/#1834 boundary).
|
|
7890
8038
|
*
|
|
7891
8039
|
* Managed agent files are canonical `MoltNetConfig` documents. Alias,
|
|
7892
8040
|
* provenance, pinned identity material, and external config paths live only
|
|
7893
|
-
* in the versioned activation map in `
|
|
8041
|
+
* in the versioned activation map in `agent-server.json`. Every attach and run loads
|
|
7894
8042
|
* the current config and verifies it with authenticated `whoami` before use.
|
|
7895
8043
|
*/
|
|
7896
8044
|
var MAX_CONFIG_BYTES = 64 * 1024;
|
|
7897
8045
|
var IDENTITY_OPERATION_TIMEOUT_MS = 15e3;
|
|
7898
8046
|
var pendingAliases = /* @__PURE__ */ new WeakMap();
|
|
7899
|
-
var
|
|
7900
|
-
name = "
|
|
8047
|
+
var AgentServerIdentityError = class extends Error {
|
|
8048
|
+
name = "AgentServerIdentityError";
|
|
7901
8049
|
constructor(code, message, options) {
|
|
7902
8050
|
super(message, options);
|
|
7903
8051
|
this.code = code;
|
|
@@ -7909,13 +8057,13 @@ function reserveAlias(store, alias) {
|
|
|
7909
8057
|
pending = /* @__PURE__ */ new Set();
|
|
7910
8058
|
pendingAliases.set(store, pending);
|
|
7911
8059
|
}
|
|
7912
|
-
if (pending.has(alias) || store.hasPendingRegistration(alias) || store.readActivation(alias) || store.readAgentConfig(alias)) throw new
|
|
8060
|
+
if (pending.has(alias) || store.hasPendingRegistration(alias) || store.readActivation(alias) || store.readAgentConfig(alias)) throw new AgentServerIdentityError("agent_exists", `agent "${alias}" already exists in the agent server store`);
|
|
7913
8061
|
pending.add(alias);
|
|
7914
8062
|
return () => pending.delete(alias);
|
|
7915
8063
|
}
|
|
7916
8064
|
async function createManagedAgent(store, secrets, input, connectAgent = connect) {
|
|
7917
8065
|
const alias = assertStoreName("agent name", input.name);
|
|
7918
|
-
if (!input.enrollmentToken.trim()) throw new
|
|
8066
|
+
if (!input.enrollmentToken.trim()) throw new AgentServerIdentityError("enrollment_required", "an enrollment token from the target team is required — a self-registered agent would be stranded in its own personal team");
|
|
7919
8067
|
const releaseAlias = reserveAlias(store, alias);
|
|
7920
8068
|
let registeredIdentityId;
|
|
7921
8069
|
try {
|
|
@@ -7923,7 +8071,7 @@ async function createManagedAgent(store, secrets, input, connectAgent = connect)
|
|
|
7923
8071
|
try {
|
|
7924
8072
|
apiUrl = requireSecureCredentialApiUrl(input.apiUrl);
|
|
7925
8073
|
} catch (cause) {
|
|
7926
|
-
throw new
|
|
8074
|
+
throw new AgentServerIdentityError("registration_failed", "registration API URL must use HTTPS or HTTP loopback", { cause });
|
|
7927
8075
|
}
|
|
7928
8076
|
store.reserveRegistration(alias, apiUrl);
|
|
7929
8077
|
const result = await register({
|
|
@@ -7932,7 +8080,7 @@ async function createManagedAgent(store, secrets, input, connectAgent = connect)
|
|
|
7932
8080
|
enrollmentToken: input.enrollmentToken,
|
|
7933
8081
|
signal: boundedIdentitySignal(input.signal)
|
|
7934
8082
|
});
|
|
7935
|
-
if (result.credentials.type !== "agent_key") throw new
|
|
8083
|
+
if (result.credentials.type !== "agent_key") throw new AgentServerIdentityError("unsupported_credential", `registration returned credential type "${result.credentials.type}"; agent server manages agent-key credentials only`);
|
|
7936
8084
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
7937
8085
|
const { identityId, fingerprint, publicKey, privateKey } = result.identity;
|
|
7938
8086
|
registeredIdentityId = identityId;
|
|
@@ -7990,9 +8138,9 @@ async function createManagedAgent(store, secrets, input, connectAgent = connect)
|
|
|
7990
8138
|
} catch (cause) {
|
|
7991
8139
|
if (!registeredIdentityId && cause instanceof MoltNetError && cause.statusCode !== void 0 && cause.statusCode >= 400 && cause.statusCode < 500) {
|
|
7992
8140
|
store.clearPendingRegistration(alias);
|
|
7993
|
-
throw new
|
|
8141
|
+
throw new AgentServerIdentityError("registration_failed", `registration for "${alias}" was rejected`, { cause });
|
|
7994
8142
|
}
|
|
7995
|
-
if (store.hasPendingRegistration(alias)) throw new
|
|
8143
|
+
if (store.hasPendingRegistration(alias)) throw new AgentServerIdentityError("registration_incomplete", registeredIdentityId ? `identity "${registeredIdentityId}" was registered but local activation is incomplete; reconcile or clear its pending Agent Server record before retrying` : `registration for "${alias}" may be incomplete; inspect the remote API before changing its pending Agent Server record`, { cause });
|
|
7996
8144
|
throw cause;
|
|
7997
8145
|
} finally {
|
|
7998
8146
|
releaseAlias();
|
|
@@ -8004,22 +8152,22 @@ async function reconcileManagedRegistration(store, secrets, aliasInput, action,
|
|
|
8004
8152
|
const activation = store.readActivation(alias);
|
|
8005
8153
|
if (activation) {
|
|
8006
8154
|
const config = store.readAgentConfig(alias);
|
|
8007
|
-
if (!config) throw new
|
|
8155
|
+
if (!config) throw new AgentServerIdentityError("registration_incomplete", `managed activation for "${alias}" is missing its canonical config`);
|
|
8008
8156
|
return {
|
|
8009
8157
|
activation,
|
|
8010
8158
|
config
|
|
8011
8159
|
};
|
|
8012
8160
|
}
|
|
8013
|
-
if (!store.hasPendingRegistration(alias)) throw new
|
|
8161
|
+
if (!store.hasPendingRegistration(alias)) throw new AgentServerIdentityError("config_not_found", `agent "${alias}" has no pending registration to reconcile`);
|
|
8014
8162
|
const config = store.readAgentConfig(alias);
|
|
8015
8163
|
if (action === "abandon") {
|
|
8016
8164
|
store.removeAgentConfig(alias);
|
|
8017
8165
|
store.clearPendingRegistration(alias);
|
|
8018
8166
|
return null;
|
|
8019
8167
|
}
|
|
8020
|
-
if (!config?.agent_key_ref || config.agent_key_ref.provider !== FILE_SECRET_PROVIDER || config.keys.private_key_ref?.provider !== FILE_SECRET_PROVIDER) throw new
|
|
8168
|
+
if (!config?.agent_key_ref || config.agent_key_ref.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`);
|
|
8021
8169
|
const [agentKey, privateKeyState] = await Promise.all([secrets.read(config.agent_key_ref.key), secrets.probe(config.keys.private_key_ref.key)]);
|
|
8022
|
-
if (!agentKey || privateKeyState !== "present") throw new
|
|
8170
|
+
if (!agentKey || privateKeyState !== "present") throw new AgentServerIdentityError("registration_incomplete", `pending registration for "${alias}" is missing persisted secret material`);
|
|
8023
8171
|
const identity = identityFromConfig(config);
|
|
8024
8172
|
const apiUrl = requireConfigApiUrl(config, store.agentPath(alias));
|
|
8025
8173
|
const whoami = await callWhoami(connectAgent, {
|
|
@@ -8047,7 +8195,7 @@ async function attachExternalAgent(store, secretProviders, input, connectAgent =
|
|
|
8047
8195
|
const alias = assertStoreName("agent name", input.name);
|
|
8048
8196
|
const releaseAlias = reserveAlias(store, alias);
|
|
8049
8197
|
try {
|
|
8050
|
-
if (!isAbsolute(input.configDir)) throw new
|
|
8198
|
+
if (!isAbsolute(input.configDir)) throw new AgentServerIdentityError("config_not_found", "external configDir must be an absolute path");
|
|
8051
8199
|
const configPath = join(input.configDir, "moltnet.json");
|
|
8052
8200
|
externalAgentLocation(configPath);
|
|
8053
8201
|
const config = await readCurrentConfig(configPath);
|
|
@@ -8083,7 +8231,7 @@ async function verifyAgentActivation(store, alias, managedSecretProviders, exter
|
|
|
8083
8231
|
const verified = activation.source === "managed" ? await verifyManagedActivation(store, activation, managedSecretProviders, connectAgent, signal) : await verifyExternalActivation(activation, externalSecretProviders, connectAgent, signal);
|
|
8084
8232
|
assertIdentityMatches(verified.whoami, activation, "authenticated whoami", `agent "${activation.alias}" pinned activation`);
|
|
8085
8233
|
const boundTeamId = boundTeamIdFromWhoami(verified.whoami);
|
|
8086
|
-
if (activation.boundTeamId !== boundTeamId) throw new
|
|
8234
|
+
if (activation.boundTeamId !== boundTeamId) throw new AgentServerIdentityError("verification_failed", `authenticated whoami team binding does not match agent "${activation.alias}" pinned activation`);
|
|
8087
8235
|
return {
|
|
8088
8236
|
activation,
|
|
8089
8237
|
config: verified.config,
|
|
@@ -8100,7 +8248,7 @@ async function verifyManagedActivation(store, activation, secretProviders, conne
|
|
|
8100
8248
|
} catch (cause) {
|
|
8101
8249
|
throw verificationError(`could not resolve the managed agent key for "${activation.alias}"`, cause);
|
|
8102
8250
|
}
|
|
8103
|
-
if (!agentKey) throw new
|
|
8251
|
+
if (!agentKey) throw new AgentServerIdentityError("verification_failed", `managed config for "${activation.alias}" has no agent_key_ref`);
|
|
8104
8252
|
return {
|
|
8105
8253
|
config,
|
|
8106
8254
|
whoami: await callWhoami(connectAgent, {
|
|
@@ -8123,16 +8271,16 @@ async function verifyExternalActivation(activation, secretProviders, connectAgen
|
|
|
8123
8271
|
/** Never let request or activation metadata redirect persisted credentials. */
|
|
8124
8272
|
function requireTrustedApiOverride(override, configApiUrl, configPath) {
|
|
8125
8273
|
if (!override) return configApiUrl;
|
|
8126
|
-
if (override !== configApiUrl) throw new
|
|
8274
|
+
if (override !== configApiUrl) throw new AgentServerIdentityError("verification_failed", `API override for ${configPath} does not match its configured endpoint`);
|
|
8127
8275
|
return configApiUrl;
|
|
8128
8276
|
}
|
|
8129
8277
|
function assertActivatedConfig(config, activation, configPath, currentApiUrl, pinnedApiUrl) {
|
|
8130
|
-
if (currentApiUrl !== pinnedApiUrl) throw new
|
|
8278
|
+
if (currentApiUrl !== pinnedApiUrl) throw new AgentServerIdentityError("verification_failed", `agent config at ${configPath} API endpoint does not match its pinned activation`);
|
|
8131
8279
|
assertIdentityMatches(identityFromConfig(config), activation, configPath, `agent "${activation.alias}" pinned activation`);
|
|
8132
8280
|
}
|
|
8133
8281
|
function requireConfigApiUrl(config, configPath) {
|
|
8134
8282
|
const apiUrl = config?.endpoints?.api?.trim();
|
|
8135
|
-
if (!apiUrl) throw new
|
|
8283
|
+
if (!apiUrl) throw new AgentServerIdentityError("verification_failed", `agent config at ${configPath} is missing endpoints.api`);
|
|
8136
8284
|
return apiUrl;
|
|
8137
8285
|
}
|
|
8138
8286
|
function requireTrustedConfigApiUrl(config, configPath) {
|
|
@@ -8149,14 +8297,14 @@ async function readCurrentConfig(configPath) {
|
|
|
8149
8297
|
try {
|
|
8150
8298
|
file = await open(configPath, "r");
|
|
8151
8299
|
} catch (cause) {
|
|
8152
|
-
throw new
|
|
8300
|
+
throw new AgentServerIdentityError("config_not_found", `agent config is missing at ${configPath}`, { cause });
|
|
8153
8301
|
}
|
|
8154
8302
|
let raw;
|
|
8155
8303
|
try {
|
|
8156
|
-
if (!(await file.stat()).isFile()) throw new
|
|
8304
|
+
if (!(await file.stat()).isFile()) throw new AgentServerIdentityError("verification_failed", `agent config at ${configPath} must be a regular file no larger than ${MAX_CONFIG_BYTES} bytes`);
|
|
8157
8305
|
const buffer = Buffer.alloc(MAX_CONFIG_BYTES + 1);
|
|
8158
8306
|
const { bytesRead } = await file.read(buffer, 0, buffer.length, 0);
|
|
8159
|
-
if (bytesRead > MAX_CONFIG_BYTES) throw new
|
|
8307
|
+
if (bytesRead > MAX_CONFIG_BYTES) throw new AgentServerIdentityError("verification_failed", `agent config at ${configPath} must be a regular file no larger than ${MAX_CONFIG_BYTES} bytes`);
|
|
8160
8308
|
raw = buffer.toString("utf8", 0, bytesRead);
|
|
8161
8309
|
} finally {
|
|
8162
8310
|
await file.close();
|
|
@@ -8171,7 +8319,7 @@ async function readCurrentConfig(configPath) {
|
|
|
8171
8319
|
function externalAgentLocation(configPath) {
|
|
8172
8320
|
const configDir = dirname(configPath);
|
|
8173
8321
|
const moltnetDir = dirname(configDir);
|
|
8174
|
-
if (!isAbsolute(configPath) || basename(configPath) !== "moltnet.json" || basename(moltnetDir) !== ".moltnet") throw new
|
|
8322
|
+
if (!isAbsolute(configPath) || basename(configPath) !== "moltnet.json" || basename(moltnetDir) !== ".moltnet") throw new AgentServerIdentityError("config_not_found", `external config must be at an absolute <agent-root>/.moltnet/<agent>/moltnet.json path`);
|
|
8175
8323
|
return {
|
|
8176
8324
|
agentName: assertStoreName("external config agent name", basename(configDir)),
|
|
8177
8325
|
agentRoot: dirname(moltnetDir)
|
|
@@ -8203,7 +8351,7 @@ function identityFromConfig(config) {
|
|
|
8203
8351
|
const identityId = config?.identity_id?.trim();
|
|
8204
8352
|
const publicKey = config?.keys?.public_key?.trim();
|
|
8205
8353
|
const fingerprint = config?.keys?.fingerprint?.trim();
|
|
8206
|
-
if (!identityId || !publicKey || !fingerprint) throw new
|
|
8354
|
+
if (!identityId || !publicKey || !fingerprint) throw new AgentServerIdentityError("verification_failed", "agent config is missing canonical identity_id, keys.public_key, or keys.fingerprint");
|
|
8207
8355
|
return {
|
|
8208
8356
|
identityId,
|
|
8209
8357
|
publicKey,
|
|
@@ -8212,10 +8360,10 @@ function identityFromConfig(config) {
|
|
|
8212
8360
|
}
|
|
8213
8361
|
function assertIdentityMatches(current, expected, currentLabel, expectedLabel) {
|
|
8214
8362
|
const assessment = assessIdentityPin(current, expected);
|
|
8215
|
-
if (!assessment.ok) throw new
|
|
8363
|
+
if (!assessment.ok) throw new AgentServerIdentityError("verification_failed", `${currentLabel} ${assessment.label} does not match ${expectedLabel}`);
|
|
8216
8364
|
}
|
|
8217
8365
|
function verificationError(message, cause) {
|
|
8218
|
-
return new
|
|
8366
|
+
return new AgentServerIdentityError("verification_failed", message, { cause });
|
|
8219
8367
|
}
|
|
8220
8368
|
/** Non-secret projection preserving the existing `/v1` response shape. */
|
|
8221
8369
|
function publicAgentView(store, activation) {
|
|
@@ -8249,11 +8397,11 @@ function boundTeamIdFromWhoami(whoami) {
|
|
|
8249
8397
|
}
|
|
8250
8398
|
function requireActivation(store, alias) {
|
|
8251
8399
|
const activation = store.readActivation(alias);
|
|
8252
|
-
if (!activation) throw new
|
|
8400
|
+
if (!activation) throw new AgentServerStoreError("not_found", `agent "${alias}" is not configured`);
|
|
8253
8401
|
return activation;
|
|
8254
8402
|
}
|
|
8255
8403
|
//#endregion
|
|
8256
|
-
//#region src/lib/
|
|
8404
|
+
//#region src/lib/agent-server/runs.ts
|
|
8257
8405
|
var STOP_GRACE_MS = 1e4;
|
|
8258
8406
|
var STOP_FORCE_MS = 2e3;
|
|
8259
8407
|
var DEFAULT_MAX_LOG_BYTES = 10 * 1024 * 1024;
|
|
@@ -8292,19 +8440,19 @@ var INHERITED_MOLTNET_ENV_NAMES = new Set([
|
|
|
8292
8440
|
"MOLTNET_SIGNER_URL",
|
|
8293
8441
|
"MOLTNET_TRACE_IDLE_POLLING"
|
|
8294
8442
|
]);
|
|
8295
|
-
var
|
|
8296
|
-
name = "
|
|
8443
|
+
var AgentServerRunError = class extends Error {
|
|
8444
|
+
name = "AgentServerRunError";
|
|
8297
8445
|
constructor(code, message) {
|
|
8298
8446
|
super(message);
|
|
8299
8447
|
this.code = code;
|
|
8300
8448
|
}
|
|
8301
8449
|
};
|
|
8302
8450
|
function validateRunSpec(spec) {
|
|
8303
|
-
if (spec.mode !== "poll" && spec.mode !== "drain") throw new
|
|
8304
|
-
if (!spec.agent) throw new
|
|
8305
|
-
if (!spec.teamId) throw new
|
|
8306
|
-
if (!Array.isArray(spec.profiles) || spec.profiles.length === 0) throw new
|
|
8307
|
-
if (!Array.isArray(spec.taskTypes) || spec.taskTypes.length === 0) throw new
|
|
8451
|
+
if (spec.mode !== "poll" && spec.mode !== "drain") throw new AgentServerRunError("invalid_spec", "mode must be poll or drain");
|
|
8452
|
+
if (!spec.agent) throw new AgentServerRunError("invalid_spec", "agent is required");
|
|
8453
|
+
if (!spec.teamId) throw new AgentServerRunError("invalid_spec", "teamId is required");
|
|
8454
|
+
if (!Array.isArray(spec.profiles) || spec.profiles.length === 0) throw new AgentServerRunError("invalid_spec", "at least one profile is required");
|
|
8455
|
+
if (!Array.isArray(spec.taskTypes) || spec.taskTypes.length === 0) throw new AgentServerRunError("invalid_spec", "at least one task type is required");
|
|
8308
8456
|
}
|
|
8309
8457
|
var RunManager = class {
|
|
8310
8458
|
active = /* @__PURE__ */ new Map();
|
|
@@ -8362,7 +8510,7 @@ var RunManager = class {
|
|
|
8362
8510
|
...target.extraArgs
|
|
8363
8511
|
];
|
|
8364
8512
|
if (activation.source === "managed") {
|
|
8365
|
-
if (!config.agent_key_ref || !config.keys.private_key_ref) throw new
|
|
8513
|
+
if (!config.agent_key_ref || !config.keys.private_key_ref) throw new AgentServerRunError("invalid_spec", `managed config for "${activation.alias}" is missing canonical secret references`);
|
|
8366
8514
|
env["MOLTNET_API_URL"] = activation.apiUrl;
|
|
8367
8515
|
env["MOLTNET_AGENT_KEY_REF"] = formatSecretReferenceString(config.agent_key_ref);
|
|
8368
8516
|
env["MOLTNET_PRIVATE_KEY_REF"] = formatSecretReferenceString(config.keys.private_key_ref);
|
|
@@ -8380,7 +8528,7 @@ var RunManager = class {
|
|
|
8380
8528
|
}
|
|
8381
8529
|
if (!agentKey && config.keys.private_key_ref) env["MOLTNET_PRIVATE_KEY"] = await resolveIdentitySeed(config, this.options.externalSecretProviders);
|
|
8382
8530
|
} catch {
|
|
8383
|
-
throw new
|
|
8531
|
+
throw new AgentServerRunError("invalid_spec", `external credentials for "${activation.alias}" could not be projected`);
|
|
8384
8532
|
}
|
|
8385
8533
|
}
|
|
8386
8534
|
env["MOLTNET_EXPECTED_IDENTITY_ID"] = activation.identityId;
|
|
@@ -8393,7 +8541,7 @@ var RunManager = class {
|
|
|
8393
8541
|
try {
|
|
8394
8542
|
value = await this.options.secretProviders.resolve(parseSecretReferenceString(provider.apiKeyRef));
|
|
8395
8543
|
} catch {
|
|
8396
|
-
throw new
|
|
8544
|
+
throw new AgentServerRunError("invalid_spec", `provider "${providerId}" API key could not be resolved`);
|
|
8397
8545
|
}
|
|
8398
8546
|
env[provider.envName] = value;
|
|
8399
8547
|
}
|
|
@@ -8416,7 +8564,7 @@ var RunManager = class {
|
|
|
8416
8564
|
this.assertStartOpen(signal);
|
|
8417
8565
|
const agent = await (this.options.verifyActivationImpl ?? verifyAgentActivation)(this.store, spec.agent, this.options.secretProviders, this.options.externalSecretProviders, void 0, signal);
|
|
8418
8566
|
this.assertStartOpen(signal);
|
|
8419
|
-
if (agent.boundTeamId && agent.boundTeamId !== spec.teamId) throw new
|
|
8567
|
+
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}`);
|
|
8420
8568
|
const id = `${Date.now().toString(36)}-${randomBytes(4).toString("hex")}`;
|
|
8421
8569
|
const runDir = this.store.runDir(id);
|
|
8422
8570
|
const piDir = join(runDir, "pi");
|
|
@@ -8449,20 +8597,20 @@ var RunManager = class {
|
|
|
8449
8597
|
try {
|
|
8450
8598
|
(this.options.symlinkImpl ?? symlinkSync)(this.store.piAuthJsonPath, join(piDir, "auth.json"));
|
|
8451
8599
|
} catch (cause) {
|
|
8452
|
-
throw new
|
|
8600
|
+
throw new AgentServerStoreError("io_error", "could not link subscription credentials into the run", { cause });
|
|
8453
8601
|
}
|
|
8454
8602
|
const entry = this.entrypoint();
|
|
8455
8603
|
logStream = createWriteStream(logPath, {
|
|
8456
8604
|
fd: openSync(logPath, "a", 384),
|
|
8457
8605
|
autoClose: true
|
|
8458
8606
|
});
|
|
8459
|
-
logLimiter = createByteLimitTransform(this.options.maxLogBytes ?? DEFAULT_MAX_LOG_BYTES, () => this.log("warn", "
|
|
8607
|
+
logLimiter = createByteLimitTransform(this.options.maxLogBytes ?? DEFAULT_MAX_LOG_BYTES, () => this.log("warn", "agent server run log output truncated", {
|
|
8460
8608
|
...runContext(id, spec.agent, child),
|
|
8461
8609
|
transition: "log_truncated"
|
|
8462
8610
|
}));
|
|
8463
8611
|
logLimiter.pipe(logStream);
|
|
8464
8612
|
logStream.on("error", (error) => {
|
|
8465
|
-
this.log("error", "
|
|
8613
|
+
this.log("error", "agent server run log stream failed", {
|
|
8466
8614
|
...runContext(id, spec.agent, child),
|
|
8467
8615
|
transition: "log_failed",
|
|
8468
8616
|
...safeRunError(error)
|
|
@@ -8508,7 +8656,7 @@ var RunManager = class {
|
|
|
8508
8656
|
this.active.delete(id);
|
|
8509
8657
|
logLimiter?.end();
|
|
8510
8658
|
const status = activeRun?.stopRequested ? "stopped" : code === 0 ? "exited" : "failed";
|
|
8511
|
-
this.log(status === "failed" ? "error" : "info", "
|
|
8659
|
+
this.log(status === "failed" ? "error" : "info", "agent server run exited", {
|
|
8512
8660
|
...runContext(id, spec.agent, child),
|
|
8513
8661
|
transition: status,
|
|
8514
8662
|
exitCode: code,
|
|
@@ -8522,7 +8670,7 @@ var RunManager = class {
|
|
|
8522
8670
|
child.once("error", (error) => {
|
|
8523
8671
|
this.active.delete(id);
|
|
8524
8672
|
logLimiter?.end();
|
|
8525
|
-
this.log("error", "
|
|
8673
|
+
this.log("error", "agent server run child process failed", {
|
|
8526
8674
|
...runContext(id, spec.agent, child),
|
|
8527
8675
|
transition: "spawn_failed",
|
|
8528
8676
|
...safeRunError(error)
|
|
@@ -8530,7 +8678,7 @@ var RunManager = class {
|
|
|
8530
8678
|
this.persistRunCompletion(id, spec.agent, { status: "failed" });
|
|
8531
8679
|
});
|
|
8532
8680
|
this.store.writeRun(record);
|
|
8533
|
-
this.log("info", "
|
|
8681
|
+
this.log("info", "agent server run started", {
|
|
8534
8682
|
...runContext(id, spec.agent, child),
|
|
8535
8683
|
transition: "running"
|
|
8536
8684
|
});
|
|
@@ -8544,7 +8692,7 @@ var RunManager = class {
|
|
|
8544
8692
|
recursive: true,
|
|
8545
8693
|
force: true
|
|
8546
8694
|
});
|
|
8547
|
-
this.log("error", "
|
|
8695
|
+
this.log("error", "agent server run failed to start", {
|
|
8548
8696
|
...runContext(id, spec.agent, child),
|
|
8549
8697
|
transition: "start_failed",
|
|
8550
8698
|
...safeRunError(cause)
|
|
@@ -8554,11 +8702,11 @@ var RunManager = class {
|
|
|
8554
8702
|
}
|
|
8555
8703
|
stop(id) {
|
|
8556
8704
|
const record = this.store.readRun(id);
|
|
8557
|
-
if (!record) throw new
|
|
8705
|
+
if (!record) throw new AgentServerRunError("run_not_found", `run "${id}" was not found`);
|
|
8558
8706
|
const activeRun = this.active.get(id);
|
|
8559
|
-
if (!activeRun) throw new
|
|
8707
|
+
if (!activeRun) throw new AgentServerRunError("run_not_active", `run "${id}" is not running`);
|
|
8560
8708
|
activeRun.stopRequested = true;
|
|
8561
|
-
this.log("info", "
|
|
8709
|
+
this.log("info", "agent server run stop requested", {
|
|
8562
8710
|
...runContext(id, record.agent, activeRun.child),
|
|
8563
8711
|
transition: "stopping",
|
|
8564
8712
|
signal: "SIGTERM"
|
|
@@ -8572,7 +8720,7 @@ var RunManager = class {
|
|
|
8572
8720
|
/** Live status merged over the persisted record. */
|
|
8573
8721
|
status(id) {
|
|
8574
8722
|
const record = this.store.readRun(id);
|
|
8575
|
-
if (!record) throw new
|
|
8723
|
+
if (!record) throw new AgentServerStoreError("not_found", `run "${id}" was not found`);
|
|
8576
8724
|
return record;
|
|
8577
8725
|
}
|
|
8578
8726
|
list(limit = Number.POSITIVE_INFINITY) {
|
|
@@ -8620,7 +8768,7 @@ var RunManager = class {
|
|
|
8620
8768
|
this.closing = true;
|
|
8621
8769
|
for (const [id, activeRun] of this.active) {
|
|
8622
8770
|
activeRun.stopRequested = true;
|
|
8623
|
-
this.log("warn", "
|
|
8771
|
+
this.log("warn", "agent server run force stop requested", {
|
|
8624
8772
|
...runContext(id, this.store.readRun(id)?.agent ?? "unknown", activeRun.child),
|
|
8625
8773
|
transition: "force_stopping",
|
|
8626
8774
|
signal: "SIGKILL"
|
|
@@ -8638,7 +8786,7 @@ var RunManager = class {
|
|
|
8638
8786
|
exitCode: null,
|
|
8639
8787
|
endedAt
|
|
8640
8788
|
});
|
|
8641
|
-
this.log("warn", "
|
|
8789
|
+
this.log("warn", "agent server run interrupted by supervisor replacement", {
|
|
8642
8790
|
runId: record.id,
|
|
8643
8791
|
agent: record.agent,
|
|
8644
8792
|
pid: record.pid,
|
|
@@ -8647,10 +8795,10 @@ var RunManager = class {
|
|
|
8647
8795
|
}
|
|
8648
8796
|
}
|
|
8649
8797
|
reserveStart(agent) {
|
|
8650
|
-
if (this.closing) throw new
|
|
8798
|
+
if (this.closing) throw new AgentServerRunError("invalid_spec", "agent server is shutting down");
|
|
8651
8799
|
const agentStarting = this.startingByAgent.get(agent) ?? 0;
|
|
8652
8800
|
const agentActive = [...this.active.values()].filter((run) => run.agent === agent).length;
|
|
8653
|
-
if (this.active.size + this.starting >= (this.options.maxActiveRuns ?? DEFAULT_MAX_ACTIVE_RUNS) || agentActive + agentStarting >= (this.options.maxActiveRunsPerAgent ?? DEFAULT_MAX_ACTIVE_RUNS_PER_AGENT)) throw new
|
|
8801
|
+
if (this.active.size + this.starting >= (this.options.maxActiveRuns ?? DEFAULT_MAX_ACTIVE_RUNS) || agentActive + agentStarting >= (this.options.maxActiveRunsPerAgent ?? DEFAULT_MAX_ACTIVE_RUNS_PER_AGENT)) throw new AgentServerRunError("invalid_spec", `active run limit reached for agent "${agent}"`);
|
|
8654
8802
|
this.starting += 1;
|
|
8655
8803
|
this.startingByAgent.set(agent, agentStarting + 1);
|
|
8656
8804
|
return () => {
|
|
@@ -8661,7 +8809,7 @@ var RunManager = class {
|
|
|
8661
8809
|
};
|
|
8662
8810
|
}
|
|
8663
8811
|
assertStartOpen(signal) {
|
|
8664
|
-
if (this.closing || signal?.aborted) throw new
|
|
8812
|
+
if (this.closing || signal?.aborted) throw new AgentServerRunError("invalid_spec", "agent server is shutting down");
|
|
8665
8813
|
}
|
|
8666
8814
|
pruneCompletedRuns() {
|
|
8667
8815
|
const removed = this.store.pruneCompletedRuns({
|
|
@@ -8670,7 +8818,7 @@ var RunManager = class {
|
|
|
8670
8818
|
maxBytes: this.options.maxRunStorageBytes ?? DEFAULT_MAX_RUN_STORAGE_BYTES,
|
|
8671
8819
|
now: this.options.now?.() ?? /* @__PURE__ */ new Date()
|
|
8672
8820
|
});
|
|
8673
|
-
if (removed.length > 0) this.log("info", "
|
|
8821
|
+
if (removed.length > 0) this.log("info", "agent server run retention removed completed artifacts", {
|
|
8674
8822
|
transition: "pruned",
|
|
8675
8823
|
removedCount: removed.length
|
|
8676
8824
|
});
|
|
@@ -8685,7 +8833,7 @@ var RunManager = class {
|
|
|
8685
8833
|
});
|
|
8686
8834
|
this.pruneCompletedRuns();
|
|
8687
8835
|
} catch (error) {
|
|
8688
|
-
this.log("error", "
|
|
8836
|
+
this.log("error", "agent server run completion persistence failed", {
|
|
8689
8837
|
runId: id,
|
|
8690
8838
|
agent,
|
|
8691
8839
|
transition: "persistence_failed",
|
|
@@ -8749,64 +8897,8 @@ async function terminateChild(child) {
|
|
|
8749
8897
|
child.kill("SIGKILL");
|
|
8750
8898
|
});
|
|
8751
8899
|
}
|
|
8752
|
-
|
|
8753
|
-
|
|
8754
|
-
var ServeLockError = class extends Error {
|
|
8755
|
-
name = "ServeLockError";
|
|
8756
|
-
constructor(code, message, options) {
|
|
8757
|
-
super(message, options);
|
|
8758
|
-
this.code = code;
|
|
8759
|
-
}
|
|
8760
|
-
};
|
|
8761
|
-
/**
|
|
8762
|
-
* Acquire the per-root singleton lock. `proper-lockfile` uses an atomic lock
|
|
8763
|
-
* directory at exactly `<root>/serve.lock`, recovers stale owners, and keeps
|
|
8764
|
-
* the mtime fresh while the supervisor is alive.
|
|
8765
|
-
*/
|
|
8766
|
-
async function acquireServeLock(root, options = {}) {
|
|
8767
|
-
const path = join(root, "serve.lock");
|
|
8768
|
-
let releaseLock;
|
|
8769
|
-
try {
|
|
8770
|
-
releaseLock = await lock(root, {
|
|
8771
|
-
lockfilePath: path,
|
|
8772
|
-
realpath: false,
|
|
8773
|
-
retries: 0,
|
|
8774
|
-
...options.staleMs === void 0 ? {} : { stale: options.staleMs },
|
|
8775
|
-
...options.updateMs === void 0 ? {} : { update: options.updateMs },
|
|
8776
|
-
onCompromised: (cause) => {
|
|
8777
|
-
const error = new ServeLockError("compromised", `serve lock ${path} was compromised: ${cause.message}`, { cause });
|
|
8778
|
-
if (options.onCompromised) {
|
|
8779
|
-
options.onCompromised(error);
|
|
8780
|
-
return;
|
|
8781
|
-
}
|
|
8782
|
-
throw error;
|
|
8783
|
-
}
|
|
8784
|
-
});
|
|
8785
|
-
} catch (cause) {
|
|
8786
|
-
if (cause.code === "ELOCKED") throw new ServeLockError("held", `another moltnet-agent serve process already owns ${path}`, { cause });
|
|
8787
|
-
throw new ServeLockError("failed", `could not acquire serve lock ${path}: ${cause.message}`, { cause });
|
|
8788
|
-
}
|
|
8789
|
-
let released = false;
|
|
8790
|
-
return {
|
|
8791
|
-
path,
|
|
8792
|
-
async release() {
|
|
8793
|
-
if (released) return;
|
|
8794
|
-
released = true;
|
|
8795
|
-
await releaseLock();
|
|
8796
|
-
}
|
|
8797
|
-
};
|
|
8798
|
-
}
|
|
8799
|
-
/** Always release after normal completion or startup/runtime failure. */
|
|
8800
|
-
async function withServeLock(root, work, options) {
|
|
8801
|
-
const held = await acquireServeLock(root, options);
|
|
8802
|
-
try {
|
|
8803
|
-
return await work();
|
|
8804
|
-
} finally {
|
|
8805
|
-
await held.release();
|
|
8806
|
-
}
|
|
8807
|
-
}
|
|
8808
|
-
var ServeModelDiscoveryError = class extends Error {
|
|
8809
|
-
name = "ServeModelDiscoveryError";
|
|
8900
|
+
var AgentServerModelDiscoveryError = class extends Error {
|
|
8901
|
+
name = "AgentServerModelDiscoveryError";
|
|
8810
8902
|
constructor(code, message, statusCode, options) {
|
|
8811
8903
|
super(message, options);
|
|
8812
8904
|
this.code = code;
|
|
@@ -8847,32 +8939,352 @@ function parseProviderBaseUrl(value, providerId) {
|
|
|
8847
8939
|
try {
|
|
8848
8940
|
parsed = new URL(value);
|
|
8849
8941
|
} catch (cause) {
|
|
8850
|
-
throw new
|
|
8942
|
+
throw new AgentServerModelDiscoveryError("invalid_provider", `provider "${providerId}" has an invalid base URL`, 400, { cause });
|
|
8851
8943
|
}
|
|
8852
|
-
if (parsed.protocol !== "http:" && parsed.protocol !== "https:" || parsed.username || parsed.password || parsed.search || parsed.hash) throw new
|
|
8944
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:" || parsed.username || parsed.password || parsed.search || parsed.hash) throw new AgentServerModelDiscoveryError("invalid_provider", `provider "${providerId}" base URL must be HTTP(S) without credentials, query, or fragment`, 400);
|
|
8945
|
+
if (isNonLoopbackPrivateAddress(parsed.hostname)) throw new AgentServerModelDiscoveryError("invalid_provider", `provider "${providerId}" base URL must not target a private network address`, 400);
|
|
8853
8946
|
return parsed;
|
|
8854
8947
|
}
|
|
8948
|
+
function isNonLoopbackPrivateAddress(hostname) {
|
|
8949
|
+
if (isIP(hostname) !== 4) return false;
|
|
8950
|
+
const [first, second] = hostname.split(".").map(Number);
|
|
8951
|
+
if (first === 127) return false;
|
|
8952
|
+
return first === 10 || first === 172 && second >= 16 && second <= 31 || first === 192 && second === 168 || first === 169 && second === 254;
|
|
8953
|
+
}
|
|
8855
8954
|
function discoveryFailure(providerId, failures) {
|
|
8856
|
-
if (failures.some((failure) => failure.kind === "http" && (failure.status === 401 || failure.status === 403))) return new
|
|
8857
|
-
if (failures.some((failure) => failure.kind === "network")) return new
|
|
8858
|
-
if (failures.some((failure) => failure.kind === "invalid_response")) return new
|
|
8859
|
-
return new
|
|
8955
|
+
if (failures.some((failure) => failure.kind === "http" && (failure.status === 401 || failure.status === 403))) return new AgentServerModelDiscoveryError("discovery_unauthorized", `provider "${providerId}" rejected model discovery; check its API key`, 502);
|
|
8956
|
+
if (failures.some((failure) => failure.kind === "network")) return new AgentServerModelDiscoveryError("discovery_unavailable", `provider "${providerId}" could not be reached for model discovery`, 502);
|
|
8957
|
+
if (failures.some((failure) => failure.kind === "invalid_response")) return new AgentServerModelDiscoveryError("discovery_invalid_response", `provider "${providerId}" returned an invalid model response`, 502);
|
|
8958
|
+
return new AgentServerModelDiscoveryError("discovery_failed", `no models discovered for provider "${providerId}"`, 502);
|
|
8860
8959
|
}
|
|
8861
8960
|
function isRecord(value) {
|
|
8862
8961
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
8863
8962
|
}
|
|
8864
8963
|
//#endregion
|
|
8865
|
-
//#region src/lib/
|
|
8964
|
+
//#region src/lib/agent-server/protocol.ts
|
|
8965
|
+
var DateTime = Type.String({ format: "date-time" });
|
|
8966
|
+
var StringList = Type.Array(Type.String());
|
|
8967
|
+
function schemaRef(schema) {
|
|
8968
|
+
const id = schema.$id;
|
|
8969
|
+
if (typeof id !== "string" || id.length === 0) throw new Error("Agent Server protocol schemas must have an identifier");
|
|
8970
|
+
return Type.Ref(id);
|
|
8971
|
+
}
|
|
8972
|
+
var AgentServerHealthSchema = Type.Object({ status: Type.Literal("ok") }, { $id: "AgentServerHealth" });
|
|
8973
|
+
var AgentServerProblemSchema = Type.Object({
|
|
8974
|
+
code: Type.String(),
|
|
8975
|
+
message: Type.String()
|
|
8976
|
+
}, { $id: "AgentServerProblem" });
|
|
8977
|
+
var AgentServerAgentSchema = Type.Object({
|
|
8978
|
+
kind: Type.Union([Type.Literal("managed"), Type.Literal("external")]),
|
|
8979
|
+
agentName: Type.String(),
|
|
8980
|
+
identityId: Type.Optional(Type.String()),
|
|
8981
|
+
fingerprint: Type.Optional(Type.String()),
|
|
8982
|
+
apiUrl: Type.Optional(Type.String()),
|
|
8983
|
+
teamId: Type.Optional(Type.String()),
|
|
8984
|
+
configDir: Type.Optional(Type.String()),
|
|
8985
|
+
createdAt: DateTime,
|
|
8986
|
+
hasAgentKey: Type.Optional(Type.Boolean()),
|
|
8987
|
+
hasPrivateKey: Type.Optional(Type.Boolean())
|
|
8988
|
+
}, { $id: "AgentServerAgent" });
|
|
8989
|
+
var AgentServerProviderSchema = Type.Object({
|
|
8990
|
+
api: Type.String(),
|
|
8991
|
+
baseUrl: Type.String({ format: "uri" }),
|
|
8992
|
+
envName: Type.String(),
|
|
8993
|
+
models: StringList,
|
|
8994
|
+
hasApiKey: Type.Boolean()
|
|
8995
|
+
}, { $id: "AgentServerProvider" });
|
|
8996
|
+
var AgentServerRunRecordSchema = Type.Object({
|
|
8997
|
+
id: Type.String(),
|
|
8998
|
+
agent: Type.String(),
|
|
8999
|
+
teamId: Type.String(),
|
|
9000
|
+
profiles: StringList,
|
|
9001
|
+
taskTypes: StringList,
|
|
9002
|
+
mode: Type.Union([Type.Literal("poll"), Type.Literal("drain")]),
|
|
9003
|
+
status: Type.Union([
|
|
9004
|
+
Type.Literal("running"),
|
|
9005
|
+
Type.Literal("exited"),
|
|
9006
|
+
Type.Literal("stopped"),
|
|
9007
|
+
Type.Literal("failed")
|
|
9008
|
+
]),
|
|
9009
|
+
pid: Type.Optional(Type.Number()),
|
|
9010
|
+
exitCode: Type.Optional(Type.Union([Type.Number(), Type.Null()])),
|
|
9011
|
+
startedAt: DateTime,
|
|
9012
|
+
endedAt: Type.Optional(DateTime)
|
|
9013
|
+
}, { $id: "AgentServerRunRecord" });
|
|
9014
|
+
var AgentServerRunSchema = Type.Intersect([schemaRef(AgentServerRunRecordSchema), Type.Object({ active: Type.Boolean() })], { $id: "AgentServerRun" });
|
|
9015
|
+
var AgentServerSubscriptionSchema = Type.Object({
|
|
9016
|
+
id: Type.String(),
|
|
9017
|
+
name: Type.String(),
|
|
9018
|
+
connected: Type.Boolean()
|
|
9019
|
+
}, { $id: "AgentServerSubscription" });
|
|
9020
|
+
var AgentServerSubscriptionLoginSchema = Type.Object({
|
|
9021
|
+
providerId: Type.String(),
|
|
9022
|
+
status: Type.Union([
|
|
9023
|
+
Type.Literal("pending"),
|
|
9024
|
+
Type.Literal("completed"),
|
|
9025
|
+
Type.Literal("failed")
|
|
9026
|
+
]),
|
|
9027
|
+
authUrl: Type.Optional(Type.String({ format: "uri" })),
|
|
9028
|
+
instructions: Type.Optional(Type.String()),
|
|
9029
|
+
userCode: Type.Optional(Type.String()),
|
|
9030
|
+
verificationUri: Type.Optional(Type.String({ format: "uri" })),
|
|
9031
|
+
error: Type.Optional(Type.String())
|
|
9032
|
+
}, { $id: "AgentServerSubscriptionLogin" });
|
|
9033
|
+
var AgentServerStatusSchema = Type.Object({
|
|
9034
|
+
version: Type.String(),
|
|
9035
|
+
platform: Type.String(),
|
|
9036
|
+
subscriptions: Type.Array(schemaRef(AgentServerSubscriptionSchema)),
|
|
9037
|
+
agents: Type.Array(schemaRef(AgentServerAgentSchema)),
|
|
9038
|
+
providers: Type.Record(Type.String(), schemaRef(AgentServerProviderSchema)),
|
|
9039
|
+
runs: Type.Array(schemaRef(AgentServerRunSchema))
|
|
9040
|
+
}, { $id: "AgentServerStatus" });
|
|
9041
|
+
var PairingStartedSchema = Type.Object({
|
|
9042
|
+
pairingId: Type.String(),
|
|
9043
|
+
approvalPath: Type.String()
|
|
9044
|
+
}, { $id: "PairingStarted" });
|
|
9045
|
+
var PairingClaimedSchema = Type.Object({ token: Type.String() }, { $id: "PairingClaimed" });
|
|
9046
|
+
var PairingParamsSchema = Type.Object({ pairingId: Type.String() });
|
|
9047
|
+
var ProviderParamsSchema = Type.Object({ providerId: Type.String() });
|
|
9048
|
+
var AgentParamsSchema = Type.Object({ agentName: Type.String() });
|
|
9049
|
+
var RunParamsSchema = Type.Object({ runId: Type.String() });
|
|
9050
|
+
var CreateAgentSchema = Type.Object({
|
|
9051
|
+
kind: Type.Union([Type.Literal("managed"), Type.Literal("external")]),
|
|
9052
|
+
name: Type.String()
|
|
9053
|
+
}, { anyOf: [Type.Object({
|
|
9054
|
+
kind: Type.Literal("managed"),
|
|
9055
|
+
enrollmentToken: Type.String()
|
|
9056
|
+
}), Type.Object({
|
|
9057
|
+
kind: Type.Literal("external"),
|
|
9058
|
+
configDir: Type.String(),
|
|
9059
|
+
apiUrl: Type.Optional(Type.String({ format: "uri" }))
|
|
9060
|
+
})] });
|
|
9061
|
+
var ReconcileAgentSchema = Type.Object({ action: Type.Union([Type.Literal("resume"), Type.Literal("abandon")]) });
|
|
9062
|
+
var ReconcileAgentResultSchema = Type.Union([schemaRef(AgentServerAgentSchema), Type.Object({ abandoned: Type.Literal(true) })], { $id: "ReconcileAgentResult" });
|
|
9063
|
+
var PutProviderSchema = Type.Object({
|
|
9064
|
+
api: Type.String(),
|
|
9065
|
+
baseUrl: Type.String({ format: "uri" }),
|
|
9066
|
+
envName: Type.String(),
|
|
9067
|
+
models: StringList,
|
|
9068
|
+
apiKey: Type.Optional(Type.String())
|
|
9069
|
+
});
|
|
9070
|
+
var DiscoverModelsSchema = Type.Object({ models: StringList }, { $id: "DiscoveredModels" });
|
|
9071
|
+
var StartRunSchema = Type.Object({
|
|
9072
|
+
agent: Type.String(),
|
|
9073
|
+
teamId: Type.String(),
|
|
9074
|
+
profiles: StringList,
|
|
9075
|
+
taskTypes: StringList,
|
|
9076
|
+
mode: Type.Union([Type.Literal("poll"), Type.Literal("drain")])
|
|
9077
|
+
});
|
|
9078
|
+
var CancelledSubscriptionSchema = Type.Object({
|
|
9079
|
+
providerId: Type.String(),
|
|
9080
|
+
status: Type.Literal("cancelled")
|
|
9081
|
+
}, { $id: "CancelledSubscription" });
|
|
9082
|
+
var LogStreamSchema = Type.String({
|
|
9083
|
+
$id: "AgentServerLogStream",
|
|
9084
|
+
contentMediaType: "text/event-stream"
|
|
9085
|
+
});
|
|
9086
|
+
var AGENT_SERVER_SCHEMAS = [
|
|
9087
|
+
AgentServerHealthSchema,
|
|
9088
|
+
AgentServerProblemSchema,
|
|
9089
|
+
AgentServerAgentSchema,
|
|
9090
|
+
AgentServerProviderSchema,
|
|
9091
|
+
AgentServerRunRecordSchema,
|
|
9092
|
+
AgentServerRunSchema,
|
|
9093
|
+
AgentServerSubscriptionSchema,
|
|
9094
|
+
AgentServerSubscriptionLoginSchema,
|
|
9095
|
+
AgentServerStatusSchema,
|
|
9096
|
+
PairingStartedSchema,
|
|
9097
|
+
PairingClaimedSchema,
|
|
9098
|
+
ReconcileAgentResultSchema,
|
|
9099
|
+
DiscoverModelsSchema,
|
|
9100
|
+
CancelledSubscriptionSchema,
|
|
9101
|
+
LogStreamSchema
|
|
9102
|
+
];
|
|
9103
|
+
var pairedSecurity = [{ agentServerToken: [] }];
|
|
9104
|
+
var problemResponse = { default: schemaRef(AgentServerProblemSchema) };
|
|
9105
|
+
var AgentServerRouteSchemas = {
|
|
9106
|
+
health: {
|
|
9107
|
+
operationId: "getAgentServerHealth",
|
|
9108
|
+
tags: ["system"],
|
|
9109
|
+
response: { 200: schemaRef(AgentServerHealthSchema) }
|
|
9110
|
+
},
|
|
9111
|
+
startPairing: {
|
|
9112
|
+
operationId: "startAgentServerPairing",
|
|
9113
|
+
tags: ["pairing"],
|
|
9114
|
+
response: {
|
|
9115
|
+
201: schemaRef(PairingStartedSchema),
|
|
9116
|
+
...problemResponse
|
|
9117
|
+
}
|
|
9118
|
+
},
|
|
9119
|
+
claimPairing: {
|
|
9120
|
+
operationId: "claimAgentServerPairing",
|
|
9121
|
+
tags: ["pairing"],
|
|
9122
|
+
params: PairingParamsSchema,
|
|
9123
|
+
response: {
|
|
9124
|
+
200: schemaRef(PairingClaimedSchema),
|
|
9125
|
+
...problemResponse
|
|
9126
|
+
}
|
|
9127
|
+
},
|
|
9128
|
+
status: {
|
|
9129
|
+
operationId: "getAgentServerStatus",
|
|
9130
|
+
tags: ["system"],
|
|
9131
|
+
security: pairedSecurity,
|
|
9132
|
+
response: {
|
|
9133
|
+
200: schemaRef(AgentServerStatusSchema),
|
|
9134
|
+
...problemResponse
|
|
9135
|
+
}
|
|
9136
|
+
},
|
|
9137
|
+
listAgents: {
|
|
9138
|
+
operationId: "listAgentServerAgents",
|
|
9139
|
+
tags: ["agents"],
|
|
9140
|
+
security: pairedSecurity,
|
|
9141
|
+
response: {
|
|
9142
|
+
200: Type.Array(schemaRef(AgentServerAgentSchema)),
|
|
9143
|
+
...problemResponse
|
|
9144
|
+
}
|
|
9145
|
+
},
|
|
9146
|
+
createAgent: {
|
|
9147
|
+
operationId: "createAgentServerAgent",
|
|
9148
|
+
tags: ["agents"],
|
|
9149
|
+
security: pairedSecurity,
|
|
9150
|
+
body: CreateAgentSchema,
|
|
9151
|
+
response: {
|
|
9152
|
+
201: schemaRef(AgentServerAgentSchema),
|
|
9153
|
+
...problemResponse
|
|
9154
|
+
}
|
|
9155
|
+
},
|
|
9156
|
+
reconcileAgent: {
|
|
9157
|
+
operationId: "reconcileAgentServerAgent",
|
|
9158
|
+
tags: ["agents"],
|
|
9159
|
+
security: pairedSecurity,
|
|
9160
|
+
params: AgentParamsSchema,
|
|
9161
|
+
body: ReconcileAgentSchema,
|
|
9162
|
+
response: {
|
|
9163
|
+
200: schemaRef(ReconcileAgentResultSchema),
|
|
9164
|
+
...problemResponse
|
|
9165
|
+
}
|
|
9166
|
+
},
|
|
9167
|
+
listProviders: {
|
|
9168
|
+
operationId: "listAgentServerProviders",
|
|
9169
|
+
tags: ["providers"],
|
|
9170
|
+
security: pairedSecurity,
|
|
9171
|
+
response: {
|
|
9172
|
+
200: Type.Record(Type.String(), schemaRef(AgentServerProviderSchema)),
|
|
9173
|
+
...problemResponse
|
|
9174
|
+
}
|
|
9175
|
+
},
|
|
9176
|
+
discoverModels: {
|
|
9177
|
+
operationId: "discoverAgentServerProviderModels",
|
|
9178
|
+
tags: ["providers"],
|
|
9179
|
+
security: pairedSecurity,
|
|
9180
|
+
params: ProviderParamsSchema,
|
|
9181
|
+
response: {
|
|
9182
|
+
200: schemaRef(DiscoverModelsSchema),
|
|
9183
|
+
...problemResponse
|
|
9184
|
+
}
|
|
9185
|
+
},
|
|
9186
|
+
putProvider: {
|
|
9187
|
+
operationId: "putAgentServerProvider",
|
|
9188
|
+
tags: ["providers"],
|
|
9189
|
+
security: pairedSecurity,
|
|
9190
|
+
params: ProviderParamsSchema,
|
|
9191
|
+
body: PutProviderSchema,
|
|
9192
|
+
response: {
|
|
9193
|
+
200: schemaRef(AgentServerProviderSchema),
|
|
9194
|
+
...problemResponse
|
|
9195
|
+
}
|
|
9196
|
+
},
|
|
9197
|
+
listSubscriptions: {
|
|
9198
|
+
operationId: "listAgentServerSubscriptions",
|
|
9199
|
+
tags: ["subscriptions"],
|
|
9200
|
+
security: pairedSecurity,
|
|
9201
|
+
response: {
|
|
9202
|
+
200: Type.Array(schemaRef(AgentServerSubscriptionSchema)),
|
|
9203
|
+
...problemResponse
|
|
9204
|
+
}
|
|
9205
|
+
},
|
|
9206
|
+
startSubscriptionLogin: {
|
|
9207
|
+
operationId: "startAgentServerSubscriptionLogin",
|
|
9208
|
+
tags: ["subscriptions"],
|
|
9209
|
+
security: pairedSecurity,
|
|
9210
|
+
params: ProviderParamsSchema,
|
|
9211
|
+
response: {
|
|
9212
|
+
201: schemaRef(AgentServerSubscriptionLoginSchema),
|
|
9213
|
+
...problemResponse
|
|
9214
|
+
}
|
|
9215
|
+
},
|
|
9216
|
+
getSubscriptionLogin: {
|
|
9217
|
+
operationId: "getAgentServerSubscriptionLogin",
|
|
9218
|
+
tags: ["subscriptions"],
|
|
9219
|
+
security: pairedSecurity,
|
|
9220
|
+
params: ProviderParamsSchema,
|
|
9221
|
+
response: {
|
|
9222
|
+
200: schemaRef(AgentServerSubscriptionLoginSchema),
|
|
9223
|
+
...problemResponse
|
|
9224
|
+
}
|
|
9225
|
+
},
|
|
9226
|
+
cancelSubscriptionLogin: {
|
|
9227
|
+
operationId: "cancelAgentServerSubscriptionLogin",
|
|
9228
|
+
tags: ["subscriptions"],
|
|
9229
|
+
security: pairedSecurity,
|
|
9230
|
+
params: ProviderParamsSchema,
|
|
9231
|
+
response: {
|
|
9232
|
+
200: schemaRef(CancelledSubscriptionSchema),
|
|
9233
|
+
...problemResponse
|
|
9234
|
+
}
|
|
9235
|
+
},
|
|
9236
|
+
listRuns: {
|
|
9237
|
+
operationId: "listAgentServerRuns",
|
|
9238
|
+
tags: ["runs"],
|
|
9239
|
+
security: pairedSecurity,
|
|
9240
|
+
response: {
|
|
9241
|
+
200: Type.Array(schemaRef(AgentServerRunSchema)),
|
|
9242
|
+
...problemResponse
|
|
9243
|
+
}
|
|
9244
|
+
},
|
|
9245
|
+
startRun: {
|
|
9246
|
+
operationId: "startAgentServerRun",
|
|
9247
|
+
tags: ["runs"],
|
|
9248
|
+
security: pairedSecurity,
|
|
9249
|
+
body: StartRunSchema,
|
|
9250
|
+
response: {
|
|
9251
|
+
201: schemaRef(AgentServerRunSchema),
|
|
9252
|
+
...problemResponse
|
|
9253
|
+
}
|
|
9254
|
+
},
|
|
9255
|
+
stopRun: {
|
|
9256
|
+
operationId: "stopAgentServerRun",
|
|
9257
|
+
tags: ["runs"],
|
|
9258
|
+
security: pairedSecurity,
|
|
9259
|
+
params: RunParamsSchema,
|
|
9260
|
+
response: {
|
|
9261
|
+
200: schemaRef(AgentServerRunRecordSchema),
|
|
9262
|
+
...problemResponse
|
|
9263
|
+
}
|
|
9264
|
+
},
|
|
9265
|
+
streamRunLogs: {
|
|
9266
|
+
operationId: "streamAgentServerRunLogs",
|
|
9267
|
+
tags: ["runs"],
|
|
9268
|
+
security: pairedSecurity,
|
|
9269
|
+
params: RunParamsSchema,
|
|
9270
|
+
response: {
|
|
9271
|
+
200: schemaRef(LogStreamSchema),
|
|
9272
|
+
...problemResponse
|
|
9273
|
+
}
|
|
9274
|
+
}
|
|
9275
|
+
};
|
|
9276
|
+
//#endregion
|
|
9277
|
+
//#region src/lib/agent-server/server.ts
|
|
8866
9278
|
/**
|
|
8867
|
-
* HTTP surface of `moltnet-agent
|
|
9279
|
+
* HTTP surface of `moltnet-agent server` (#2061), built on the shared
|
|
8868
9280
|
* loopback-companion security profile (#2066): loopback Host enforcement,
|
|
8869
9281
|
* exact-origin CORS, Fetch-Metadata guards, strict JSON parsing.
|
|
8870
9282
|
*
|
|
8871
9283
|
* Everything under `/v1` except the pairing bootstrap requires a paired
|
|
8872
|
-
* origin: the `x-moltnet-
|
|
9284
|
+
* origin: the `x-moltnet-agent-server-token` header must verify against the
|
|
8873
9285
|
* origin-bound token issued by the one-click pairing ceremony.
|
|
8874
9286
|
*/
|
|
8875
|
-
var
|
|
9287
|
+
var AGENT_SERVER_TOKEN_HEADER = "x-moltnet-agent-server-token";
|
|
8876
9288
|
var BODY_LIMIT = 64 * 1024;
|
|
8877
9289
|
var LOG_POLL_INTERVAL_MS = 500;
|
|
8878
9290
|
var LOG_STREAM_MAX_DURATION_MS = 3600 * 1e3;
|
|
@@ -8881,7 +9293,7 @@ var LOG_READ_LIMIT_BYTES = 256 * 1024;
|
|
|
8881
9293
|
var RUN_HISTORY_LIMIT = 100;
|
|
8882
9294
|
var RATE_LIMIT_MAX = 120;
|
|
8883
9295
|
var RATE_LIMIT_WINDOW_MS = 6e4;
|
|
8884
|
-
async function
|
|
9296
|
+
async function readAgentServerLogDelta(handle, state, limit = LOG_READ_LIMIT_BYTES) {
|
|
8885
9297
|
const info = await handle.stat();
|
|
8886
9298
|
if (!info.isFile() || info.size <= state.offset) return {
|
|
8887
9299
|
lines: [],
|
|
@@ -8929,8 +9341,8 @@ async function readServeLogDelta(handle, state, limit = LOG_READ_LIMIT_BYTES) {
|
|
|
8929
9341
|
omitted
|
|
8930
9342
|
};
|
|
8931
9343
|
}
|
|
8932
|
-
var
|
|
8933
|
-
name = "
|
|
9344
|
+
var AgentServerHttpError = class extends Error {
|
|
9345
|
+
name = "AgentServerHttpError";
|
|
8934
9346
|
constructor(statusCode, code, message) {
|
|
8935
9347
|
super(message);
|
|
8936
9348
|
this.statusCode = statusCode;
|
|
@@ -8939,23 +9351,23 @@ var ServeHttpError = class extends Error {
|
|
|
8939
9351
|
};
|
|
8940
9352
|
function requireBody(request) {
|
|
8941
9353
|
const body = request.body;
|
|
8942
|
-
if (typeof body !== "object" || body === null || Array.isArray(body)) throw new
|
|
9354
|
+
if (typeof body !== "object" || body === null || Array.isArray(body)) throw new AgentServerHttpError(400, "invalid_body", "JSON object body required");
|
|
8943
9355
|
return body;
|
|
8944
9356
|
}
|
|
8945
9357
|
function requireString(body, field) {
|
|
8946
9358
|
const value = body[field];
|
|
8947
|
-
if (typeof value !== "string" || value.trim().length === 0) throw new
|
|
9359
|
+
if (typeof value !== "string" || value.trim().length === 0) throw new AgentServerHttpError(400, "invalid_body", `"${field}" must be a non-empty string`);
|
|
8948
9360
|
return value.trim();
|
|
8949
9361
|
}
|
|
8950
9362
|
function optionalString(body, field) {
|
|
8951
9363
|
const value = body[field];
|
|
8952
9364
|
if (value === void 0 || value === null) return void 0;
|
|
8953
|
-
if (typeof value !== "string" || value.trim().length === 0) throw new
|
|
9365
|
+
if (typeof value !== "string" || value.trim().length === 0) throw new AgentServerHttpError(400, "invalid_body", `"${field}" must be a non-empty string when present`);
|
|
8954
9366
|
return value.trim();
|
|
8955
9367
|
}
|
|
8956
9368
|
function stringArray(body, field, options = {}) {
|
|
8957
9369
|
const value = body[field];
|
|
8958
|
-
if (!Array.isArray(value) || !options.allowEmpty && value.length === 0 || value.some((item) => typeof item !== "string" || item.length === 0)) throw new
|
|
9370
|
+
if (!Array.isArray(value) || !options.allowEmpty && value.length === 0 || value.some((item) => typeof item !== "string" || item.length === 0)) throw new AgentServerHttpError(400, "invalid_body", `"${field}" must be ${options.allowEmpty ? "a" : "a non-empty"} string array`);
|
|
8959
9371
|
return value;
|
|
8960
9372
|
}
|
|
8961
9373
|
function requestOperationSignal(request, shutdownSignal) {
|
|
@@ -8964,16 +9376,18 @@ function requestOperationSignal(request, shutdownSignal) {
|
|
|
8964
9376
|
else request.raw.once("aborted", () => disconnected.abort());
|
|
8965
9377
|
return shutdownSignal ? AbortSignal.any([disconnected.signal, shutdownSignal]) : disconnected.signal;
|
|
8966
9378
|
}
|
|
8967
|
-
function
|
|
9379
|
+
function buildAgentServer(options) {
|
|
8968
9380
|
const { pairing } = options;
|
|
8969
9381
|
const app = options.logger ? Fastify({
|
|
8970
9382
|
bodyLimit: BODY_LIMIT,
|
|
8971
9383
|
loggerInstance: options.logger
|
|
8972
9384
|
}) : Fastify({ bodyLimit: BODY_LIMIT });
|
|
9385
|
+
options.registerOpenApi?.(app);
|
|
9386
|
+
for (const schema of AGENT_SERVER_SCHEMAS) app.addSchema(schema);
|
|
8973
9387
|
registerLoopbackSecurity(app, {
|
|
8974
9388
|
allowedOrigins: options.allowedOrigins,
|
|
8975
9389
|
...options.selfOrigin ? { selfOrigins: [options.selfOrigin] } : {},
|
|
8976
|
-
allowedHeaders: [
|
|
9390
|
+
allowedHeaders: [AGENT_SERVER_TOKEN_HEADER],
|
|
8977
9391
|
methods: [
|
|
8978
9392
|
"GET",
|
|
8979
9393
|
"POST",
|
|
@@ -8986,35 +9400,37 @@ function buildServeServer(options) {
|
|
|
8986
9400
|
global: false,
|
|
8987
9401
|
max: options.rateLimitMax ?? RATE_LIMIT_MAX,
|
|
8988
9402
|
timeWindow: RATE_LIMIT_WINDOW_MS,
|
|
8989
|
-
errorResponseBuilder: () => new
|
|
9403
|
+
errorResponseBuilder: () => new AgentServerHttpError(429, "rate_limited", "Too many requests"),
|
|
8990
9404
|
keyGenerator: (request) => {
|
|
8991
9405
|
const origin = request.headers.origin;
|
|
8992
9406
|
return isConfiguredOrigin(origin, options) ? `origin:${origin}` : `ip:${request.ip}`;
|
|
8993
9407
|
}
|
|
8994
9408
|
});
|
|
8995
|
-
app.after(() => app.addHook("onRequest", app.rateLimit()));
|
|
8996
9409
|
app.addContentTypeParser("application/x-www-form-urlencoded", { parseAs: "buffer" }, (_request, body, done) => {
|
|
8997
9410
|
try {
|
|
8998
9411
|
const form = new TextDecoder("utf-8", { fatal: true }).decode(typeof body === "string" ? Buffer.from(body) : body);
|
|
8999
9412
|
done(null, new URLSearchParams(form));
|
|
9000
9413
|
} catch {
|
|
9001
|
-
done(new
|
|
9414
|
+
done(new AgentServerHttpError(400, "invalid_body", "Form body is invalid"), void 0);
|
|
9002
9415
|
}
|
|
9003
9416
|
});
|
|
9004
9417
|
const requirePairedOrigin = (request) => {
|
|
9005
9418
|
const origin = requireOriginHeader(request.headers);
|
|
9006
|
-
const token = request.headers[
|
|
9007
|
-
if (typeof token !== "string" || token.length === 0) throw new
|
|
9419
|
+
const token = request.headers[AGENT_SERVER_TOKEN_HEADER];
|
|
9420
|
+
if (typeof token !== "string" || token.length === 0) throw new AgentServerHttpError(401, "pairing_required", "Pairing token is required");
|
|
9008
9421
|
pairing.verify(origin, token);
|
|
9009
9422
|
return origin;
|
|
9010
9423
|
};
|
|
9011
|
-
app.
|
|
9012
|
-
|
|
9013
|
-
|
|
9014
|
-
|
|
9015
|
-
|
|
9016
|
-
|
|
9017
|
-
|
|
9424
|
+
app.after(() => {
|
|
9425
|
+
app.addHook("onRequest", app.rateLimit());
|
|
9426
|
+
app.get("/health", { schema: AgentServerRouteSchemas.health }, async () => ({ status: "ok" }));
|
|
9427
|
+
registerPairingRoutes(app, pairing);
|
|
9428
|
+
registerStatusRoute(app, options, requirePairedOrigin);
|
|
9429
|
+
registerAgentRoutes(app, options, requirePairedOrigin);
|
|
9430
|
+
registerProviderRoutes(app, options, requirePairedOrigin);
|
|
9431
|
+
registerSubscriptionRoutes(app, options, requirePairedOrigin);
|
|
9432
|
+
registerRunRoutes(app, options, requirePairedOrigin);
|
|
9433
|
+
});
|
|
9018
9434
|
app.addHook("onClose", () => {
|
|
9019
9435
|
options.subscriptions.close();
|
|
9020
9436
|
});
|
|
@@ -9023,13 +9439,13 @@ function buildServeServer(options) {
|
|
|
9023
9439
|
message: "Route is not available"
|
|
9024
9440
|
}));
|
|
9025
9441
|
app.setErrorHandler(async (error, request, reply) => {
|
|
9026
|
-
const { statusCode, code, message } =
|
|
9442
|
+
const { statusCode, code, message } = normalizeAgentServerError(error);
|
|
9027
9443
|
if (statusCode === 500) request.log.error({
|
|
9028
9444
|
...safeErrorContext(error),
|
|
9029
|
-
code: "
|
|
9445
|
+
code: "agent_server_request_failed",
|
|
9030
9446
|
method: request.method,
|
|
9031
9447
|
route: request.routeOptions.url
|
|
9032
|
-
}, "
|
|
9448
|
+
}, "AgentServer request failed");
|
|
9033
9449
|
return reply.code(statusCode).send({
|
|
9034
9450
|
code,
|
|
9035
9451
|
message
|
|
@@ -9038,11 +9454,11 @@ function buildServeServer(options) {
|
|
|
9038
9454
|
return app;
|
|
9039
9455
|
}
|
|
9040
9456
|
function registerPairingRoutes(app, pairing) {
|
|
9041
|
-
app.post("/v1/pairings", async (request, reply) => {
|
|
9457
|
+
app.post("/v1/pairings", { schema: AgentServerRouteSchemas.startPairing }, async (request, reply) => {
|
|
9042
9458
|
const origin = requireOriginHeader(request.headers);
|
|
9043
9459
|
return reply.code(201).send(pairing.start(origin));
|
|
9044
9460
|
});
|
|
9045
|
-
app.get("/pairings/:pairingId", async (request, reply) => {
|
|
9461
|
+
app.get("/pairings/:pairingId", { schema: { hide: true } }, async (request, reply) => {
|
|
9046
9462
|
assertNavigationRequest(request.headers);
|
|
9047
9463
|
const { pairingId } = request.params;
|
|
9048
9464
|
const approval = pairing.approval(pairingId);
|
|
@@ -9052,17 +9468,17 @@ function registerPairingRoutes(app, pairing) {
|
|
|
9052
9468
|
confirmToken: approval.confirmToken
|
|
9053
9469
|
}));
|
|
9054
9470
|
});
|
|
9055
|
-
app.post("/pairings/:pairingId/confirm", async (request, reply) => {
|
|
9471
|
+
app.post("/pairings/:pairingId/confirm", { schema: { hide: true } }, async (request, reply) => {
|
|
9056
9472
|
rejectExplicitCrossSite(request.headers);
|
|
9057
9473
|
const { pairingId } = request.params;
|
|
9058
|
-
if (!(request.body instanceof URLSearchParams)) throw new
|
|
9474
|
+
if (!(request.body instanceof URLSearchParams)) throw new AgentServerHttpError(400, "invalid_body", "Confirmation form is invalid");
|
|
9059
9475
|
const { origin } = pairing.confirm(pairingId, request.body.get("confirmToken") ?? "");
|
|
9060
9476
|
return reply.type("text/html; charset=utf-8").send(renderPairingResultPage({
|
|
9061
9477
|
title: "Connection approved",
|
|
9062
9478
|
message: `${origin} can now manage local MoltNet agents on this machine.`
|
|
9063
9479
|
}));
|
|
9064
9480
|
});
|
|
9065
|
-
app.post("/v1/pairings/:pairingId/claim", async (request) => {
|
|
9481
|
+
app.post("/v1/pairings/:pairingId/claim", { schema: AgentServerRouteSchemas.claimPairing }, async (request) => {
|
|
9066
9482
|
const origin = requireOriginHeader(request.headers);
|
|
9067
9483
|
const { pairingId } = request.params;
|
|
9068
9484
|
return pairing.claim(pairingId, origin);
|
|
@@ -9070,7 +9486,7 @@ function registerPairingRoutes(app, pairing) {
|
|
|
9070
9486
|
}
|
|
9071
9487
|
function registerStatusRoute(app, options, requirePairedOrigin) {
|
|
9072
9488
|
const { store, runs } = options;
|
|
9073
|
-
app.get("/v1/status", async (request) => {
|
|
9489
|
+
app.get("/v1/status", { schema: AgentServerRouteSchemas.status }, async (request) => {
|
|
9074
9490
|
requirePairedOrigin(request);
|
|
9075
9491
|
return {
|
|
9076
9492
|
version: options.version,
|
|
@@ -9084,17 +9500,20 @@ function registerStatusRoute(app, options, requirePairedOrigin) {
|
|
|
9084
9500
|
}
|
|
9085
9501
|
function registerAgentRoutes(app, options, requirePairedOrigin) {
|
|
9086
9502
|
const { store } = options;
|
|
9087
|
-
app.get("/v1/agents", async (request) => {
|
|
9503
|
+
app.get("/v1/agents", { schema: AgentServerRouteSchemas.listAgents }, async (request) => {
|
|
9088
9504
|
requirePairedOrigin(request);
|
|
9089
9505
|
return store.listActivations().map((activation) => publicAgentView(store, activation));
|
|
9090
9506
|
});
|
|
9091
|
-
app.post("/v1/agents",
|
|
9507
|
+
app.post("/v1/agents", {
|
|
9508
|
+
schema: AgentServerRouteSchemas.createAgent,
|
|
9509
|
+
attachValidation: true
|
|
9510
|
+
}, async (request, reply) => {
|
|
9092
9511
|
requirePairedOrigin(request);
|
|
9093
9512
|
const body = requireBody(request);
|
|
9094
9513
|
const signal = requestOperationSignal(request, options.shutdownSignal);
|
|
9095
9514
|
const kind = requireString(body, "kind");
|
|
9096
9515
|
if (kind === "managed") {
|
|
9097
|
-
if (body["apiUrl"] !== void 0) throw new
|
|
9516
|
+
if (body["apiUrl"] !== void 0) throw new AgentServerHttpError(400, "invalid_body", "managed agent registration uses the configured MoltNet API URL; apiUrl cannot be overridden");
|
|
9098
9517
|
const entry = await createManagedAgent(store, options.secrets, {
|
|
9099
9518
|
name: requireString(body, "name"),
|
|
9100
9519
|
apiUrl: options.defaultApiUrl,
|
|
@@ -9113,13 +9532,16 @@ function registerAgentRoutes(app, options, requirePairedOrigin) {
|
|
|
9113
9532
|
});
|
|
9114
9533
|
return reply.code(201).send(publicAgentView(store, entry.activation));
|
|
9115
9534
|
}
|
|
9116
|
-
throw new
|
|
9535
|
+
throw new AgentServerHttpError(400, "invalid_body", "\"kind\" must be \"managed\" or \"external\"");
|
|
9117
9536
|
});
|
|
9118
|
-
app.post("/v1/agents/:agentName/reconcile",
|
|
9537
|
+
app.post("/v1/agents/:agentName/reconcile", {
|
|
9538
|
+
schema: AgentServerRouteSchemas.reconcileAgent,
|
|
9539
|
+
attachValidation: true
|
|
9540
|
+
}, async (request) => {
|
|
9119
9541
|
requirePairedOrigin(request);
|
|
9120
9542
|
const { agentName } = request.params;
|
|
9121
9543
|
const action = requireString(requireBody(request), "action");
|
|
9122
|
-
if (action !== "resume" && action !== "abandon") throw new
|
|
9544
|
+
if (action !== "resume" && action !== "abandon") throw new AgentServerHttpError(400, "invalid_body", "\"action\" must be \"resume\" or \"abandon\"");
|
|
9123
9545
|
const reconciled = await reconcileManagedRegistration(store, options.secrets, agentName, action, void 0, requestOperationSignal(request, options.shutdownSignal));
|
|
9124
9546
|
return reconciled ? publicAgentView(store, reconciled.activation) : { abandoned: true };
|
|
9125
9547
|
});
|
|
@@ -9132,16 +9554,16 @@ function registerProviderRoutes(app, options, requirePairedOrigin) {
|
|
|
9132
9554
|
mutationQueue = result.then(() => void 0, () => void 0);
|
|
9133
9555
|
return result;
|
|
9134
9556
|
};
|
|
9135
|
-
app.get("/v1/providers", async (request) => {
|
|
9557
|
+
app.get("/v1/providers", { schema: AgentServerRouteSchemas.listProviders }, async (request) => {
|
|
9136
9558
|
requirePairedOrigin(request);
|
|
9137
9559
|
return Object.fromEntries(Object.entries(store.readProviders()).map(([id, provider]) => [id, providerView(provider)]));
|
|
9138
9560
|
});
|
|
9139
|
-
app.post("/v1/providers/:providerId/discover-models", async (request) => {
|
|
9561
|
+
app.post("/v1/providers/:providerId/discover-models", { schema: AgentServerRouteSchemas.discoverModels }, async (request) => {
|
|
9140
9562
|
requirePairedOrigin(request);
|
|
9141
9563
|
const { providerId: rawProviderId } = request.params;
|
|
9142
9564
|
const providerId = assertProviderId(rawProviderId);
|
|
9143
9565
|
const provider = store.readProviders()[providerId];
|
|
9144
|
-
if (!provider) throw new
|
|
9566
|
+
if (!provider) throw new AgentServerHttpError(404, "provider_not_found", `provider "${providerId}" was not found`);
|
|
9145
9567
|
const parsed = parseProviderBaseUrl(provider.baseUrl, providerId);
|
|
9146
9568
|
const baseUrl = parsed.href.replace(/\/$/u, "");
|
|
9147
9569
|
let apiKey;
|
|
@@ -9150,10 +9572,10 @@ function registerProviderRoutes(app, options, requirePairedOrigin) {
|
|
|
9150
9572
|
} catch (error) {
|
|
9151
9573
|
request.log.warn({
|
|
9152
9574
|
...safeErrorContext(error),
|
|
9153
|
-
code: "
|
|
9575
|
+
code: "agent_server_provider_secret_unavailable",
|
|
9154
9576
|
providerId
|
|
9155
9577
|
}, "Provider API key could not be resolved for model discovery");
|
|
9156
|
-
throw new
|
|
9578
|
+
throw new AgentServerHttpError(400, "provider_secret_unavailable", `provider "${providerId}" API key could not be resolved`);
|
|
9157
9579
|
}
|
|
9158
9580
|
const headers = apiKey ? { authorization: `Bearer ${apiKey}` } : {};
|
|
9159
9581
|
const fetchImpl = options.discoverFetch ?? fetch;
|
|
@@ -9174,7 +9596,7 @@ function registerProviderRoutes(app, options, requirePairedOrigin) {
|
|
|
9174
9596
|
errorType
|
|
9175
9597
|
});
|
|
9176
9598
|
request.log.warn({
|
|
9177
|
-
code: "
|
|
9599
|
+
code: "agent_server_provider_discovery_request_failed",
|
|
9178
9600
|
endpoint,
|
|
9179
9601
|
errorType,
|
|
9180
9602
|
providerId
|
|
@@ -9187,7 +9609,7 @@ function registerProviderRoutes(app, options, requirePairedOrigin) {
|
|
|
9187
9609
|
status: response.status
|
|
9188
9610
|
});
|
|
9189
9611
|
const context = {
|
|
9190
|
-
code: "
|
|
9612
|
+
code: "agent_server_provider_discovery_upstream_error",
|
|
9191
9613
|
endpoint,
|
|
9192
9614
|
providerId,
|
|
9193
9615
|
statusCode: response.status
|
|
@@ -9201,7 +9623,7 @@ function registerProviderRoutes(app, options, requirePairedOrigin) {
|
|
|
9201
9623
|
} catch {
|
|
9202
9624
|
failures.push({ kind: "invalid_response" });
|
|
9203
9625
|
request.log.warn({
|
|
9204
|
-
code: "
|
|
9626
|
+
code: "agent_server_provider_discovery_invalid_json",
|
|
9205
9627
|
endpoint,
|
|
9206
9628
|
providerId
|
|
9207
9629
|
}, "Provider model discovery returned invalid JSON");
|
|
@@ -9212,19 +9634,22 @@ function registerProviderRoutes(app, options, requirePairedOrigin) {
|
|
|
9212
9634
|
if (collector.size === 0) collector.addOllamaResponse(await tryJson("ollama_tags", `${parsed.origin}/api/tags`));
|
|
9213
9635
|
const result = collector.result(providerId, failures);
|
|
9214
9636
|
if (result.discoveredCount > result.models.length) request.log.warn({
|
|
9215
|
-
code: "
|
|
9637
|
+
code: "agent_server_provider_discovery_truncated",
|
|
9216
9638
|
discoveredCount: result.discoveredCount,
|
|
9217
9639
|
providerId,
|
|
9218
9640
|
returnedCount: 500
|
|
9219
9641
|
}, "Provider model discovery result was truncated");
|
|
9220
9642
|
request.log.info({
|
|
9221
|
-
code: "
|
|
9643
|
+
code: "agent_server_provider_discovery_completed",
|
|
9222
9644
|
modelCount: result.models.length,
|
|
9223
9645
|
providerId
|
|
9224
9646
|
}, "Provider model discovery completed");
|
|
9225
9647
|
return { models: result.models };
|
|
9226
9648
|
});
|
|
9227
|
-
app.put("/v1/providers/:providerId",
|
|
9649
|
+
app.put("/v1/providers/:providerId", {
|
|
9650
|
+
schema: AgentServerRouteSchemas.putProvider,
|
|
9651
|
+
attachValidation: true
|
|
9652
|
+
}, async (request, reply) => {
|
|
9228
9653
|
requirePairedOrigin(request);
|
|
9229
9654
|
const { providerId: rawProviderId } = request.params;
|
|
9230
9655
|
const providerId = assertProviderId(rawProviderId);
|
|
@@ -9261,22 +9686,22 @@ function runViews(runs) {
|
|
|
9261
9686
|
}));
|
|
9262
9687
|
}
|
|
9263
9688
|
function registerSubscriptionRoutes(app, options, requirePairedOrigin) {
|
|
9264
|
-
app.get("/v1/subscriptions", async (request) => {
|
|
9689
|
+
app.get("/v1/subscriptions", { schema: AgentServerRouteSchemas.listSubscriptions }, async (request) => {
|
|
9265
9690
|
requirePairedOrigin(request);
|
|
9266
9691
|
return options.subscriptions.list();
|
|
9267
9692
|
});
|
|
9268
|
-
app.post("/v1/subscriptions/:providerId/login", async (request, reply) => {
|
|
9693
|
+
app.post("/v1/subscriptions/:providerId/login", { schema: AgentServerRouteSchemas.startSubscriptionLogin }, async (request, reply) => {
|
|
9269
9694
|
requirePairedOrigin(request);
|
|
9270
9695
|
const { providerId } = request.params;
|
|
9271
9696
|
const login = await options.subscriptions.start(providerId);
|
|
9272
9697
|
return reply.code(201).send(login);
|
|
9273
9698
|
});
|
|
9274
|
-
app.get("/v1/subscriptions/:providerId/login", async (request) => {
|
|
9699
|
+
app.get("/v1/subscriptions/:providerId/login", { schema: AgentServerRouteSchemas.getSubscriptionLogin }, async (request) => {
|
|
9275
9700
|
requirePairedOrigin(request);
|
|
9276
9701
|
const { providerId } = request.params;
|
|
9277
9702
|
return options.subscriptions.status(providerId);
|
|
9278
9703
|
});
|
|
9279
|
-
app.delete("/v1/subscriptions/:providerId/login", async (request) => {
|
|
9704
|
+
app.delete("/v1/subscriptions/:providerId/login", { schema: AgentServerRouteSchemas.cancelSubscriptionLogin }, async (request) => {
|
|
9280
9705
|
requirePairedOrigin(request);
|
|
9281
9706
|
const { providerId } = request.params;
|
|
9282
9707
|
return options.subscriptions.cancel(providerId);
|
|
@@ -9284,11 +9709,14 @@ function registerSubscriptionRoutes(app, options, requirePairedOrigin) {
|
|
|
9284
9709
|
}
|
|
9285
9710
|
function registerRunRoutes(app, options, requirePairedOrigin) {
|
|
9286
9711
|
const { runs } = options;
|
|
9287
|
-
app.get("/v1/runs", async (request) => {
|
|
9712
|
+
app.get("/v1/runs", { schema: AgentServerRouteSchemas.listRuns }, async (request) => {
|
|
9288
9713
|
requirePairedOrigin(request);
|
|
9289
9714
|
return runViews(runs);
|
|
9290
9715
|
});
|
|
9291
|
-
app.post("/v1/runs",
|
|
9716
|
+
app.post("/v1/runs", {
|
|
9717
|
+
schema: AgentServerRouteSchemas.startRun,
|
|
9718
|
+
attachValidation: true
|
|
9719
|
+
}, async (request, reply) => {
|
|
9292
9720
|
requirePairedOrigin(request);
|
|
9293
9721
|
const body = requireBody(request);
|
|
9294
9722
|
const record = await runs.start({
|
|
@@ -9298,9 +9726,12 @@ function registerRunRoutes(app, options, requirePairedOrigin) {
|
|
|
9298
9726
|
taskTypes: stringArray(body, "taskTypes"),
|
|
9299
9727
|
mode: requireString(body, "mode")
|
|
9300
9728
|
}, requestOperationSignal(request, options.shutdownSignal));
|
|
9301
|
-
return reply.code(201).send(
|
|
9729
|
+
return reply.code(201).send({
|
|
9730
|
+
...record,
|
|
9731
|
+
active: runs.isActive(record.id)
|
|
9732
|
+
});
|
|
9302
9733
|
});
|
|
9303
|
-
app.delete("/v1/runs/:runId", async (request) => {
|
|
9734
|
+
app.delete("/v1/runs/:runId", { schema: AgentServerRouteSchemas.stopRun }, async (request) => {
|
|
9304
9735
|
requirePairedOrigin(request);
|
|
9305
9736
|
const { runId } = request.params;
|
|
9306
9737
|
return runs.stop(runId);
|
|
@@ -9310,13 +9741,14 @@ function registerRunRoutes(app, options, requirePairedOrigin) {
|
|
|
9310
9741
|
function registerRunLogRoute(app, options, requirePairedOrigin) {
|
|
9311
9742
|
const { runs, store } = options;
|
|
9312
9743
|
let openStreams = 0;
|
|
9313
|
-
app.get("/v1/runs/:runId/logs", async (request, reply) => {
|
|
9744
|
+
app.get("/v1/runs/:runId/logs", { schema: AgentServerRouteSchemas.streamRunLogs }, async (request, reply) => {
|
|
9314
9745
|
requirePairedOrigin(request);
|
|
9315
9746
|
const { runId } = request.params;
|
|
9316
9747
|
const record = runs.status(runId);
|
|
9317
9748
|
store.resolveRunLogPath(record.id);
|
|
9318
|
-
if (openStreams >= MAX_LOG_STREAMS) throw new
|
|
9749
|
+
if (openStreams >= MAX_LOG_STREAMS) throw new AgentServerHttpError(429, "rate_limited", "Too many concurrent log streams");
|
|
9319
9750
|
openStreams += 1;
|
|
9751
|
+
reply.hijack();
|
|
9320
9752
|
reply.raw.writeHead(200, {
|
|
9321
9753
|
"content-type": "text/event-stream",
|
|
9322
9754
|
"cache-control": "no-store",
|
|
@@ -9355,7 +9787,7 @@ function registerRunLogRoute(app, options, requirePairedOrigin) {
|
|
|
9355
9787
|
const push = async () => {
|
|
9356
9788
|
const handle = await open(store.resolveRunLogPath(record.id), constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
9357
9789
|
try {
|
|
9358
|
-
const { lines, omitted } = await
|
|
9790
|
+
const { lines, omitted } = await readAgentServerLogDelta(handle, readState);
|
|
9359
9791
|
if (omitted) await writeData("[older log output omitted]");
|
|
9360
9792
|
for (const line of lines) await writeData(line);
|
|
9361
9793
|
} finally {
|
|
@@ -9366,9 +9798,9 @@ function registerRunLogRoute(app, options, requirePairedOrigin) {
|
|
|
9366
9798
|
if (closed) return;
|
|
9367
9799
|
request.log.warn({
|
|
9368
9800
|
...safeErrorContext(error),
|
|
9369
|
-
code: "
|
|
9801
|
+
code: "agent_server_log_tail_failed",
|
|
9370
9802
|
runId
|
|
9371
|
-
}, "
|
|
9803
|
+
}, "Agent Server log tail failed");
|
|
9372
9804
|
finish(true);
|
|
9373
9805
|
};
|
|
9374
9806
|
const schedule = () => {
|
|
@@ -9381,7 +9813,7 @@ function registerRunLogRoute(app, options, requirePairedOrigin) {
|
|
|
9381
9813
|
}, LOG_POLL_INTERVAL_MS);
|
|
9382
9814
|
pollTimer.unref();
|
|
9383
9815
|
};
|
|
9384
|
-
|
|
9816
|
+
reply.raw.on("close", () => finish());
|
|
9385
9817
|
try {
|
|
9386
9818
|
await push();
|
|
9387
9819
|
if (runs.isActive(record.id)) schedule();
|
|
@@ -9426,8 +9858,8 @@ function corsHeadersFor(request, options) {
|
|
|
9426
9858
|
function isConfiguredOrigin(origin, options) {
|
|
9427
9859
|
return typeof origin === "string" && (options.allowedOrigins.includes(origin) || origin === options.selfOrigin);
|
|
9428
9860
|
}
|
|
9429
|
-
function
|
|
9430
|
-
if (error instanceof
|
|
9861
|
+
function normalizeAgentServerError(error) {
|
|
9862
|
+
if (error instanceof AgentServerHttpError) return {
|
|
9431
9863
|
statusCode: error.statusCode,
|
|
9432
9864
|
code: error.code,
|
|
9433
9865
|
message: error.message
|
|
@@ -9437,32 +9869,32 @@ function normalizeServeError(error) {
|
|
|
9437
9869
|
code: error.kind,
|
|
9438
9870
|
message: error.message
|
|
9439
9871
|
};
|
|
9440
|
-
if (error instanceof
|
|
9872
|
+
if (error instanceof AgentServerPairingError) return {
|
|
9441
9873
|
statusCode: error.code === "pairing_not_found" ? 404 : error.code === "pairing_token_invalid" || error.code === "pairing_not_approved" ? 401 : 403,
|
|
9442
9874
|
code: error.code,
|
|
9443
9875
|
message: error.message
|
|
9444
9876
|
};
|
|
9445
|
-
if (error instanceof
|
|
9877
|
+
if (error instanceof AgentServerStoreError) return {
|
|
9446
9878
|
statusCode: error.code === "not_found" ? 404 : error.code === "io_error" ? 500 : 400,
|
|
9447
9879
|
code: error.code,
|
|
9448
9880
|
message: error.message
|
|
9449
9881
|
};
|
|
9450
|
-
if (error instanceof
|
|
9882
|
+
if (error instanceof AgentServerRunError) return {
|
|
9451
9883
|
statusCode: error.code === "run_not_found" ? 404 : 400,
|
|
9452
9884
|
code: error.code,
|
|
9453
9885
|
message: error.message
|
|
9454
9886
|
};
|
|
9455
|
-
if (error instanceof
|
|
9887
|
+
if (error instanceof AgentServerSubscriptionError) return {
|
|
9456
9888
|
statusCode: error.code === "login_not_found" ? 404 : error.code === "provider_unknown" ? 404 : 400,
|
|
9457
9889
|
code: error.code,
|
|
9458
9890
|
message: error.message
|
|
9459
9891
|
};
|
|
9460
|
-
if (error instanceof
|
|
9892
|
+
if (error instanceof AgentServerModelDiscoveryError) return {
|
|
9461
9893
|
statusCode: error.statusCode,
|
|
9462
9894
|
code: error.code,
|
|
9463
9895
|
message: error.message
|
|
9464
9896
|
};
|
|
9465
|
-
if (error instanceof
|
|
9897
|
+
if (error instanceof AgentServerIdentityError) return {
|
|
9466
9898
|
statusCode: error.code === "agent_exists" ? 409 : 400,
|
|
9467
9899
|
code: error.code,
|
|
9468
9900
|
message: error.message
|
|
@@ -9474,9 +9906,9 @@ function normalizeServeError(error) {
|
|
|
9474
9906
|
};
|
|
9475
9907
|
}
|
|
9476
9908
|
//#endregion
|
|
9477
|
-
//#region src/cli/
|
|
9909
|
+
//#region src/cli/server.ts
|
|
9478
9910
|
/**
|
|
9479
|
-
* `moltnet-agent
|
|
9911
|
+
* `moltnet-agent server` — per-user loopback supervisor (#2061).
|
|
9480
9912
|
*
|
|
9481
9913
|
* Starts nothing on its own: it binds 127.0.0.1 and waits for a paired
|
|
9482
9914
|
* Console origin to configure agents/providers and start/stop runs.
|
|
@@ -9485,12 +9917,12 @@ var DEFAULT_PORT = 17374;
|
|
|
9485
9917
|
var DEFAULT_ALLOWED_ORIGINS = "https://console.themolt.net";
|
|
9486
9918
|
var DEFAULT_API_URL = "https://api.themolt.net";
|
|
9487
9919
|
var SHUTDOWN_TIMEOUT_MS = 15e3;
|
|
9488
|
-
async function
|
|
9920
|
+
async function runAgentServer(argv) {
|
|
9489
9921
|
if (isHelpFlag(argv)) {
|
|
9490
|
-
console.log(
|
|
9922
|
+
console.log(AGENT_SERVER_HELP);
|
|
9491
9923
|
return 0;
|
|
9492
9924
|
}
|
|
9493
|
-
const envConfig =
|
|
9925
|
+
const envConfig = loadAgentServerEnvConfig();
|
|
9494
9926
|
const { values } = parseArgs({
|
|
9495
9927
|
args: argv,
|
|
9496
9928
|
options: {
|
|
@@ -9506,19 +9938,19 @@ async function runServe(argv) {
|
|
|
9506
9938
|
return 1;
|
|
9507
9939
|
}
|
|
9508
9940
|
const allowedOrigins = parseAllowedOrigins(values["allowed-origins"] ?? (envConfig.allowedOrigins || DEFAULT_ALLOWED_ORIGINS));
|
|
9509
|
-
const root = values.root ??
|
|
9941
|
+
const root = values.root ?? resolveAgentServerRoot({
|
|
9510
9942
|
root: envConfig.root,
|
|
9511
9943
|
xdgConfigHome: envConfig.xdgConfigHome
|
|
9512
9944
|
});
|
|
9513
9945
|
const defaultApiUrl = values["api-url"] ?? (envConfig.apiUrl || DEFAULT_API_URL);
|
|
9514
|
-
const store = new
|
|
9946
|
+
const store = new AgentServerStore(root).ensure();
|
|
9515
9947
|
const { logger, shutdown: shutdownLogger } = createRootLogger({
|
|
9516
|
-
name: "agent-daemon.
|
|
9948
|
+
name: "agent-daemon.server",
|
|
9517
9949
|
level: envConfig.logLevel || "info"
|
|
9518
9950
|
});
|
|
9519
9951
|
try {
|
|
9520
9952
|
try {
|
|
9521
|
-
return await
|
|
9953
|
+
return await withAgentServerLock(root, async () => {
|
|
9522
9954
|
const secrets = new FileSecretProvider({
|
|
9523
9955
|
root: store.secretsDir,
|
|
9524
9956
|
writable: true
|
|
@@ -9527,7 +9959,7 @@ async function runServe(argv) {
|
|
|
9527
9959
|
const externalSecretProviders = createNodeSecretProviderRegistry();
|
|
9528
9960
|
const pairing = new PairingService();
|
|
9529
9961
|
const shutdownController = new AbortController();
|
|
9530
|
-
const subscriptions =
|
|
9962
|
+
const subscriptions = await ProviderLoginService.create({
|
|
9531
9963
|
authPath: store.piAuthJsonPath,
|
|
9532
9964
|
logger
|
|
9533
9965
|
});
|
|
@@ -9538,7 +9970,7 @@ async function runServe(argv) {
|
|
|
9538
9970
|
baseEnv: processEnvSnapshot(),
|
|
9539
9971
|
logger
|
|
9540
9972
|
});
|
|
9541
|
-
const app =
|
|
9973
|
+
const app = buildAgentServer({
|
|
9542
9974
|
store,
|
|
9543
9975
|
secrets,
|
|
9544
9976
|
secretProviders,
|
|
@@ -9558,11 +9990,11 @@ async function runServe(argv) {
|
|
|
9558
9990
|
host: "127.0.0.1",
|
|
9559
9991
|
port
|
|
9560
9992
|
});
|
|
9561
|
-
console.error(`moltnet-agent
|
|
9993
|
+
console.error(`moltnet-agent server listening on ${address}`);
|
|
9562
9994
|
console.error(`config root: ${root}`);
|
|
9563
9995
|
console.error(`allowed origins: ${allowedOrigins.join(", ")}`);
|
|
9564
9996
|
console.error("Pair from the Console \"Local runtime\" page; approve the one-click prompt this server opens.");
|
|
9565
|
-
return await
|
|
9997
|
+
return await waitForAgentServerShutdown(runs, app, shutdownController);
|
|
9566
9998
|
} catch (cause) {
|
|
9567
9999
|
await app.close().catch(() => void 0);
|
|
9568
10000
|
throw cause;
|
|
@@ -9573,7 +10005,7 @@ async function runServe(argv) {
|
|
|
9573
10005
|
process.kill(process.pid, "SIGTERM");
|
|
9574
10006
|
} });
|
|
9575
10007
|
} catch (cause) {
|
|
9576
|
-
if (cause instanceof
|
|
10008
|
+
if (cause instanceof AgentServerLockError) {
|
|
9577
10009
|
console.error(cause.message);
|
|
9578
10010
|
return 1;
|
|
9579
10011
|
}
|
|
@@ -9583,7 +10015,7 @@ async function runServe(argv) {
|
|
|
9583
10015
|
await shutdownLogger();
|
|
9584
10016
|
}
|
|
9585
10017
|
}
|
|
9586
|
-
function
|
|
10018
|
+
function waitForAgentServerShutdown(runs, app, shutdownController) {
|
|
9587
10019
|
return new Promise((resolvePromise) => {
|
|
9588
10020
|
let shuttingDown = false;
|
|
9589
10021
|
const shutdown = () => {
|
|
@@ -9808,7 +10240,7 @@ async function runAgentDaemonCli(options) {
|
|
|
9808
10240
|
case "poll": return runPoll(rest, options.runtime);
|
|
9809
10241
|
case "once": return runOnce(rest, options.runtime);
|
|
9810
10242
|
case "drain": return runDrain(rest, options.runtime);
|
|
9811
|
-
case "
|
|
10243
|
+
case "server": return runAgentServer(rest);
|
|
9812
10244
|
case "sync-sessions": return runSyncSessions(rest);
|
|
9813
10245
|
case "-h":
|
|
9814
10246
|
case "--help":
|