@themoltnet/agent-daemon 0.51.0 → 0.53.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +16 -8
- package/dist/cli.js +279 -15
- package/package.json +11 -9
package/README.md
CHANGED
|
@@ -100,7 +100,7 @@ environment only — never store it in `moltnet.json`. The daemon reconciles a
|
|
|
100
100
|
team-bound key against `--team` at startup; an identity-scoped key may select
|
|
101
101
|
any team where the agent is authorized. It fails fast if the key is rejected,
|
|
102
102
|
is not an agent, or a team binding mismatches. See
|
|
103
|
-
[Run the daemon with an agent key](../../docs/operate/
|
|
103
|
+
[Run the daemon with an agent key](../../docs/operate/agent-keys.md#run-the-daemon-with-an-agent-key).
|
|
104
104
|
|
|
105
105
|
Daemon authentication and the guest boundary are two separate concerns. How the
|
|
106
106
|
daemon authenticates (an agent key, or OAuth2 resolved from
|
|
@@ -135,14 +135,22 @@ of `MOLTNET_PRIVATE_KEY`.
|
|
|
135
135
|
An agent key used by the daemon needs this least-privilege scope set:
|
|
136
136
|
|
|
137
137
|
```text
|
|
138
|
-
agent:profile runtime:read task:read task:claim task:execute
|
|
138
|
+
agent:profile crypto:sign runtime:read task:read task:claim task:execute
|
|
139
139
|
```
|
|
140
140
|
|
|
141
|
-
The Console selects these
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
141
|
+
The Console selects these scopes by default. Knowledge-enabled workers must add
|
|
142
|
+
`diary:read`, `diary:write`, `pack:read`, and `pack:write` when the key is
|
|
143
|
+
issued.
|
|
144
|
+
|
|
145
|
+
`crypto:sign` is in the minimum because host-capability signing runs on the
|
|
146
|
+
daemon's own credential: the local seed signer calls the signing-request
|
|
147
|
+
endpoints, which require it. Without it the daemon refuses to start rather than
|
|
148
|
+
booting cleanly and failing the first time guest code signs a diary entry or a
|
|
149
|
+
commit. Keys issued with the older five-scope set must be reissued.
|
|
150
|
+
|
|
151
|
+
Runtime policy can narrow key authority but cannot add missing scopes, and
|
|
152
|
+
existing keys are never widened automatically; issue a replacement credential
|
|
153
|
+
when broader authority is required.
|
|
146
154
|
|
|
147
155
|
### Pi provider auth
|
|
148
156
|
|
|
@@ -196,7 +204,7 @@ host-exec command is safe to run without a dialog.
|
|
|
196
204
|
### Remote runtime profiles
|
|
197
205
|
|
|
198
206
|
The canonical user-facing guide lives in the public docs:
|
|
199
|
-
[Running Agents § Runtime Profiles](https://docs.themolt.net/operate/
|
|
207
|
+
[Running Agents § Runtime Profiles](https://docs.themolt.net/operate/runtime-profiles).
|
|
200
208
|
|
|
201
209
|
## Correlation anchors
|
|
202
210
|
|
package/dist/cli.js
CHANGED
|
@@ -16,7 +16,7 @@ import { FILE_SECRET_PROVIDER, FileSecretProvider, connect, createNodeSecretProv
|
|
|
16
16
|
import { execFile, execFileSync, spawn } from "node:child_process";
|
|
17
17
|
import { constants, createReadStream, createWriteStream, existsSync, lstatSync, mkdirSync, openSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
|
|
18
18
|
import { AuthenticationError, MoltNetError, agentKeyKey, assertTrustedConfigApiUrl, createExecutorAttestor, deriveMcpUrl, formatSecretReferenceString, identitySeedKey, parseSecretReferenceString, readConfig, register, requireSecureCredentialApiUrl, resolveAgentKey, resolveEnvSecretReference, resolveIdentitySeed, resolveOAuth2ClientSecret } from "@themoltnet/sdk";
|
|
19
|
-
import { createHash, randomBytes, randomUUID, timingSafeEqual } from "node:crypto";
|
|
19
|
+
import { X509Certificate, createHash, createPrivateKey, createPublicKey, randomBytes, randomUUID, timingSafeEqual, webcrypto } from "node:crypto";
|
|
20
20
|
import { once } from "node:events";
|
|
21
21
|
import { metrics } from "@opentelemetry/api";
|
|
22
22
|
import { OTLPMetricExporter } from "@opentelemetry/exporter-metrics-otlp-proto";
|
|
@@ -29,6 +29,7 @@ import { ATTR_SERVICE_NAME, ATTR_SERVICE_VERSION } from "@opentelemetry/semantic
|
|
|
29
29
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
30
30
|
import { mkdir, open, readFile, realpath, stat, writeFile } from "node:fs/promises";
|
|
31
31
|
import { pipeline } from "node:stream/promises";
|
|
32
|
+
import { createInterface } from "node:readline/promises";
|
|
32
33
|
import cors from "@fastify/cors";
|
|
33
34
|
import helmet from "@fastify/helmet";
|
|
34
35
|
import { lock, lockSync } from "proper-lockfile";
|
|
@@ -41,6 +42,8 @@ import { StringDecoder } from "node:string_decoder";
|
|
|
41
42
|
import rateLimit from "@fastify/rate-limit";
|
|
42
43
|
import Fastify from "fastify";
|
|
43
44
|
import { isIP } from "node:net";
|
|
45
|
+
import "reflect-metadata";
|
|
46
|
+
import { BasicConstraintsExtension, ExtendedKeyUsage, ExtendedKeyUsageExtension, IP, KeyUsageFlags, KeyUsagesExtension, SubjectAlternativeNameExtension, X509CertificateGenerator } from "@peculiar/x509";
|
|
44
47
|
import { createGzip } from "node:zlib";
|
|
45
48
|
//#region ../../libs/tasks/src/rubric.ts
|
|
46
49
|
/**
|
|
@@ -776,9 +779,16 @@ var ALL_CREDENTIAL_SCOPES = Object.freeze(Object.values(CREDENTIAL_SCOPES));
|
|
|
776
779
|
/**
|
|
777
780
|
* Minimum grant for the agent daemon. Task credentials attenuate this further
|
|
778
781
|
* to `task:execute` alone.
|
|
782
|
+
*
|
|
783
|
+
* `crypto:sign` is part of the minimum because host-capability signing runs on
|
|
784
|
+
* the daemon's own credential: the local seed signer calls the signing-request
|
|
785
|
+
* endpoints, which require it. A grant without it produces a daemon that boots
|
|
786
|
+
* cleanly and then fails the first time guest code signs a diary entry or a
|
|
787
|
+
* commit.
|
|
779
788
|
*/
|
|
780
789
|
var AGENT_CREDENTIAL_SCOPES = [
|
|
781
790
|
CREDENTIAL_SCOPES.AgentProfile,
|
|
791
|
+
CREDENTIAL_SCOPES.CryptoSign,
|
|
782
792
|
CREDENTIAL_SCOPES.RuntimeRead,
|
|
783
793
|
CREDENTIAL_SCOPES.TaskRead,
|
|
784
794
|
CREDENTIAL_SCOPES.TaskClaim,
|
|
@@ -3431,6 +3441,8 @@ Commands:
|
|
|
3431
3441
|
server Loopback supervisor for console-managed runs: pairing,
|
|
3432
3442
|
agent/provider config store, and start/stop of poll/drain
|
|
3433
3443
|
child processes. Binds 127.0.0.1 only.
|
|
3444
|
+
server trust
|
|
3445
|
+
Install the per-user macOS local-HTTPS CA after explicit consent.
|
|
3434
3446
|
sync-sessions
|
|
3435
3447
|
Repair durable runtime-session checkpoints from local slot files.
|
|
3436
3448
|
update check
|
|
@@ -3582,6 +3594,10 @@ Options:
|
|
|
3582
3594
|
(or MOLTNET_AGENT_SERVER_ROOT).
|
|
3583
3595
|
--api-url <url> Default MoltNet API for new managed agents.
|
|
3584
3596
|
Default: https://api.themolt.net.
|
|
3597
|
+
|
|
3598
|
+
On macOS, the first interactive run asks to trust a per-user local CA in the
|
|
3599
|
+
login keychain and serves HTTPS. Run \`agent-daemon server trust --remove\` to
|
|
3600
|
+
remove that exact CA. Linux continues to use the Chromium PNA HTTP path.
|
|
3585
3601
|
`;
|
|
3586
3602
|
//#endregion
|
|
3587
3603
|
//#region src/lib/identity-pin.ts
|
|
@@ -3845,7 +3861,7 @@ async function resolveDaemonAgentIdentity(input) {
|
|
|
3845
3861
|
}
|
|
3846
3862
|
//#endregion
|
|
3847
3863
|
//#region src/lib/correlation.ts
|
|
3848
|
-
var execFileAsync = promisify(execFile);
|
|
3864
|
+
var execFileAsync$1 = promisify(execFile);
|
|
3849
3865
|
var CORRELATION_TRAILER_KEY = "Moltnet-Correlation-Id";
|
|
3850
3866
|
var CORRELATION_MARKER_RE = /<!--\s*moltnet-correlation:\s*([\w-]+)\s*-->/i;
|
|
3851
3867
|
new RegExp(`^${CORRELATION_TRAILER_KEY}:\\s*(\\S+)\\s*$`, "m");
|
|
@@ -3892,7 +3908,7 @@ function makePrBodyAnchorWriter(deps) {
|
|
|
3892
3908
|
function createGhCliClient() {
|
|
3893
3909
|
return {
|
|
3894
3910
|
async get({ owner, repo, number }) {
|
|
3895
|
-
const { stdout } = await execFileAsync("gh", [
|
|
3911
|
+
const { stdout } = await execFileAsync$1("gh", [
|
|
3896
3912
|
"api",
|
|
3897
3913
|
`repos/${owner}/${repo}/pulls/${number}`,
|
|
3898
3914
|
"--jq",
|
|
@@ -3901,7 +3917,7 @@ function createGhCliClient() {
|
|
|
3901
3917
|
return JSON.parse(stdout);
|
|
3902
3918
|
},
|
|
3903
3919
|
async patch({ owner, repo, number }, body) {
|
|
3904
|
-
await execFileAsync("gh", [
|
|
3920
|
+
await execFileAsync$1("gh", [
|
|
3905
3921
|
"api",
|
|
3906
3922
|
"-X",
|
|
3907
3923
|
"PATCH",
|
|
@@ -7366,6 +7382,12 @@ function renderPairingApprovalPage(input) {
|
|
|
7366
7382
|
</style>
|
|
7367
7383
|
</head>
|
|
7368
7384
|
<body>
|
|
7385
|
+
<script>
|
|
7386
|
+
// The Console must remain this popup's opener until it finishes navigating
|
|
7387
|
+
// from about:blank. Safari rejects that cross-origin navigation otherwise.
|
|
7388
|
+
// Once this trusted local approval document has loaded, it needs no opener.
|
|
7389
|
+
window.opener = null;
|
|
7390
|
+
<\/script>
|
|
7369
7391
|
<main>
|
|
7370
7392
|
<h1>Allow this site to manage local MoltNet agents?</h1>
|
|
7371
7393
|
<p><code>${escapeHtml(input.origin)}</code> asks to configure agents and start or stop local daemon runs on this machine.</p>
|
|
@@ -8148,7 +8170,7 @@ async function createManagedAgent(store, secrets, input, connectAgent = connect)
|
|
|
8148
8170
|
} catch (cause) {
|
|
8149
8171
|
if (!registeredIdentityId && cause instanceof MoltNetError && cause.statusCode !== void 0 && cause.statusCode >= 400 && cause.statusCode < 500) {
|
|
8150
8172
|
store.clearPendingRegistration(alias);
|
|
8151
|
-
throw new AgentServerIdentityError("registration_failed",
|
|
8173
|
+
throw new AgentServerIdentityError("registration_failed", registrationRejectionMessage(alias, cause), { cause });
|
|
8152
8174
|
}
|
|
8153
8175
|
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 });
|
|
8154
8176
|
throw cause;
|
|
@@ -8156,6 +8178,9 @@ async function createManagedAgent(store, secrets, input, connectAgent = connect)
|
|
|
8156
8178
|
releaseAlias();
|
|
8157
8179
|
}
|
|
8158
8180
|
}
|
|
8181
|
+
function registrationRejectionMessage(alias, cause) {
|
|
8182
|
+
return `registration for "${alias}" was rejected${cause.statusCode === void 0 ? "" : ` (${cause.statusCode})`}: ${cause.detail?.trim() || cause.message}`;
|
|
8183
|
+
}
|
|
8159
8184
|
/** Resume a fully persisted registration or explicitly abandon local recovery. */
|
|
8160
8185
|
async function reconcileManagedRegistration(store, secrets, aliasInput, action, connectAgent = connect, signal) {
|
|
8161
8186
|
const alias = assertStoreName("agent name", aliasInput);
|
|
@@ -9416,6 +9441,16 @@ var AgentServerRouteSchemas = {
|
|
|
9416
9441
|
...problemResponse
|
|
9417
9442
|
}
|
|
9418
9443
|
},
|
|
9444
|
+
deleteProvider: {
|
|
9445
|
+
operationId: "deleteAgentServerProvider",
|
|
9446
|
+
tags: ["providers"],
|
|
9447
|
+
security: pairedSecurity,
|
|
9448
|
+
params: ProviderParamsSchema,
|
|
9449
|
+
response: {
|
|
9450
|
+
204: Type.Any(),
|
|
9451
|
+
...problemResponse
|
|
9452
|
+
}
|
|
9453
|
+
},
|
|
9419
9454
|
listSubscriptions: {
|
|
9420
9455
|
operationId: "listAgentServerSubscriptions",
|
|
9421
9456
|
tags: ["subscriptions"],
|
|
@@ -9600,10 +9635,14 @@ function requestOperationSignal(request, shutdownSignal) {
|
|
|
9600
9635
|
}
|
|
9601
9636
|
function buildAgentServer(options) {
|
|
9602
9637
|
const { pairing } = options;
|
|
9603
|
-
const
|
|
9638
|
+
const fastifyOptions = {
|
|
9604
9639
|
bodyLimit: BODY_LIMIT,
|
|
9640
|
+
...options.tls ? { https: options.tls } : {}
|
|
9641
|
+
};
|
|
9642
|
+
const app = options.logger ? Fastify({
|
|
9643
|
+
...fastifyOptions,
|
|
9605
9644
|
loggerInstance: options.logger
|
|
9606
|
-
}) : Fastify(
|
|
9645
|
+
}) : Fastify(fastifyOptions);
|
|
9607
9646
|
options.registerOpenApi?.(app);
|
|
9608
9647
|
for (const schema of AGENT_SERVER_SCHEMAS) app.addSchema(schema);
|
|
9609
9648
|
registerLoopbackSecurity(app, {
|
|
@@ -9662,12 +9701,12 @@ function buildAgentServer(options) {
|
|
|
9662
9701
|
}));
|
|
9663
9702
|
app.setErrorHandler(async (error, request, reply) => {
|
|
9664
9703
|
const { statusCode, code, message } = normalizeAgentServerError(error);
|
|
9665
|
-
if (statusCode === 500) request.log
|
|
9704
|
+
if (statusCode === 500 || error instanceof AgentServerIdentityError) request.log[statusCode === 500 ? "error" : "warn"]({
|
|
9666
9705
|
...safeErrorContext(error),
|
|
9667
|
-
code: "agent_server_request_failed",
|
|
9706
|
+
code: statusCode === 500 ? "agent_server_request_failed" : "agent_server_identity_rejected",
|
|
9668
9707
|
method: request.method,
|
|
9669
9708
|
route: request.routeOptions.url
|
|
9670
|
-
}, "AgentServer request failed");
|
|
9709
|
+
}, statusCode === 500 ? "AgentServer request failed" : "AgentServer identity request rejected");
|
|
9671
9710
|
return reply.code(statusCode).send({
|
|
9672
9711
|
code,
|
|
9673
9712
|
message
|
|
@@ -9894,12 +9933,29 @@ function registerProviderRoutes(app, options, requirePairedOrigin) {
|
|
|
9894
9933
|
provider: FILE_SECRET_PROVIDER,
|
|
9895
9934
|
key
|
|
9896
9935
|
});
|
|
9897
|
-
} else if (providers[providerId]?.apiKeyRef
|
|
9936
|
+
} else if (providers[providerId]?.apiKeyRef) entry.apiKeyRef = providers[providerId].apiKeyRef;
|
|
9898
9937
|
providers[providerId] = entry;
|
|
9899
9938
|
store.writeProviders(providers);
|
|
9900
9939
|
});
|
|
9901
9940
|
return reply.code(200).send(providerView(entry));
|
|
9902
9941
|
});
|
|
9942
|
+
app.delete("/v1/providers/:providerId", {
|
|
9943
|
+
schema: AgentServerRouteSchemas.deleteProvider,
|
|
9944
|
+
attachValidation: true
|
|
9945
|
+
}, async (request, reply) => {
|
|
9946
|
+
requirePairedOrigin(request);
|
|
9947
|
+
const { providerId: rawProviderId } = request.params;
|
|
9948
|
+
const providerId = assertProviderId(rawProviderId);
|
|
9949
|
+
await serialize(async () => {
|
|
9950
|
+
const providers = store.readProviders();
|
|
9951
|
+
const provider = providers[providerId];
|
|
9952
|
+
if (!provider) throw new AgentServerHttpError(404, "agent_server_provider_not_found", `Provider ${providerId} was not found`);
|
|
9953
|
+
if (provider.apiKeyRef) await options.secrets.delete(`pi-provider/${providerId}`);
|
|
9954
|
+
delete providers[providerId];
|
|
9955
|
+
store.writeProviders(providers);
|
|
9956
|
+
});
|
|
9957
|
+
return reply.code(204).send(null);
|
|
9958
|
+
});
|
|
9903
9959
|
}
|
|
9904
9960
|
function runViews(runs) {
|
|
9905
9961
|
return runs.list(RUN_HISTORY_LIMIT).map((record) => ({
|
|
@@ -10051,12 +10107,23 @@ function safeErrorContext(error) {
|
|
|
10051
10107
|
const applicationCode = safeErrorToken(error?.code);
|
|
10052
10108
|
if (applicationCode) context["applicationCode"] = applicationCode;
|
|
10053
10109
|
const cause = error instanceof Error ? error.cause : void 0;
|
|
10110
|
+
if (cause instanceof Error) {
|
|
10111
|
+
context["causeType"] = cause.name;
|
|
10112
|
+
const causeMessage = safeLogMessage(cause.message);
|
|
10113
|
+
if (causeMessage) context["causeMessage"] = causeMessage;
|
|
10114
|
+
}
|
|
10054
10115
|
const fsCode = safeErrorToken(cause?.code);
|
|
10055
10116
|
const syscall = safeErrorToken(cause?.syscall);
|
|
10056
10117
|
if (fsCode) context["fsCode"] = fsCode;
|
|
10057
10118
|
if (syscall) context["syscall"] = syscall;
|
|
10119
|
+
const causeStatus = cause?.statusCode;
|
|
10120
|
+
if (typeof causeStatus === "number") context["causeStatusCode"] = causeStatus;
|
|
10058
10121
|
return context;
|
|
10059
10122
|
}
|
|
10123
|
+
function safeLogMessage(value) {
|
|
10124
|
+
const normalized = value.replace(/[\r\n\t]/gu, " ").trim();
|
|
10125
|
+
return normalized ? normalized.slice(0, 500) : void 0;
|
|
10126
|
+
}
|
|
10060
10127
|
function safeErrorToken(value) {
|
|
10061
10128
|
return typeof value === "string" && /^[a-z0-9_:-]{1,64}$/iu.test(value) ? value : void 0;
|
|
10062
10129
|
}
|
|
@@ -10128,6 +10195,163 @@ function normalizeAgentServerError(error) {
|
|
|
10128
10195
|
};
|
|
10129
10196
|
}
|
|
10130
10197
|
//#endregion
|
|
10198
|
+
//#region src/lib/agent-server/tls.ts
|
|
10199
|
+
var execFileAsync = promisify(execFile);
|
|
10200
|
+
var CA_COMMON_NAME = "MoltNet Local Agent CA";
|
|
10201
|
+
var LEAF_COMMON_NAME = "MoltNet Local Agent";
|
|
10202
|
+
var RENEW_BEFORE_MS = 720 * 60 * 60 * 1e3;
|
|
10203
|
+
function loginKeychainPath() {
|
|
10204
|
+
return join(homedir(), "Library", "Keychains", "login.keychain-db");
|
|
10205
|
+
}
|
|
10206
|
+
function pemPrivateKey(key) {
|
|
10207
|
+
return webcrypto.subtle.exportKey("pkcs8", key).then((der) => createPrivateKey({
|
|
10208
|
+
key: Buffer.from(der),
|
|
10209
|
+
format: "der",
|
|
10210
|
+
type: "pkcs8"
|
|
10211
|
+
}).export({
|
|
10212
|
+
format: "pem",
|
|
10213
|
+
type: "pkcs8"
|
|
10214
|
+
}).toString());
|
|
10215
|
+
}
|
|
10216
|
+
async function importCaKeyPair(pem) {
|
|
10217
|
+
const privateKey = createPrivateKey(pem);
|
|
10218
|
+
const privateDer = privateKey.export({
|
|
10219
|
+
format: "der",
|
|
10220
|
+
type: "pkcs8"
|
|
10221
|
+
});
|
|
10222
|
+
const publicDer = createPublicKey(privateKey).export({
|
|
10223
|
+
format: "der",
|
|
10224
|
+
type: "spki"
|
|
10225
|
+
});
|
|
10226
|
+
const [privateCryptoKey, publicCryptoKey] = await Promise.all([webcrypto.subtle.importKey("pkcs8", privateDer, {
|
|
10227
|
+
name: "ECDSA",
|
|
10228
|
+
namedCurve: "P-256"
|
|
10229
|
+
}, false, ["sign"]), webcrypto.subtle.importKey("spki", publicDer, {
|
|
10230
|
+
name: "ECDSA",
|
|
10231
|
+
namedCurve: "P-256"
|
|
10232
|
+
}, false, ["verify"])]);
|
|
10233
|
+
return {
|
|
10234
|
+
privateKey: privateCryptoKey,
|
|
10235
|
+
publicKey: publicCryptoKey
|
|
10236
|
+
};
|
|
10237
|
+
}
|
|
10238
|
+
async function localTlsMaterialFromDirectory(dir) {
|
|
10239
|
+
try {
|
|
10240
|
+
const [key, cert, ca] = await Promise.all([
|
|
10241
|
+
readFile(join(dir, "loopback-key.pem"), "utf8"),
|
|
10242
|
+
readFile(join(dir, "loopback-cert.pem"), "utf8"),
|
|
10243
|
+
readFile(join(dir, "local-ca.pem"), "utf8")
|
|
10244
|
+
]);
|
|
10245
|
+
const parsed = new X509Certificate(cert);
|
|
10246
|
+
if (Date.parse(parsed.validTo) - Date.now() > RENEW_BEFORE_MS) return {
|
|
10247
|
+
key,
|
|
10248
|
+
cert,
|
|
10249
|
+
ca,
|
|
10250
|
+
fingerprint: new X509Certificate(ca).fingerprint256
|
|
10251
|
+
};
|
|
10252
|
+
} catch {}
|
|
10253
|
+
return null;
|
|
10254
|
+
}
|
|
10255
|
+
/** Creates a per-user CA and loopback-only leaf certificate under a 0700 directory. */
|
|
10256
|
+
async function ensureLocalTlsMaterial(root) {
|
|
10257
|
+
const dir = join(root, "tls");
|
|
10258
|
+
await mkdir(dir, {
|
|
10259
|
+
recursive: true,
|
|
10260
|
+
mode: 448
|
|
10261
|
+
});
|
|
10262
|
+
const existing = await localTlsMaterialFromDirectory(dir);
|
|
10263
|
+
if (existing) return existing;
|
|
10264
|
+
let ca;
|
|
10265
|
+
let caKeys;
|
|
10266
|
+
let caKey;
|
|
10267
|
+
try {
|
|
10268
|
+
ca = await readFile(join(dir, "local-ca.pem"), "utf8");
|
|
10269
|
+
caKeys = await importCaKeyPair(await readFile(join(dir, "local-ca-key.pem"), "utf8"));
|
|
10270
|
+
} catch {
|
|
10271
|
+
caKeys = await webcrypto.subtle.generateKey({
|
|
10272
|
+
name: "ECDSA",
|
|
10273
|
+
namedCurve: "P-256"
|
|
10274
|
+
}, true, ["sign", "verify"]);
|
|
10275
|
+
ca = (await X509CertificateGenerator.createSelfSigned({
|
|
10276
|
+
name: `CN=${CA_COMMON_NAME}`,
|
|
10277
|
+
keys: caKeys,
|
|
10278
|
+
notAfter: new Date(Date.now() + 10 * 365 * 24 * 60 * 60 * 1e3),
|
|
10279
|
+
extensions: [new BasicConstraintsExtension(true, void 0, true), new KeyUsagesExtension(KeyUsageFlags.keyCertSign | KeyUsageFlags.cRLSign, true)]
|
|
10280
|
+
})).toString("pem");
|
|
10281
|
+
caKey = await pemPrivateKey(caKeys.privateKey);
|
|
10282
|
+
}
|
|
10283
|
+
const leafKeys = await webcrypto.subtle.generateKey({
|
|
10284
|
+
name: "ECDSA",
|
|
10285
|
+
namedCurve: "P-256"
|
|
10286
|
+
}, true, ["sign", "verify"]);
|
|
10287
|
+
const leafCert = await X509CertificateGenerator.create({
|
|
10288
|
+
subject: `CN=${LEAF_COMMON_NAME}`,
|
|
10289
|
+
issuer: `CN=${CA_COMMON_NAME}`,
|
|
10290
|
+
publicKey: leafKeys.publicKey,
|
|
10291
|
+
signingKey: caKeys.privateKey,
|
|
10292
|
+
notAfter: new Date(Date.now() + 365 * 24 * 60 * 60 * 1e3),
|
|
10293
|
+
extensions: [
|
|
10294
|
+
new BasicConstraintsExtension(false, void 0, true),
|
|
10295
|
+
new KeyUsagesExtension(KeyUsageFlags.digitalSignature, true),
|
|
10296
|
+
new ExtendedKeyUsageExtension([ExtendedKeyUsage.serverAuth]),
|
|
10297
|
+
new SubjectAlternativeNameExtension([{
|
|
10298
|
+
type: IP,
|
|
10299
|
+
value: "127.0.0.1"
|
|
10300
|
+
}], true)
|
|
10301
|
+
]
|
|
10302
|
+
});
|
|
10303
|
+
const key = await pemPrivateKey(leafKeys.privateKey);
|
|
10304
|
+
const cert = leafCert.toString("pem");
|
|
10305
|
+
const writes = [writeFile(join(dir, "loopback-key.pem"), key, { mode: 384 }), writeFile(join(dir, "loopback-cert.pem"), cert, { mode: 384 })];
|
|
10306
|
+
if (caKey) writes.push(writeFile(join(dir, "local-ca.pem"), ca, { mode: 384 }), writeFile(join(dir, "local-ca-key.pem"), caKey, { mode: 384 }));
|
|
10307
|
+
await Promise.all(writes);
|
|
10308
|
+
return {
|
|
10309
|
+
key,
|
|
10310
|
+
cert,
|
|
10311
|
+
ca,
|
|
10312
|
+
fingerprint: new X509Certificate(ca).fingerprint256
|
|
10313
|
+
};
|
|
10314
|
+
}
|
|
10315
|
+
async function trustLocalCa(root) {
|
|
10316
|
+
const caPath = join(root, "tls", "local-ca.pem");
|
|
10317
|
+
await execFileAsync("security", [
|
|
10318
|
+
"add-trusted-cert",
|
|
10319
|
+
"-d",
|
|
10320
|
+
"-r",
|
|
10321
|
+
"trustRoot",
|
|
10322
|
+
"-k",
|
|
10323
|
+
loginKeychainPath(),
|
|
10324
|
+
caPath
|
|
10325
|
+
]);
|
|
10326
|
+
}
|
|
10327
|
+
async function isLocalCaTrusted(root) {
|
|
10328
|
+
const ca = await readFile(join(root, "tls", "local-ca.pem"), "utf8");
|
|
10329
|
+
try {
|
|
10330
|
+
const { stdout } = await execFileAsync("security", [
|
|
10331
|
+
"find-certificate",
|
|
10332
|
+
"-a",
|
|
10333
|
+
"-p",
|
|
10334
|
+
"-c",
|
|
10335
|
+
CA_COMMON_NAME,
|
|
10336
|
+
loginKeychainPath()
|
|
10337
|
+
]);
|
|
10338
|
+
return stdout.includes(ca.trim());
|
|
10339
|
+
} catch {
|
|
10340
|
+
return false;
|
|
10341
|
+
}
|
|
10342
|
+
}
|
|
10343
|
+
async function removeLocalCa(root) {
|
|
10344
|
+
await execFileAsync("security", [
|
|
10345
|
+
"delete-certificate",
|
|
10346
|
+
"-Z",
|
|
10347
|
+
new X509Certificate(await readFile(join(root, "tls", "local-ca.pem"), "utf8")).fingerprint256.replaceAll(":", ""),
|
|
10348
|
+
loginKeychainPath()
|
|
10349
|
+
]);
|
|
10350
|
+
}
|
|
10351
|
+
function isMacos() {
|
|
10352
|
+
return process.platform === "darwin";
|
|
10353
|
+
}
|
|
10354
|
+
//#endregion
|
|
10131
10355
|
//#region src/cli/server.ts
|
|
10132
10356
|
/**
|
|
10133
10357
|
* `moltnet-agent server` — per-user loopback supervisor (#2061).
|
|
@@ -10144,14 +10368,17 @@ async function runAgentServer(argv) {
|
|
|
10144
10368
|
console.log(AGENT_SERVER_HELP);
|
|
10145
10369
|
return 0;
|
|
10146
10370
|
}
|
|
10371
|
+
const trustRequested = argv[0] === "trust";
|
|
10372
|
+
const commandArgs = trustRequested ? argv.slice(1) : argv;
|
|
10147
10373
|
const envConfig = loadAgentServerEnvConfig();
|
|
10148
10374
|
const { values } = parseArgs({
|
|
10149
|
-
args:
|
|
10375
|
+
args: commandArgs,
|
|
10150
10376
|
options: {
|
|
10151
10377
|
port: { type: "string" },
|
|
10152
10378
|
"allowed-origins": { type: "string" },
|
|
10153
10379
|
root: { type: "string" },
|
|
10154
|
-
"api-url": { type: "string" }
|
|
10380
|
+
"api-url": { type: "string" },
|
|
10381
|
+
remove: { type: "boolean" }
|
|
10155
10382
|
}
|
|
10156
10383
|
});
|
|
10157
10384
|
const port = Number.parseInt(values.port ?? (envConfig.port || `${DEFAULT_PORT}`), 10);
|
|
@@ -10166,6 +10393,7 @@ async function runAgentServer(argv) {
|
|
|
10166
10393
|
});
|
|
10167
10394
|
const defaultApiUrl = values["api-url"] ?? (envConfig.apiUrl || DEFAULT_API_URL);
|
|
10168
10395
|
const store = new AgentServerStore(root).ensure();
|
|
10396
|
+
if (trustRequested) return runTrustCommand(commandArgs, root);
|
|
10169
10397
|
const { logger, shutdown: shutdownLogger } = createRootLogger({
|
|
10170
10398
|
name: "agent-daemon.server",
|
|
10171
10399
|
level: envConfig.logLevel || "info"
|
|
@@ -10193,6 +10421,7 @@ async function runAgentServer(argv) {
|
|
|
10193
10421
|
logger,
|
|
10194
10422
|
runtimeRegistry: new RuntimeRegistry(store.root)
|
|
10195
10423
|
});
|
|
10424
|
+
const tls = isMacos() ? await ensureTrustedLocalTls(root) : void 0;
|
|
10196
10425
|
const app = buildAgentServer({
|
|
10197
10426
|
store,
|
|
10198
10427
|
secrets,
|
|
@@ -10202,7 +10431,11 @@ async function runAgentServer(argv) {
|
|
|
10202
10431
|
runs,
|
|
10203
10432
|
subscriptions,
|
|
10204
10433
|
allowedOrigins,
|
|
10205
|
-
selfOrigin:
|
|
10434
|
+
selfOrigin: `${tls ? "https" : "http"}://127.0.0.1:${port}`,
|
|
10435
|
+
...tls ? { tls: {
|
|
10436
|
+
key: tls.key,
|
|
10437
|
+
cert: tls.cert
|
|
10438
|
+
} } : {},
|
|
10206
10439
|
defaultApiUrl,
|
|
10207
10440
|
version: "dev",
|
|
10208
10441
|
logger,
|
|
@@ -10238,6 +10471,37 @@ async function runAgentServer(argv) {
|
|
|
10238
10471
|
await shutdownLogger();
|
|
10239
10472
|
}
|
|
10240
10473
|
}
|
|
10474
|
+
async function runTrustCommand(argv, root) {
|
|
10475
|
+
if (!isMacos()) {
|
|
10476
|
+
console.error("Local HTTPS trust setup is currently supported on macOS only.");
|
|
10477
|
+
return 1;
|
|
10478
|
+
}
|
|
10479
|
+
if (argv.includes("--remove")) {
|
|
10480
|
+
await removeLocalCa(root);
|
|
10481
|
+
console.error("Removed the MoltNet local CA from your login keychain.");
|
|
10482
|
+
return 0;
|
|
10483
|
+
}
|
|
10484
|
+
await ensureTrustedLocalTls(root);
|
|
10485
|
+
console.error("MoltNet local HTTPS trust is ready for this macOS user.");
|
|
10486
|
+
return 0;
|
|
10487
|
+
}
|
|
10488
|
+
async function ensureTrustedLocalTls(root) {
|
|
10489
|
+
const material = await ensureLocalTlsMaterial(root);
|
|
10490
|
+
if (await isLocalCaTrusted(root)) return material;
|
|
10491
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) throw new Error("Local HTTPS trust is not configured. Run `moltnet-agent server trust` from an interactive terminal.");
|
|
10492
|
+
const prompt = createInterface({
|
|
10493
|
+
input: process.stdin,
|
|
10494
|
+
output: process.stdout
|
|
10495
|
+
});
|
|
10496
|
+
try {
|
|
10497
|
+
const answer = await prompt.question(`Trust MoltNet's local CA (${material.fingerprint}) in this macOS login keychain? [y/N] `);
|
|
10498
|
+
if (!/^y(es)?$/i.test(answer.trim())) throw new Error("Local HTTPS trust was not approved.");
|
|
10499
|
+
} finally {
|
|
10500
|
+
prompt.close();
|
|
10501
|
+
}
|
|
10502
|
+
await trustLocalCa(root);
|
|
10503
|
+
return material;
|
|
10504
|
+
}
|
|
10241
10505
|
function waitForAgentServerShutdown(runs, app, shutdownController) {
|
|
10242
10506
|
return new Promise((resolvePromise) => {
|
|
10243
10507
|
let shuttingDown = false;
|
|
@@ -10555,7 +10819,7 @@ async function writeCache(cache) {
|
|
|
10555
10819
|
}
|
|
10556
10820
|
//#endregion
|
|
10557
10821
|
//#region src/version.ts
|
|
10558
|
-
var DAEMON_VERSION = "0.
|
|
10822
|
+
var DAEMON_VERSION = "0.53.0";
|
|
10559
10823
|
//#endregion
|
|
10560
10824
|
//#region src/cli.ts
|
|
10561
10825
|
async function runAgentDaemonCli(options) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@themoltnet/agent-daemon",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.53.0",
|
|
4
4
|
"license": "AGPL-3.0-only",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "Universal MoltNet agent daemon host with a built-in Pi/Gondolin runtime and support for trusted operator-owned runtime modules. CLI: moltnet-agent.",
|
|
@@ -75,6 +75,7 @@
|
|
|
75
75
|
"@opentelemetry/sdk-trace-base": "^2.5.1",
|
|
76
76
|
"@opentelemetry/sdk-trace-node": "^2.5.1",
|
|
77
77
|
"@opentelemetry/semantic-conventions": "^1.39.0",
|
|
78
|
+
"@peculiar/x509": "^2.0.0",
|
|
78
79
|
"fastify": "^5.8.5",
|
|
79
80
|
"fastify-plugin": "^5.1.0",
|
|
80
81
|
"multiformats": "^13.3.0",
|
|
@@ -82,11 +83,12 @@
|
|
|
82
83
|
"pino-opentelemetry-transport": "^3.0.0",
|
|
83
84
|
"pino-pretty": "^13.1.3",
|
|
84
85
|
"proper-lockfile": "4.1.2",
|
|
86
|
+
"reflect-metadata": "^0.2.2",
|
|
85
87
|
"typebox": "^1.2.8",
|
|
86
|
-
"@themoltnet/agent-runtime": "0.45.
|
|
87
|
-
"@themoltnet/pi-runtime": "0.14.1",
|
|
88
|
+
"@themoltnet/agent-runtime": "0.45.3",
|
|
88
89
|
"@themoltnet/os-keyring": "0.3.0",
|
|
89
|
-
"@themoltnet/
|
|
90
|
+
"@themoltnet/pi-runtime": "0.14.2",
|
|
91
|
+
"@themoltnet/sdk": "0.140.1"
|
|
90
92
|
},
|
|
91
93
|
"devDependencies": {
|
|
92
94
|
"@fastify/swagger": "^9.6.1",
|
|
@@ -97,15 +99,15 @@
|
|
|
97
99
|
"vite-plugin-dts": "^4.5.4",
|
|
98
100
|
"vitest": "^3.0.0",
|
|
99
101
|
"@moltnet/agent-eval": "0.1.0",
|
|
100
|
-
"@moltnet/bootstrap": "0.1.0",
|
|
101
102
|
"@moltnet/crypto-service": "0.1.0",
|
|
102
|
-
"@moltnet/execution-plan": "0.1.0",
|
|
103
103
|
"@moltnet/execution-integrations": "0.1.0",
|
|
104
|
+
"@moltnet/bootstrap": "0.1.0",
|
|
105
|
+
"@moltnet/execution-plan": "0.1.0",
|
|
104
106
|
"@moltnet/models": "0.1.0",
|
|
105
|
-
"@moltnet/loopback-companion": "0.1.0",
|
|
106
|
-
"@moltnet/tasks": "0.1.0",
|
|
107
107
|
"@moltnet/observability": "0.1.0",
|
|
108
|
-
"@moltnet/
|
|
108
|
+
"@moltnet/loopback-companion": "0.1.0",
|
|
109
|
+
"@moltnet/runtime-profiles": "0.1.0",
|
|
110
|
+
"@moltnet/tasks": "0.1.0"
|
|
109
111
|
},
|
|
110
112
|
"nx": {
|
|
111
113
|
"projectType": "application",
|