@themoltnet/agent-daemon 0.51.0 → 0.52.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 +2 -2
- package/dist/cli.js +272 -15
- package/package.json +9 -7
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
|
|
@@ -196,7 +196,7 @@ host-exec command is safe to run without a dialog.
|
|
|
196
196
|
### Remote runtime profiles
|
|
197
197
|
|
|
198
198
|
The canonical user-facing guide lives in the public docs:
|
|
199
|
-
[Running Agents § Runtime Profiles](https://docs.themolt.net/operate/
|
|
199
|
+
[Running Agents § Runtime Profiles](https://docs.themolt.net/operate/runtime-profiles).
|
|
200
200
|
|
|
201
201
|
## Correlation anchors
|
|
202
202
|
|
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
|
/**
|
|
@@ -3431,6 +3434,8 @@ Commands:
|
|
|
3431
3434
|
server Loopback supervisor for console-managed runs: pairing,
|
|
3432
3435
|
agent/provider config store, and start/stop of poll/drain
|
|
3433
3436
|
child processes. Binds 127.0.0.1 only.
|
|
3437
|
+
server trust
|
|
3438
|
+
Install the per-user macOS local-HTTPS CA after explicit consent.
|
|
3434
3439
|
sync-sessions
|
|
3435
3440
|
Repair durable runtime-session checkpoints from local slot files.
|
|
3436
3441
|
update check
|
|
@@ -3582,6 +3587,10 @@ Options:
|
|
|
3582
3587
|
(or MOLTNET_AGENT_SERVER_ROOT).
|
|
3583
3588
|
--api-url <url> Default MoltNet API for new managed agents.
|
|
3584
3589
|
Default: https://api.themolt.net.
|
|
3590
|
+
|
|
3591
|
+
On macOS, the first interactive run asks to trust a per-user local CA in the
|
|
3592
|
+
login keychain and serves HTTPS. Run \`agent-daemon server trust --remove\` to
|
|
3593
|
+
remove that exact CA. Linux continues to use the Chromium PNA HTTP path.
|
|
3585
3594
|
`;
|
|
3586
3595
|
//#endregion
|
|
3587
3596
|
//#region src/lib/identity-pin.ts
|
|
@@ -3845,7 +3854,7 @@ async function resolveDaemonAgentIdentity(input) {
|
|
|
3845
3854
|
}
|
|
3846
3855
|
//#endregion
|
|
3847
3856
|
//#region src/lib/correlation.ts
|
|
3848
|
-
var execFileAsync = promisify(execFile);
|
|
3857
|
+
var execFileAsync$1 = promisify(execFile);
|
|
3849
3858
|
var CORRELATION_TRAILER_KEY = "Moltnet-Correlation-Id";
|
|
3850
3859
|
var CORRELATION_MARKER_RE = /<!--\s*moltnet-correlation:\s*([\w-]+)\s*-->/i;
|
|
3851
3860
|
new RegExp(`^${CORRELATION_TRAILER_KEY}:\\s*(\\S+)\\s*$`, "m");
|
|
@@ -3892,7 +3901,7 @@ function makePrBodyAnchorWriter(deps) {
|
|
|
3892
3901
|
function createGhCliClient() {
|
|
3893
3902
|
return {
|
|
3894
3903
|
async get({ owner, repo, number }) {
|
|
3895
|
-
const { stdout } = await execFileAsync("gh", [
|
|
3904
|
+
const { stdout } = await execFileAsync$1("gh", [
|
|
3896
3905
|
"api",
|
|
3897
3906
|
`repos/${owner}/${repo}/pulls/${number}`,
|
|
3898
3907
|
"--jq",
|
|
@@ -3901,7 +3910,7 @@ function createGhCliClient() {
|
|
|
3901
3910
|
return JSON.parse(stdout);
|
|
3902
3911
|
},
|
|
3903
3912
|
async patch({ owner, repo, number }, body) {
|
|
3904
|
-
await execFileAsync("gh", [
|
|
3913
|
+
await execFileAsync$1("gh", [
|
|
3905
3914
|
"api",
|
|
3906
3915
|
"-X",
|
|
3907
3916
|
"PATCH",
|
|
@@ -7366,6 +7375,12 @@ function renderPairingApprovalPage(input) {
|
|
|
7366
7375
|
</style>
|
|
7367
7376
|
</head>
|
|
7368
7377
|
<body>
|
|
7378
|
+
<script>
|
|
7379
|
+
// The Console must remain this popup's opener until it finishes navigating
|
|
7380
|
+
// from about:blank. Safari rejects that cross-origin navigation otherwise.
|
|
7381
|
+
// Once this trusted local approval document has loaded, it needs no opener.
|
|
7382
|
+
window.opener = null;
|
|
7383
|
+
<\/script>
|
|
7369
7384
|
<main>
|
|
7370
7385
|
<h1>Allow this site to manage local MoltNet agents?</h1>
|
|
7371
7386
|
<p><code>${escapeHtml(input.origin)}</code> asks to configure agents and start or stop local daemon runs on this machine.</p>
|
|
@@ -8148,7 +8163,7 @@ async function createManagedAgent(store, secrets, input, connectAgent = connect)
|
|
|
8148
8163
|
} catch (cause) {
|
|
8149
8164
|
if (!registeredIdentityId && cause instanceof MoltNetError && cause.statusCode !== void 0 && cause.statusCode >= 400 && cause.statusCode < 500) {
|
|
8150
8165
|
store.clearPendingRegistration(alias);
|
|
8151
|
-
throw new AgentServerIdentityError("registration_failed",
|
|
8166
|
+
throw new AgentServerIdentityError("registration_failed", registrationRejectionMessage(alias, cause), { cause });
|
|
8152
8167
|
}
|
|
8153
8168
|
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
8169
|
throw cause;
|
|
@@ -8156,6 +8171,9 @@ async function createManagedAgent(store, secrets, input, connectAgent = connect)
|
|
|
8156
8171
|
releaseAlias();
|
|
8157
8172
|
}
|
|
8158
8173
|
}
|
|
8174
|
+
function registrationRejectionMessage(alias, cause) {
|
|
8175
|
+
return `registration for "${alias}" was rejected${cause.statusCode === void 0 ? "" : ` (${cause.statusCode})`}: ${cause.detail?.trim() || cause.message}`;
|
|
8176
|
+
}
|
|
8159
8177
|
/** Resume a fully persisted registration or explicitly abandon local recovery. */
|
|
8160
8178
|
async function reconcileManagedRegistration(store, secrets, aliasInput, action, connectAgent = connect, signal) {
|
|
8161
8179
|
const alias = assertStoreName("agent name", aliasInput);
|
|
@@ -9416,6 +9434,16 @@ var AgentServerRouteSchemas = {
|
|
|
9416
9434
|
...problemResponse
|
|
9417
9435
|
}
|
|
9418
9436
|
},
|
|
9437
|
+
deleteProvider: {
|
|
9438
|
+
operationId: "deleteAgentServerProvider",
|
|
9439
|
+
tags: ["providers"],
|
|
9440
|
+
security: pairedSecurity,
|
|
9441
|
+
params: ProviderParamsSchema,
|
|
9442
|
+
response: {
|
|
9443
|
+
204: Type.Any(),
|
|
9444
|
+
...problemResponse
|
|
9445
|
+
}
|
|
9446
|
+
},
|
|
9419
9447
|
listSubscriptions: {
|
|
9420
9448
|
operationId: "listAgentServerSubscriptions",
|
|
9421
9449
|
tags: ["subscriptions"],
|
|
@@ -9600,10 +9628,14 @@ function requestOperationSignal(request, shutdownSignal) {
|
|
|
9600
9628
|
}
|
|
9601
9629
|
function buildAgentServer(options) {
|
|
9602
9630
|
const { pairing } = options;
|
|
9603
|
-
const
|
|
9631
|
+
const fastifyOptions = {
|
|
9604
9632
|
bodyLimit: BODY_LIMIT,
|
|
9633
|
+
...options.tls ? { https: options.tls } : {}
|
|
9634
|
+
};
|
|
9635
|
+
const app = options.logger ? Fastify({
|
|
9636
|
+
...fastifyOptions,
|
|
9605
9637
|
loggerInstance: options.logger
|
|
9606
|
-
}) : Fastify(
|
|
9638
|
+
}) : Fastify(fastifyOptions);
|
|
9607
9639
|
options.registerOpenApi?.(app);
|
|
9608
9640
|
for (const schema of AGENT_SERVER_SCHEMAS) app.addSchema(schema);
|
|
9609
9641
|
registerLoopbackSecurity(app, {
|
|
@@ -9662,12 +9694,12 @@ function buildAgentServer(options) {
|
|
|
9662
9694
|
}));
|
|
9663
9695
|
app.setErrorHandler(async (error, request, reply) => {
|
|
9664
9696
|
const { statusCode, code, message } = normalizeAgentServerError(error);
|
|
9665
|
-
if (statusCode === 500) request.log
|
|
9697
|
+
if (statusCode === 500 || error instanceof AgentServerIdentityError) request.log[statusCode === 500 ? "error" : "warn"]({
|
|
9666
9698
|
...safeErrorContext(error),
|
|
9667
|
-
code: "agent_server_request_failed",
|
|
9699
|
+
code: statusCode === 500 ? "agent_server_request_failed" : "agent_server_identity_rejected",
|
|
9668
9700
|
method: request.method,
|
|
9669
9701
|
route: request.routeOptions.url
|
|
9670
|
-
}, "AgentServer request failed");
|
|
9702
|
+
}, statusCode === 500 ? "AgentServer request failed" : "AgentServer identity request rejected");
|
|
9671
9703
|
return reply.code(statusCode).send({
|
|
9672
9704
|
code,
|
|
9673
9705
|
message
|
|
@@ -9894,12 +9926,29 @@ function registerProviderRoutes(app, options, requirePairedOrigin) {
|
|
|
9894
9926
|
provider: FILE_SECRET_PROVIDER,
|
|
9895
9927
|
key
|
|
9896
9928
|
});
|
|
9897
|
-
} else if (providers[providerId]?.apiKeyRef
|
|
9929
|
+
} else if (providers[providerId]?.apiKeyRef) entry.apiKeyRef = providers[providerId].apiKeyRef;
|
|
9898
9930
|
providers[providerId] = entry;
|
|
9899
9931
|
store.writeProviders(providers);
|
|
9900
9932
|
});
|
|
9901
9933
|
return reply.code(200).send(providerView(entry));
|
|
9902
9934
|
});
|
|
9935
|
+
app.delete("/v1/providers/:providerId", {
|
|
9936
|
+
schema: AgentServerRouteSchemas.deleteProvider,
|
|
9937
|
+
attachValidation: true
|
|
9938
|
+
}, async (request, reply) => {
|
|
9939
|
+
requirePairedOrigin(request);
|
|
9940
|
+
const { providerId: rawProviderId } = request.params;
|
|
9941
|
+
const providerId = assertProviderId(rawProviderId);
|
|
9942
|
+
await serialize(async () => {
|
|
9943
|
+
const providers = store.readProviders();
|
|
9944
|
+
const provider = providers[providerId];
|
|
9945
|
+
if (!provider) throw new AgentServerHttpError(404, "agent_server_provider_not_found", `Provider ${providerId} was not found`);
|
|
9946
|
+
if (provider.apiKeyRef) await options.secrets.delete(`pi-provider/${providerId}`);
|
|
9947
|
+
delete providers[providerId];
|
|
9948
|
+
store.writeProviders(providers);
|
|
9949
|
+
});
|
|
9950
|
+
return reply.code(204).send(null);
|
|
9951
|
+
});
|
|
9903
9952
|
}
|
|
9904
9953
|
function runViews(runs) {
|
|
9905
9954
|
return runs.list(RUN_HISTORY_LIMIT).map((record) => ({
|
|
@@ -10051,12 +10100,23 @@ function safeErrorContext(error) {
|
|
|
10051
10100
|
const applicationCode = safeErrorToken(error?.code);
|
|
10052
10101
|
if (applicationCode) context["applicationCode"] = applicationCode;
|
|
10053
10102
|
const cause = error instanceof Error ? error.cause : void 0;
|
|
10103
|
+
if (cause instanceof Error) {
|
|
10104
|
+
context["causeType"] = cause.name;
|
|
10105
|
+
const causeMessage = safeLogMessage(cause.message);
|
|
10106
|
+
if (causeMessage) context["causeMessage"] = causeMessage;
|
|
10107
|
+
}
|
|
10054
10108
|
const fsCode = safeErrorToken(cause?.code);
|
|
10055
10109
|
const syscall = safeErrorToken(cause?.syscall);
|
|
10056
10110
|
if (fsCode) context["fsCode"] = fsCode;
|
|
10057
10111
|
if (syscall) context["syscall"] = syscall;
|
|
10112
|
+
const causeStatus = cause?.statusCode;
|
|
10113
|
+
if (typeof causeStatus === "number") context["causeStatusCode"] = causeStatus;
|
|
10058
10114
|
return context;
|
|
10059
10115
|
}
|
|
10116
|
+
function safeLogMessage(value) {
|
|
10117
|
+
const normalized = value.replace(/[\r\n\t]/gu, " ").trim();
|
|
10118
|
+
return normalized ? normalized.slice(0, 500) : void 0;
|
|
10119
|
+
}
|
|
10060
10120
|
function safeErrorToken(value) {
|
|
10061
10121
|
return typeof value === "string" && /^[a-z0-9_:-]{1,64}$/iu.test(value) ? value : void 0;
|
|
10062
10122
|
}
|
|
@@ -10128,6 +10188,163 @@ function normalizeAgentServerError(error) {
|
|
|
10128
10188
|
};
|
|
10129
10189
|
}
|
|
10130
10190
|
//#endregion
|
|
10191
|
+
//#region src/lib/agent-server/tls.ts
|
|
10192
|
+
var execFileAsync = promisify(execFile);
|
|
10193
|
+
var CA_COMMON_NAME = "MoltNet Local Agent CA";
|
|
10194
|
+
var LEAF_COMMON_NAME = "MoltNet Local Agent";
|
|
10195
|
+
var RENEW_BEFORE_MS = 720 * 60 * 60 * 1e3;
|
|
10196
|
+
function loginKeychainPath() {
|
|
10197
|
+
return join(homedir(), "Library", "Keychains", "login.keychain-db");
|
|
10198
|
+
}
|
|
10199
|
+
function pemPrivateKey(key) {
|
|
10200
|
+
return webcrypto.subtle.exportKey("pkcs8", key).then((der) => createPrivateKey({
|
|
10201
|
+
key: Buffer.from(der),
|
|
10202
|
+
format: "der",
|
|
10203
|
+
type: "pkcs8"
|
|
10204
|
+
}).export({
|
|
10205
|
+
format: "pem",
|
|
10206
|
+
type: "pkcs8"
|
|
10207
|
+
}).toString());
|
|
10208
|
+
}
|
|
10209
|
+
async function importCaKeyPair(pem) {
|
|
10210
|
+
const privateKey = createPrivateKey(pem);
|
|
10211
|
+
const privateDer = privateKey.export({
|
|
10212
|
+
format: "der",
|
|
10213
|
+
type: "pkcs8"
|
|
10214
|
+
});
|
|
10215
|
+
const publicDer = createPublicKey(privateKey).export({
|
|
10216
|
+
format: "der",
|
|
10217
|
+
type: "spki"
|
|
10218
|
+
});
|
|
10219
|
+
const [privateCryptoKey, publicCryptoKey] = await Promise.all([webcrypto.subtle.importKey("pkcs8", privateDer, {
|
|
10220
|
+
name: "ECDSA",
|
|
10221
|
+
namedCurve: "P-256"
|
|
10222
|
+
}, false, ["sign"]), webcrypto.subtle.importKey("spki", publicDer, {
|
|
10223
|
+
name: "ECDSA",
|
|
10224
|
+
namedCurve: "P-256"
|
|
10225
|
+
}, false, ["verify"])]);
|
|
10226
|
+
return {
|
|
10227
|
+
privateKey: privateCryptoKey,
|
|
10228
|
+
publicKey: publicCryptoKey
|
|
10229
|
+
};
|
|
10230
|
+
}
|
|
10231
|
+
async function localTlsMaterialFromDirectory(dir) {
|
|
10232
|
+
try {
|
|
10233
|
+
const [key, cert, ca] = await Promise.all([
|
|
10234
|
+
readFile(join(dir, "loopback-key.pem"), "utf8"),
|
|
10235
|
+
readFile(join(dir, "loopback-cert.pem"), "utf8"),
|
|
10236
|
+
readFile(join(dir, "local-ca.pem"), "utf8")
|
|
10237
|
+
]);
|
|
10238
|
+
const parsed = new X509Certificate(cert);
|
|
10239
|
+
if (Date.parse(parsed.validTo) - Date.now() > RENEW_BEFORE_MS) return {
|
|
10240
|
+
key,
|
|
10241
|
+
cert,
|
|
10242
|
+
ca,
|
|
10243
|
+
fingerprint: new X509Certificate(ca).fingerprint256
|
|
10244
|
+
};
|
|
10245
|
+
} catch {}
|
|
10246
|
+
return null;
|
|
10247
|
+
}
|
|
10248
|
+
/** Creates a per-user CA and loopback-only leaf certificate under a 0700 directory. */
|
|
10249
|
+
async function ensureLocalTlsMaterial(root) {
|
|
10250
|
+
const dir = join(root, "tls");
|
|
10251
|
+
await mkdir(dir, {
|
|
10252
|
+
recursive: true,
|
|
10253
|
+
mode: 448
|
|
10254
|
+
});
|
|
10255
|
+
const existing = await localTlsMaterialFromDirectory(dir);
|
|
10256
|
+
if (existing) return existing;
|
|
10257
|
+
let ca;
|
|
10258
|
+
let caKeys;
|
|
10259
|
+
let caKey;
|
|
10260
|
+
try {
|
|
10261
|
+
ca = await readFile(join(dir, "local-ca.pem"), "utf8");
|
|
10262
|
+
caKeys = await importCaKeyPair(await readFile(join(dir, "local-ca-key.pem"), "utf8"));
|
|
10263
|
+
} catch {
|
|
10264
|
+
caKeys = await webcrypto.subtle.generateKey({
|
|
10265
|
+
name: "ECDSA",
|
|
10266
|
+
namedCurve: "P-256"
|
|
10267
|
+
}, true, ["sign", "verify"]);
|
|
10268
|
+
ca = (await X509CertificateGenerator.createSelfSigned({
|
|
10269
|
+
name: `CN=${CA_COMMON_NAME}`,
|
|
10270
|
+
keys: caKeys,
|
|
10271
|
+
notAfter: new Date(Date.now() + 10 * 365 * 24 * 60 * 60 * 1e3),
|
|
10272
|
+
extensions: [new BasicConstraintsExtension(true, void 0, true), new KeyUsagesExtension(KeyUsageFlags.keyCertSign | KeyUsageFlags.cRLSign, true)]
|
|
10273
|
+
})).toString("pem");
|
|
10274
|
+
caKey = await pemPrivateKey(caKeys.privateKey);
|
|
10275
|
+
}
|
|
10276
|
+
const leafKeys = await webcrypto.subtle.generateKey({
|
|
10277
|
+
name: "ECDSA",
|
|
10278
|
+
namedCurve: "P-256"
|
|
10279
|
+
}, true, ["sign", "verify"]);
|
|
10280
|
+
const leafCert = await X509CertificateGenerator.create({
|
|
10281
|
+
subject: `CN=${LEAF_COMMON_NAME}`,
|
|
10282
|
+
issuer: `CN=${CA_COMMON_NAME}`,
|
|
10283
|
+
publicKey: leafKeys.publicKey,
|
|
10284
|
+
signingKey: caKeys.privateKey,
|
|
10285
|
+
notAfter: new Date(Date.now() + 365 * 24 * 60 * 60 * 1e3),
|
|
10286
|
+
extensions: [
|
|
10287
|
+
new BasicConstraintsExtension(false, void 0, true),
|
|
10288
|
+
new KeyUsagesExtension(KeyUsageFlags.digitalSignature, true),
|
|
10289
|
+
new ExtendedKeyUsageExtension([ExtendedKeyUsage.serverAuth]),
|
|
10290
|
+
new SubjectAlternativeNameExtension([{
|
|
10291
|
+
type: IP,
|
|
10292
|
+
value: "127.0.0.1"
|
|
10293
|
+
}], true)
|
|
10294
|
+
]
|
|
10295
|
+
});
|
|
10296
|
+
const key = await pemPrivateKey(leafKeys.privateKey);
|
|
10297
|
+
const cert = leafCert.toString("pem");
|
|
10298
|
+
const writes = [writeFile(join(dir, "loopback-key.pem"), key, { mode: 384 }), writeFile(join(dir, "loopback-cert.pem"), cert, { mode: 384 })];
|
|
10299
|
+
if (caKey) writes.push(writeFile(join(dir, "local-ca.pem"), ca, { mode: 384 }), writeFile(join(dir, "local-ca-key.pem"), caKey, { mode: 384 }));
|
|
10300
|
+
await Promise.all(writes);
|
|
10301
|
+
return {
|
|
10302
|
+
key,
|
|
10303
|
+
cert,
|
|
10304
|
+
ca,
|
|
10305
|
+
fingerprint: new X509Certificate(ca).fingerprint256
|
|
10306
|
+
};
|
|
10307
|
+
}
|
|
10308
|
+
async function trustLocalCa(root) {
|
|
10309
|
+
const caPath = join(root, "tls", "local-ca.pem");
|
|
10310
|
+
await execFileAsync("security", [
|
|
10311
|
+
"add-trusted-cert",
|
|
10312
|
+
"-d",
|
|
10313
|
+
"-r",
|
|
10314
|
+
"trustRoot",
|
|
10315
|
+
"-k",
|
|
10316
|
+
loginKeychainPath(),
|
|
10317
|
+
caPath
|
|
10318
|
+
]);
|
|
10319
|
+
}
|
|
10320
|
+
async function isLocalCaTrusted(root) {
|
|
10321
|
+
const ca = await readFile(join(root, "tls", "local-ca.pem"), "utf8");
|
|
10322
|
+
try {
|
|
10323
|
+
const { stdout } = await execFileAsync("security", [
|
|
10324
|
+
"find-certificate",
|
|
10325
|
+
"-a",
|
|
10326
|
+
"-p",
|
|
10327
|
+
"-c",
|
|
10328
|
+
CA_COMMON_NAME,
|
|
10329
|
+
loginKeychainPath()
|
|
10330
|
+
]);
|
|
10331
|
+
return stdout.includes(ca.trim());
|
|
10332
|
+
} catch {
|
|
10333
|
+
return false;
|
|
10334
|
+
}
|
|
10335
|
+
}
|
|
10336
|
+
async function removeLocalCa(root) {
|
|
10337
|
+
await execFileAsync("security", [
|
|
10338
|
+
"delete-certificate",
|
|
10339
|
+
"-Z",
|
|
10340
|
+
new X509Certificate(await readFile(join(root, "tls", "local-ca.pem"), "utf8")).fingerprint256.replaceAll(":", ""),
|
|
10341
|
+
loginKeychainPath()
|
|
10342
|
+
]);
|
|
10343
|
+
}
|
|
10344
|
+
function isMacos() {
|
|
10345
|
+
return process.platform === "darwin";
|
|
10346
|
+
}
|
|
10347
|
+
//#endregion
|
|
10131
10348
|
//#region src/cli/server.ts
|
|
10132
10349
|
/**
|
|
10133
10350
|
* `moltnet-agent server` — per-user loopback supervisor (#2061).
|
|
@@ -10144,14 +10361,17 @@ async function runAgentServer(argv) {
|
|
|
10144
10361
|
console.log(AGENT_SERVER_HELP);
|
|
10145
10362
|
return 0;
|
|
10146
10363
|
}
|
|
10364
|
+
const trustRequested = argv[0] === "trust";
|
|
10365
|
+
const commandArgs = trustRequested ? argv.slice(1) : argv;
|
|
10147
10366
|
const envConfig = loadAgentServerEnvConfig();
|
|
10148
10367
|
const { values } = parseArgs({
|
|
10149
|
-
args:
|
|
10368
|
+
args: commandArgs,
|
|
10150
10369
|
options: {
|
|
10151
10370
|
port: { type: "string" },
|
|
10152
10371
|
"allowed-origins": { type: "string" },
|
|
10153
10372
|
root: { type: "string" },
|
|
10154
|
-
"api-url": { type: "string" }
|
|
10373
|
+
"api-url": { type: "string" },
|
|
10374
|
+
remove: { type: "boolean" }
|
|
10155
10375
|
}
|
|
10156
10376
|
});
|
|
10157
10377
|
const port = Number.parseInt(values.port ?? (envConfig.port || `${DEFAULT_PORT}`), 10);
|
|
@@ -10166,6 +10386,7 @@ async function runAgentServer(argv) {
|
|
|
10166
10386
|
});
|
|
10167
10387
|
const defaultApiUrl = values["api-url"] ?? (envConfig.apiUrl || DEFAULT_API_URL);
|
|
10168
10388
|
const store = new AgentServerStore(root).ensure();
|
|
10389
|
+
if (trustRequested) return runTrustCommand(commandArgs, root);
|
|
10169
10390
|
const { logger, shutdown: shutdownLogger } = createRootLogger({
|
|
10170
10391
|
name: "agent-daemon.server",
|
|
10171
10392
|
level: envConfig.logLevel || "info"
|
|
@@ -10193,6 +10414,7 @@ async function runAgentServer(argv) {
|
|
|
10193
10414
|
logger,
|
|
10194
10415
|
runtimeRegistry: new RuntimeRegistry(store.root)
|
|
10195
10416
|
});
|
|
10417
|
+
const tls = isMacos() ? await ensureTrustedLocalTls(root) : void 0;
|
|
10196
10418
|
const app = buildAgentServer({
|
|
10197
10419
|
store,
|
|
10198
10420
|
secrets,
|
|
@@ -10202,7 +10424,11 @@ async function runAgentServer(argv) {
|
|
|
10202
10424
|
runs,
|
|
10203
10425
|
subscriptions,
|
|
10204
10426
|
allowedOrigins,
|
|
10205
|
-
selfOrigin:
|
|
10427
|
+
selfOrigin: `${tls ? "https" : "http"}://127.0.0.1:${port}`,
|
|
10428
|
+
...tls ? { tls: {
|
|
10429
|
+
key: tls.key,
|
|
10430
|
+
cert: tls.cert
|
|
10431
|
+
} } : {},
|
|
10206
10432
|
defaultApiUrl,
|
|
10207
10433
|
version: "dev",
|
|
10208
10434
|
logger,
|
|
@@ -10238,6 +10464,37 @@ async function runAgentServer(argv) {
|
|
|
10238
10464
|
await shutdownLogger();
|
|
10239
10465
|
}
|
|
10240
10466
|
}
|
|
10467
|
+
async function runTrustCommand(argv, root) {
|
|
10468
|
+
if (!isMacos()) {
|
|
10469
|
+
console.error("Local HTTPS trust setup is currently supported on macOS only.");
|
|
10470
|
+
return 1;
|
|
10471
|
+
}
|
|
10472
|
+
if (argv.includes("--remove")) {
|
|
10473
|
+
await removeLocalCa(root);
|
|
10474
|
+
console.error("Removed the MoltNet local CA from your login keychain.");
|
|
10475
|
+
return 0;
|
|
10476
|
+
}
|
|
10477
|
+
await ensureTrustedLocalTls(root);
|
|
10478
|
+
console.error("MoltNet local HTTPS trust is ready for this macOS user.");
|
|
10479
|
+
return 0;
|
|
10480
|
+
}
|
|
10481
|
+
async function ensureTrustedLocalTls(root) {
|
|
10482
|
+
const material = await ensureLocalTlsMaterial(root);
|
|
10483
|
+
if (await isLocalCaTrusted(root)) return material;
|
|
10484
|
+
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.");
|
|
10485
|
+
const prompt = createInterface({
|
|
10486
|
+
input: process.stdin,
|
|
10487
|
+
output: process.stdout
|
|
10488
|
+
});
|
|
10489
|
+
try {
|
|
10490
|
+
const answer = await prompt.question(`Trust MoltNet's local CA (${material.fingerprint}) in this macOS login keychain? [y/N] `);
|
|
10491
|
+
if (!/^y(es)?$/i.test(answer.trim())) throw new Error("Local HTTPS trust was not approved.");
|
|
10492
|
+
} finally {
|
|
10493
|
+
prompt.close();
|
|
10494
|
+
}
|
|
10495
|
+
await trustLocalCa(root);
|
|
10496
|
+
return material;
|
|
10497
|
+
}
|
|
10241
10498
|
function waitForAgentServerShutdown(runs, app, shutdownController) {
|
|
10242
10499
|
return new Promise((resolvePromise) => {
|
|
10243
10500
|
let shuttingDown = false;
|
|
@@ -10555,7 +10812,7 @@ async function writeCache(cache) {
|
|
|
10555
10812
|
}
|
|
10556
10813
|
//#endregion
|
|
10557
10814
|
//#region src/version.ts
|
|
10558
|
-
var DAEMON_VERSION = "0.
|
|
10815
|
+
var DAEMON_VERSION = "0.52.0";
|
|
10559
10816
|
//#endregion
|
|
10560
10817
|
//#region src/cli.ts
|
|
10561
10818
|
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.52.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,9 +83,10 @@
|
|
|
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.2",
|
|
87
88
|
"@themoltnet/pi-runtime": "0.14.1",
|
|
89
|
+
"@themoltnet/agent-runtime": "0.45.2",
|
|
88
90
|
"@themoltnet/os-keyring": "0.3.0",
|
|
89
91
|
"@themoltnet/sdk": "0.140.0"
|
|
90
92
|
},
|
|
@@ -96,16 +98,16 @@
|
|
|
96
98
|
"vite": "^8.0.0",
|
|
97
99
|
"vite-plugin-dts": "^4.5.4",
|
|
98
100
|
"vitest": "^3.0.0",
|
|
99
|
-
"@moltnet/agent-eval": "0.1.0",
|
|
100
101
|
"@moltnet/bootstrap": "0.1.0",
|
|
102
|
+
"@moltnet/agent-eval": "0.1.0",
|
|
101
103
|
"@moltnet/crypto-service": "0.1.0",
|
|
102
|
-
"@moltnet/execution-plan": "0.1.0",
|
|
103
104
|
"@moltnet/execution-integrations": "0.1.0",
|
|
104
|
-
"@moltnet/
|
|
105
|
+
"@moltnet/execution-plan": "0.1.0",
|
|
105
106
|
"@moltnet/loopback-companion": "0.1.0",
|
|
106
|
-
"@moltnet/tasks": "0.1.0",
|
|
107
107
|
"@moltnet/observability": "0.1.0",
|
|
108
|
-
"@moltnet/runtime-profiles": "0.1.0"
|
|
108
|
+
"@moltnet/runtime-profiles": "0.1.0",
|
|
109
|
+
"@moltnet/models": "0.1.0",
|
|
110
|
+
"@moltnet/tasks": "0.1.0"
|
|
109
111
|
},
|
|
110
112
|
"nx": {
|
|
111
113
|
"projectType": "application",
|