@themoltnet/agent-daemon 0.61.0 → 0.62.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.
Files changed (3) hide show
  1. package/README.md +10 -9
  2. package/dist/cli.js +135 -326
  3. package/package.json +6 -8
package/README.md CHANGED
@@ -15,17 +15,18 @@ from [themolt.net/download](https://themolt.net/download). Opening the app:
15
15
 
16
16
  1. installs or updates the publisher-verified Agent CLI bundle under
17
17
  `~/.local/share/moltnet/agent`;
18
- 2. asks before adding the per-user local HTTPS CA to the login Keychain;
19
- 3. starts a foreground, supervised Agent Server; and
20
- 4. opens Console for OAuth PKCE approval and runtime management.
18
+ 2. starts a foreground, supervised Agent Server over a private native socket;
19
+ and
20
+ 3. opens the browser only when OAuth approval is required.
21
21
 
22
22
  Closing the status window hides it. **Quit and Stop Server** stops the owned
23
- server process before the app exits. After the server restarts, reconnect Console through OAuth PKCE. The local
24
- operator, identities, and provider configuration persist.
23
+ server process before the app exits. Desktop re-establishes its private native
24
+ control channel after a server restart. The local operator, identities, and
25
+ provider configuration persist.
25
26
 
26
27
  Agent CLI updates and desktop-app updates use independent signed channels and
27
28
  always require consent. Removing the Agent CLI bundle preserves
28
- `~/.config/moltnet`. Removing the local CA is a separate opt-in action.
29
+ `~/.config/moltnet`.
29
30
 
30
31
  ### Agent CLI
31
32
 
@@ -123,8 +124,8 @@ For config-based runs, the agent's `moltnet.json` lives in the central store
123
124
  (`~/.config/moltnet/identities/<agent>/`) or, for an external config, next to
124
125
  its gitconfig in `.moltnet/<agent>/`. Three paths create it:
125
126
 
126
- - The Console's Local Runtime page creates a managed agent through the Agent
127
- Server: keypair and agent key generated on this machine, stored under the
127
+ - MoltNet Agent creates a managed agent through the Agent Server: keypair and
128
+ agent key generated on this machine, stored under the
128
129
  store's `secrets/` directory, no CLI involved.
129
130
  - [`moltnet register`](../../docs/start/install-and-initialize.md#register-an-agent)
130
131
  creates an OAuth2 identity from the CLI; add a stored agent key with
@@ -209,7 +210,7 @@ carries them -- read access to the agent's teams and their diaries, and
209
210
  enrollment into a team. Startup does not check for them: a key without them
210
211
  claims and runs work, and the daemon reports which are absent.
211
212
 
212
- The Console selects the full set by default. Knowledge-enabled workers must add
213
+ Desktop selects the full set by default. Knowledge-enabled workers must add
213
214
  `diary:write`, `pack:read`, and `pack:write` when the key is issued.
214
215
 
215
216
  `crypto:sign` is in the minimum because host-capability signing runs on the
package/dist/cli.js CHANGED
@@ -16,7 +16,7 @@ import { constants, copyFileSync, createReadStream, createWriteStream, existsSyn
16
16
  import { AuthenticationError, IDENTITY_ALIAS_PATTERN, MoltNetError, assertIdentityAlias, assertTrustedConfigApiUrl, createExecutorAttestor, formatSecretReferenceString, getConfigDir, getIdentityDir, hasAgentKeyConfiguration, isCanonicalConfig, parseSecretReferenceString, readConfig, requireSecureCredentialApiUrl, resolveAgentKey, resolveEnvSecretReference, resolveIdentitySeed, selectAgentKeyReference, signBytes } from "@themoltnet/sdk";
17
17
  import { AgentRuntime, ApiTaskReporter, ApiTaskSource, PollingApiTaskSource, RuntimeProfilePrerequisiteError, createLocalSeedSigner, resolveAgentIdentity, resolveRuntimeProfile, resolveRuntimeProfiles, validateRuntimeProfilePrerequisites } from "@themoltnet/agent-runtime";
18
18
  import { GuestEnvironmentBoundaryError, assertGuestEnvironmentBoundary, createPiRetryTriage, findMainWorktree, isResolvedPathInsideRoot, normalizeRetryTriageResult, redactRetryTriageSecrets, resolveRuntimeProfileModel } from "@themoltnet/pi-runtime";
19
- import { X509Certificate, createHash, createPrivateKey, createPublicKey, randomBytes, randomUUID, timingSafeEqual, webcrypto } from "node:crypto";
19
+ import { createHash, randomBytes, randomUUID, timingSafeEqual } 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";
@@ -28,7 +28,7 @@ import { NodeTracerProvider } from "@opentelemetry/sdk-trace-node";
28
28
  import { ATTR_SERVICE_NAME, ATTR_SERVICE_VERSION } from "@opentelemetry/semantic-conventions";
29
29
  import { homedir, platform, tmpdir } from "node:os";
30
30
  import { PI_MODEL_MODALITIES, writePiConfig } from "@themoltnet/pi-runtime/pi-config";
31
- import { mkdir, open, readFile, readdir, realpath, rm, stat, writeFile } from "node:fs/promises";
31
+ import { chmod, lstat, mkdir, open, readFile, readdir, realpath, stat, writeFile } from "node:fs/promises";
32
32
  import { AsyncLocalStorage } from "node:async_hooks";
33
33
  import { pipeline } from "node:stream/promises";
34
34
  import { createInterface } from "node:readline/promises";
@@ -45,8 +45,6 @@ import { pathToFileURL } from "node:url";
45
45
  import { StringDecoder } from "node:string_decoder";
46
46
  import rateLimit from "@fastify/rate-limit";
47
47
  import Fastify from "fastify";
48
- import "reflect-metadata";
49
- import { BasicConstraintsExtension, ExtendedKeyUsage, ExtendedKeyUsageExtension, IP, KeyUsageFlags, KeyUsagesExtension, SubjectAlternativeNameExtension, X509CertificateGenerator } from "@peculiar/x509";
50
48
  import { createGzip } from "node:zlib";
51
49
  //#region ../../libs/tasks/src/rubric.ts
52
50
  /**
@@ -3851,13 +3849,13 @@ function loadUpdateEnvConfig() {
3851
3849
  }
3852
3850
  //#endregion
3853
3851
  //#region src/lib/run-project-selection.ts
3854
- var execFileAsync$2 = promisify(execFile);
3852
+ var execFileAsync$1 = promisify(execFile);
3855
3853
  var GIT_TIMEOUT_MS = 1e4;
3856
3854
  var GIT_MAX_OUTPUT_BYTES = 64 * 1024;
3857
3855
  async function validateGitSource(source) {
3858
3856
  const inherited = processEnvSnapshot();
3859
3857
  try {
3860
- const { stdout } = await execFileAsync$2("git", [
3858
+ const { stdout } = await execFileAsync$1("git", [
3861
3859
  "rev-parse",
3862
3860
  "--show-toplevel",
3863
3861
  "--verify",
@@ -4015,11 +4013,9 @@ Commands:
4015
4013
  once Claim and execute one specific queued task by id, then exit.
4016
4014
  drain Poll until the queue has nothing claimable, then exit.
4017
4015
  Useful for batch eval runs and demos.
4018
- server Loopback supervisor for console-managed runs: OAuth local control,
4019
- agent/provider config store, and start/stop of poll/drain
4020
- child processes. Binds 127.0.0.1 only.
4021
- server trust
4022
- Install the per-user macOS local-HTTPS CA after explicit consent.
4016
+ server Supervisor for managed runs: authorized standalone local control,
4017
+ agent/provider config store, and start/stop of poll/drain child
4018
+ processes. Binds 127.0.0.1 or a private native socket.
4023
4019
  providers Manage configured endpoints and Pi OAuth subscriptions without
4024
4020
  starting the Agent Server. See \`agent-daemon providers --help\`.
4025
4021
  sync-sessions
@@ -4169,16 +4165,17 @@ function isHelpFlag(args) {
4169
4165
  return args.includes("--help") || args.includes("-h");
4170
4166
  }
4171
4167
  var AGENT_SERVER_HELP = `\
4172
- agent-daemon server — loopback supervisor for console-managed runs.
4168
+ agent-daemon server — local supervisor for managed runs.
4173
4169
 
4174
- Binds 127.0.0.1 only. An authorized Console origin configures agents and
4175
- providers (secret references only) and starts/stops poll/drain runs as
4176
- child processes of this supervisor.
4170
+ Standalone mode binds 127.0.0.1 for authorized local-control clients. MoltNet
4171
+ Agent Desktop instead uses a private Unix socket with a process-scoped grant.
4172
+ Both modes configure agents and providers and start/stop child runs.
4177
4173
 
4178
4174
  Options:
4179
4175
  --port <n> Loopback port. Default: 17374.
4180
4176
  Env: MOLTNET_AGENT_SERVER_PORT.
4181
- --allowed-origins <csv> Exact Console origins allowed local control.
4177
+ --allowed-origins <csv> Exact browser-controller origins allowed
4178
+ local control.
4182
4179
  Default: https://console.themolt.net.
4183
4180
  Env: MOLTNET_AGENT_SERVER_ALLOWED_ORIGINS.
4184
4181
  --root <path> Config root. Default: ~/.config/moltnet
@@ -4188,14 +4185,13 @@ Options:
4188
4185
  --heartbeat-interval-ms <n> Child reporter heartbeat cadence. Default: 60000.
4189
4186
  --warm-retention-sec <n> Child session/workspace retention. Default: 1800.
4190
4187
  --supervised Also stop gracefully when stdin reaches EOF.
4188
+ --native-socket <path> Private native-only socket (requires --supervised).
4189
+ Absolute, at most 100 bytes, with a new socket in
4190
+ a caller-owned 0700 directory. TCP flags are not
4191
+ accepted; inherited TCP env settings are ignored.
4191
4192
 
4192
- On macOS, the first interactive run asks to trust a per-user local CA in the
4193
- login keychain and serves HTTPS. Native supervisors use:
4194
- server trust --status --json
4195
- server trust --yes --json
4196
- server trust --remove --yes --json
4197
- Run \`agent-daemon server trust --remove\` interactively to remove that exact
4198
- CA. Linux continues to use the Chromium PNA HTTP path.
4193
+ Standalone mode uses loopback HTTP on every platform. Desktop socket mode does
4194
+ not open a TCP listener.
4199
4195
  `;
4200
4196
  var PROVIDERS_HELP = `\
4201
4197
  moltnet-agent providers — manage local model providers.
@@ -4258,7 +4254,7 @@ async function resolveDaemonAgentIdentity(input) {
4258
4254
  }
4259
4255
  //#endregion
4260
4256
  //#region src/lib/correlation.ts
4261
- var execFileAsync$1 = promisify(execFile);
4257
+ var execFileAsync = promisify(execFile);
4262
4258
  var CORRELATION_TRAILER_KEY = "Moltnet-Correlation-Id";
4263
4259
  var CORRELATION_MARKER_RE = /<!--\s*moltnet-correlation:\s*([\w-]+)\s*-->/i;
4264
4260
  new RegExp(`^${CORRELATION_TRAILER_KEY}:\\s*(\\S+)\\s*$`, "m");
@@ -4305,7 +4301,7 @@ function makePrBodyAnchorWriter(deps) {
4305
4301
  function createGhCliClient() {
4306
4302
  return {
4307
4303
  async get({ owner, repo, number }) {
4308
- const { stdout } = await execFileAsync$1("gh", [
4304
+ const { stdout } = await execFileAsync("gh", [
4309
4305
  "api",
4310
4306
  `repos/${owner}/${repo}/pulls/${number}`,
4311
4307
  "--jq",
@@ -4314,7 +4310,7 @@ function createGhCliClient() {
4314
4310
  return JSON.parse(stdout);
4315
4311
  },
4316
4312
  async patch({ owner, repo, number }, body) {
4317
- await execFileAsync$1("gh", [
4313
+ await execFileAsync("gh", [
4318
4314
  "api",
4319
4315
  "-X",
4320
4316
  "PATCH",
@@ -9469,6 +9465,29 @@ var NativeGrantService = class {
9469
9465
  }
9470
9466
  };
9471
9467
  //#endregion
9468
+ //#region src/lib/agent-server/native-socket.ts
9469
+ /** The supervisor owns the private directory; the server never removes peers. */
9470
+ async function validateNativeSocket(path) {
9471
+ if (!isAbsolute(path) || Buffer.byteLength(path) > 100) throw new Error("Native socket must be an absolute path of at most 100 bytes");
9472
+ const parent = dirname(path);
9473
+ const metadata = await lstat(parent);
9474
+ if (metadata.isSymbolicLink()) throw new Error(`Native socket parent contains a symlink: ${parent}`);
9475
+ if (!metadata.isDirectory()) throw new Error(`Native socket parent is not a directory: ${parent}`);
9476
+ const expectedUid = process.getuid?.();
9477
+ if (expectedUid === void 0 || metadata.uid !== expectedUid) throw new Error(`Native socket parent has uid ${metadata.uid}; expected ${String(expectedUid)}: ${parent}`);
9478
+ const mode = metadata.mode & 511;
9479
+ if (mode !== 448) throw new Error(`Native socket parent has mode ${mode.toString(8)}; expected 700: ${parent}`);
9480
+ const canonical = await realpath(parent);
9481
+ if (canonical !== resolve(parent)) throw new Error(`Native socket parent contains a symlink: ${parent} resolves to ${canonical}`);
9482
+ try {
9483
+ await lstat(path);
9484
+ } catch (error) {
9485
+ if (error.code === "ENOENT") return;
9486
+ throw error;
9487
+ }
9488
+ throw new Error("Native socket path already exists; it was left untouched");
9489
+ }
9490
+ //#endregion
9472
9491
  //#region src/lib/agent-server/operator-oauth.ts
9473
9492
  var InvalidOperatorGrantError = class extends Error {};
9474
9493
  var LOCAL_SCOPE = OPERATOR_OAUTH.localControlScope;
@@ -9671,8 +9690,8 @@ var OperatorOAuth = class {
9671
9690
  /**
9672
9691
  * Subscription-provider OAuth brokering for Agent Server (#2061 slice 4).
9673
9692
  *
9674
- * The console clicks "Connect"; agent server runs the Pi OAuth flow host-side via
9675
- * `ModelRuntime.login()` (which owns persistence into the shared
9693
+ * An authorized controller starts the flow; Agent Server runs Pi OAuth
9694
+ * host-side via `ModelRuntime.login()` (which owns persistence into the shared
9676
9695
  * `pi/auth.json` and token rotation thereafter). The browser only ever sees
9677
9696
  * the provider's authorize URL or device code — never tokens.
9678
9697
  *
@@ -11406,17 +11425,17 @@ async function enrollIdentityTeam(options) {
11406
11425
  /**
11407
11426
  * The identity-wide team/diary binding, read from `<identityDir>/env`.
11408
11427
  *
11409
- * This mirrors the Go CLI's `identityDefaultBinding` (`context_store.go:219`),
11428
+ * This mirrors the Go CLI's `identityDefaultBinding` (`project_selection.go`),
11410
11429
  * which is the fallback the CLI uses when a working directory has no
11411
- * location-keyed binding in `contexts.json`.
11430
+ * registered project binding.
11412
11431
  *
11413
11432
  * The desktop cannot use the CLI's *location* bindings at all — its composer
11414
11433
  * has no working directory to key on — but it can honour this identity-wide
11415
- * default, so an operator who ran `moltnet context set` sees their familiar
11416
- * team preselected rather than an arbitrary first entry.
11434
+ * default, so an operator sees their familiar team preselected rather than an
11435
+ * arbitrary first entry.
11417
11436
  *
11418
11437
  * Like the CLI, a half-filled pair is treated as no binding: a team without a
11419
- * diary is exactly the state `validateContextBinding` rejects.
11438
+ * diary is ignored.
11420
11439
  */
11421
11440
  function readIdentityDefaultBinding(identityDir) {
11422
11441
  let contents;
@@ -11982,10 +12001,7 @@ function buildAgentServer(options) {
11982
12001
  const { nativeGrant } = options;
11983
12002
  const oauth = options.operatorOAuth;
11984
12003
  let restartRequired = false;
11985
- const fastifyOptions = {
11986
- bodyLimit: BODY_LIMIT,
11987
- ...options.tls ? { https: options.tls } : {}
11988
- };
12004
+ const fastifyOptions = { bodyLimit: BODY_LIMIT };
11989
12005
  const app = options.logger ? Fastify({
11990
12006
  ...fastifyOptions,
11991
12007
  loggerInstance: options.logger
@@ -12021,6 +12037,31 @@ function buildAgentServer(options) {
12021
12037
  browserVerification.set(request, pending);
12022
12038
  return pending;
12023
12039
  }
12040
+ function hasValidNativeGrant(origin, token) {
12041
+ if (origin !== "moltnet-agent-desktop://native" || typeof token !== "string" || token.length === 0) return false;
12042
+ try {
12043
+ nativeGrant.verify(origin, token);
12044
+ return true;
12045
+ } catch (error) {
12046
+ if (error instanceof NativeGrantError) return false;
12047
+ throw error;
12048
+ }
12049
+ }
12050
+ const nativeVerification = /* @__PURE__ */ new WeakSet();
12051
+ function requireNativeGrant(request) {
12052
+ if (nativeVerification.has(request)) return;
12053
+ const origin = request.headers.origin;
12054
+ const token = request.headers[AGENT_SERVER_TOKEN_HEADER];
12055
+ if (hasValidNativeGrant(origin, token)) {
12056
+ nativeVerification.add(request);
12057
+ return;
12058
+ }
12059
+ request.log.warn({
12060
+ stage: "native-control-authorization",
12061
+ outcome: "rejected"
12062
+ }, "Native control authorization failed");
12063
+ throw new AgentServerHttpError(401, "native_token_invalid", "Native authorization required");
12064
+ }
12024
12065
  app.register(rateLimit, {
12025
12066
  global: true,
12026
12067
  max: options.rateLimitMax ?? RATE_LIMIT_MAX,
@@ -12032,15 +12073,15 @@ function buildAgentServer(options) {
12032
12073
  const presented = request.headers[AGENT_SERVER_TOKEN_HEADER];
12033
12074
  let authenticated = false;
12034
12075
  if (typeof presented === "string" && presented.length > 0) try {
12035
- if (origin === "moltnet-agent-desktop://native") nativeGrant.verify(origin, presented);
12076
+ if (origin === "moltnet-agent-desktop://native") authenticated = hasValidNativeGrant(origin, presented);
12036
12077
  else {
12037
12078
  if (!oauth) return `unauth:${origin}:${request.ip}`;
12038
12079
  await verifyBrowser(request, presented);
12080
+ authenticated = true;
12039
12081
  }
12040
- authenticated = true;
12041
12082
  } catch (error) {
12042
12083
  if (error instanceof AgentServerHttpError && error.statusCode === 429) throw error;
12043
- if (origin === "moltnet-agent-desktop://native" && !(error instanceof NativeGrantError)) throw error;
12084
+ if (origin === "moltnet-agent-desktop://native") throw error;
12044
12085
  }
12045
12086
  return authenticated ? `origin:${origin}` : `unauth:${origin}:${request.ip}`;
12046
12087
  }
@@ -12050,7 +12091,7 @@ function buildAgentServer(options) {
12050
12091
  const origin = requireOriginHeader(request.headers);
12051
12092
  const token = request.headers[AGENT_SERVER_TOKEN_HEADER];
12052
12093
  if (typeof token !== "string" || token.length === 0) throw new AgentServerHttpError(401, "authorization_required", "Local control token is required");
12053
- if (origin === "moltnet-agent-desktop://native") nativeGrant.verify(origin, token);
12094
+ if (origin === "moltnet-agent-desktop://native") requireNativeGrant(request);
12054
12095
  else try {
12055
12096
  if (!oauth) throw new AgentServerHttpError(503, "oauth_unavailable", "Local OAuth is not configured");
12056
12097
  const admission = browserVerification.get(request);
@@ -12079,6 +12120,9 @@ function buildAgentServer(options) {
12079
12120
  return origin;
12080
12121
  };
12081
12122
  app.after(() => {
12123
+ if (options.nativeOnly) app.addHook("preParsing", async (request) => {
12124
+ requireNativeGrant(request);
12125
+ });
12082
12126
  app.get("/health", { schema: AgentServerRouteSchemas.health }, async () => ({ status: "ok" }));
12083
12127
  app.get("/v1/native/connection-settings", { schema: { hide: true } }, async (request) => {
12084
12128
  if (await requireAuthorizedOrigin(request) !== "moltnet-agent-desktop://native" || !options.connectionSettings) throw new AgentServerHttpError(403, "native_required", "Native administration required");
@@ -12657,196 +12701,42 @@ function normalizeAgentServerError(error) {
12657
12701
  };
12658
12702
  }
12659
12703
  //#endregion
12660
- //#region src/lib/agent-server/tls.ts
12661
- var execFileAsync = promisify(execFile);
12662
- var CA_COMMON_NAME = "MoltNet Local Agent CA";
12663
- var LEAF_COMMON_NAME = "MoltNet Local Agent";
12664
- var RENEW_BEFORE_MS = 720 * 60 * 60 * 1e3;
12665
- function loginKeychainPath() {
12666
- return join(homedir(), "Library", "Keychains", "login.keychain-db");
12667
- }
12668
- function pemPrivateKey(key) {
12669
- return webcrypto.subtle.exportKey("pkcs8", key).then((der) => createPrivateKey({
12670
- key: Buffer.from(der),
12671
- format: "der",
12672
- type: "pkcs8"
12673
- }).export({
12674
- format: "pem",
12675
- type: "pkcs8"
12676
- }).toString());
12677
- }
12678
- async function localTlsMaterialFromDirectory(dir) {
12679
- try {
12680
- const [key, cert, ca] = await Promise.all([
12681
- readFile(join(dir, "loopback-key.pem"), "utf8"),
12682
- readFile(join(dir, "loopback-cert.pem"), "utf8"),
12683
- readFile(join(dir, "local-ca.pem"), "utf8")
12684
- ]);
12685
- const parsed = new X509Certificate(cert);
12686
- const authority = new X509Certificate(ca);
12687
- if (Date.parse(parsed.validTo) - Date.now() > RENEW_BEFORE_MS && Date.parse(authority.validTo) - Date.now() > RENEW_BEFORE_MS && authority.keyUsage?.includes("1.3.6.1.5.5.7.3.1") && parsed.checkIP("127.0.0.1") === "127.0.0.1" && parsed.verify(authority.publicKey) && parsed.publicKey.equals(createPublicKey(key))) return {
12688
- key,
12689
- cert,
12690
- ca,
12691
- fingerprint: new X509Certificate(ca).fingerprint256
12692
- };
12693
- } catch {}
12694
- return null;
12695
- }
12696
- /** Read-only inspection for native status polling. */
12697
- async function inspectLocalTlsMaterial(root) {
12698
- if (await hasStoredSigningKey(join(root, "tls"))) return null;
12699
- return localTlsMaterialFromDirectory(join(root, "tls"));
12700
- }
12701
- async function hasStoredSigningKey(dir) {
12702
- return stat(join(dir, "local-ca-key.pem")).then(() => true, (error) => {
12703
- if (error.code === "ENOENT") return false;
12704
- throw error;
12705
- });
12706
- }
12707
- /** Creates a per-user CA and loopback-only leaf certificate under a 0700 directory. */
12708
- async function ensureLocalTlsMaterial(root) {
12709
- const dir = join(root, "tls");
12710
- await mkdir(dir, {
12711
- recursive: true,
12712
- mode: 448
12713
- });
12714
- const previousSigningKey = join(dir, "local-ca-key.pem");
12715
- const hasPreviousSigningKey = await hasStoredSigningKey(dir);
12716
- const existing = await localTlsMaterialFromDirectory(dir);
12717
- if (existing && !hasPreviousSigningKey) return existing;
12718
- if (isMacos() && await isLocalCaTrusted(root)) await removeLocalCa(root);
12719
- const caKeys = await webcrypto.subtle.generateKey({
12720
- name: "ECDSA",
12721
- namedCurve: "P-256"
12722
- }, false, ["sign", "verify"]);
12723
- const ca = (await X509CertificateGenerator.createSelfSigned({
12724
- name: `CN=${CA_COMMON_NAME}`,
12725
- keys: caKeys,
12726
- notAfter: new Date(Date.now() + 366 * 24 * 60 * 60 * 1e3),
12727
- extensions: [
12728
- new BasicConstraintsExtension(true, 0, true),
12729
- new KeyUsagesExtension(KeyUsageFlags.keyCertSign, true),
12730
- new ExtendedKeyUsageExtension([ExtendedKeyUsage.serverAuth], true)
12731
- ]
12732
- })).toString("pem");
12733
- const leafKeys = await webcrypto.subtle.generateKey({
12734
- name: "ECDSA",
12735
- namedCurve: "P-256"
12736
- }, true, ["sign", "verify"]);
12737
- const leafCert = await X509CertificateGenerator.create({
12738
- subject: `CN=${LEAF_COMMON_NAME}`,
12739
- issuer: `CN=${CA_COMMON_NAME}`,
12740
- publicKey: leafKeys.publicKey,
12741
- signingKey: caKeys.privateKey,
12742
- notAfter: new Date(Date.now() + 365 * 24 * 60 * 60 * 1e3),
12743
- extensions: [
12744
- new BasicConstraintsExtension(false, void 0, true),
12745
- new KeyUsagesExtension(KeyUsageFlags.digitalSignature, true),
12746
- new ExtendedKeyUsageExtension([ExtendedKeyUsage.serverAuth]),
12747
- new SubjectAlternativeNameExtension([{
12748
- type: IP,
12749
- value: "127.0.0.1"
12750
- }], true)
12751
- ]
12752
- });
12753
- const key = await pemPrivateKey(leafKeys.privateKey);
12754
- const cert = leafCert.toString("pem");
12755
- const writes = [writeFile(join(dir, "loopback-key.pem"), key, { mode: 384 }), writeFile(join(dir, "loopback-cert.pem"), cert, { mode: 384 })];
12756
- writes.push(writeFile(join(dir, "local-ca.pem"), ca, { mode: 384 }));
12757
- await Promise.all(writes);
12758
- await rm(previousSigningKey, { force: true });
12759
- return {
12760
- key,
12761
- cert,
12762
- ca,
12763
- fingerprint: new X509Certificate(ca).fingerprint256
12764
- };
12765
- }
12766
- async function trustLocalCa(root) {
12767
- const caPath = join(root, "tls", "local-ca.pem");
12768
- await execFileAsync("/usr/bin/security", [
12769
- "add-trusted-cert",
12770
- "-r",
12771
- "trustRoot",
12772
- "-p",
12773
- "ssl",
12774
- "-s",
12775
- "127.0.0.1",
12776
- "-k",
12777
- loginKeychainPath(),
12778
- caPath
12779
- ]);
12780
- }
12781
- async function isLocalCaTrusted(root) {
12782
- try {
12783
- const ca = await readFile(join(root, "tls", "local-ca.pem"), "utf8");
12784
- const { stdout } = await execFileAsync("/usr/bin/security", [
12785
- "find-certificate",
12786
- "-a",
12787
- "-p",
12788
- "-c",
12789
- CA_COMMON_NAME,
12790
- loginKeychainPath()
12791
- ]);
12792
- return stdout.includes(ca.trim());
12793
- } catch (error) {
12794
- if (error && typeof error === "object" && "code" in error && error.code === "ENOENT") return false;
12795
- const stderr = error && typeof error === "object" && "stderr" in error ? error.stderr : void 0;
12796
- if (typeof stderr === "string" && stderr.trim() === "security: SecKeychainSearchCopyNext: The specified item could not be found in the keychain.") return false;
12797
- throw error;
12798
- }
12799
- }
12800
- async function removeLocalCa(root) {
12801
- const fingerprint = new X509Certificate(await readFile(join(root, "tls", "local-ca.pem"), "utf8")).fingerprint256.replaceAll(":", "");
12802
- const caPath = join(root, "tls", "local-ca.pem");
12803
- await removeTrustSettings([
12804
- "remove-trusted-cert",
12805
- "-d",
12806
- caPath
12807
- ]);
12808
- await removeTrustSettings(["remove-trusted-cert", caPath]);
12809
- await execFileAsync("/usr/bin/security", [
12810
- "delete-certificate",
12811
- "-Z",
12812
- fingerprint,
12813
- loginKeychainPath()
12814
- ]);
12815
- }
12816
- function isMacos() {
12817
- return process.platform === "darwin";
12818
- }
12819
- async function removeTrustSettings(args) {
12820
- try {
12821
- await execFileAsync("/usr/bin/security", args, { env: { LC_ALL: "C" } });
12822
- } catch (error) {
12823
- const stderr = error && typeof error === "object" && "stderr" in error ? error.stderr : void 0;
12824
- if (typeof stderr === "string" && ["SecTrustSettingsRemoveTrustSettings: No Trust Settings were found.", "SecTrustSettingsRemoveTrustSettings: The specified item could not be found in the keychain."].includes(stderr.trim())) return;
12825
- throw error;
12826
- }
12827
- }
12828
- //#endregion
12829
12704
  //#region src/cli/server.ts
12830
12705
  /**
12831
12706
  * `moltnet-agent server` — per-user loopback supervisor (#2061).
12832
12707
  *
12833
- * Starts nothing on its own: it binds 127.0.0.1 and waits for an authorized
12834
- * Console origin to configure agents/providers and start/stop runs.
12708
+ * Starts nothing on its own: it binds 127.0.0.1 or a private native socket and
12709
+ * waits for an authorized controller to configure agents/providers and
12710
+ * start or stop runs.
12835
12711
  */
12836
12712
  var DEFAULT_PORT = OPERATOR_OAUTH.serverPort;
12837
12713
  var DEFAULT_ALLOWED_ORIGINS = "https://console.themolt.net";
12838
12714
  var SHUTDOWN_TIMEOUT_MS = 15e3;
12715
+ var LOCK_HELD_EXIT_CODE = 75;
12716
+ function agentServerLockExitCode(error) {
12717
+ return error.code === "held" ? LOCK_HELD_EXIT_CODE : 1;
12718
+ }
12719
+ function validateNativeSocketOptions(options) {
12720
+ if (!options.nativeSocket) return void 0;
12721
+ if (!options.supervised) return "--native-socket requires --supervised";
12722
+ if (options.port || options.allowedOrigins) return "--native-socket cannot be combined with TCP options";
12723
+ }
12724
+ function nativeSocketValidationOptions(input) {
12725
+ return {
12726
+ ...input.nativeSocket ? { nativeSocket: input.nativeSocket } : {},
12727
+ ...input.supervised ? { supervised: true } : {},
12728
+ ...input.cliPort ? { port: input.cliPort } : {},
12729
+ ...input.cliAllowedOrigins ? { allowedOrigins: input.cliAllowedOrigins } : {}
12730
+ };
12731
+ }
12839
12732
  async function runAgentServer(argv) {
12840
12733
  if (isHelpFlag(argv)) {
12841
12734
  console.log(AGENT_SERVER_HELP);
12842
12735
  return 0;
12843
12736
  }
12844
- const trustRequested = argv[0] === "trust";
12845
- const commandArgs = trustRequested ? argv.slice(1) : argv;
12846
12737
  const envConfig = loadAgentServerEnvConfig();
12847
- if (trustRequested) return runTrustCommand(commandArgs, resolveAgentServerRoot({ root: envConfig.root }));
12848
12738
  const { values } = parseArgs({
12849
- args: commandArgs,
12739
+ args: argv,
12850
12740
  options: {
12851
12741
  port: { type: "string" },
12852
12742
  "allowed-origins": { type: "string" },
@@ -12854,17 +12744,30 @@ async function runAgentServer(argv) {
12854
12744
  "api-url": { type: "string" },
12855
12745
  "heartbeat-interval-ms": { type: "string" },
12856
12746
  "warm-retention-sec": { type: "string" },
12857
- supervised: { type: "boolean" }
12747
+ supervised: { type: "boolean" },
12748
+ "native-socket": { type: "string" }
12858
12749
  }
12859
12750
  });
12751
+ const nativeSocket = values["native-socket"];
12752
+ const nativeSocketError = validateNativeSocketOptions(nativeSocketValidationOptions({
12753
+ ...nativeSocket ? { nativeSocket } : {},
12754
+ ...values.supervised ? { supervised: true } : {},
12755
+ ...values.port ? { cliPort: values.port } : {},
12756
+ ...values["allowed-origins"] ? { cliAllowedOrigins: values["allowed-origins"] } : {},
12757
+ ...envConfig.port ? { envPort: envConfig.port } : {},
12758
+ ...envConfig.allowedOrigins ? { envAllowedOrigins: envConfig.allowedOrigins } : {}
12759
+ }));
12760
+ if (nativeSocketError) {
12761
+ console.error(nativeSocketError);
12762
+ return 1;
12763
+ }
12860
12764
  const port = Number.parseInt(values.port ?? (envConfig.port || `${DEFAULT_PORT}`), 10);
12861
12765
  if (!Number.isInteger(port) || port < 1 || port > 65535) {
12862
12766
  console.error(`Invalid --port: ${String(values.port)}`);
12863
12767
  return 1;
12864
12768
  }
12865
12769
  const allowedOrigins = parseAllowedOrigins(values["allowed-origins"] ?? (envConfig.allowedOrigins || DEFAULT_ALLOWED_ORIGINS));
12866
- const settingsRoot = values.root ?? resolveAgentServerRoot({ root: envConfig.root });
12867
- const connectionSettings = new ConnectionSettingsStore(settingsRoot, {
12770
+ const connectionSettings = new ConnectionSettingsStore(values.root ?? resolveAgentServerRoot({ root: envConfig.root }), {
12868
12771
  ...envConfig.operatorOAuth,
12869
12772
  ...values["api-url"] || envConfig.apiUrl ? { apiUrl: values["api-url"] || envConfig.apiUrl } : {}
12870
12773
  });
@@ -12892,7 +12795,7 @@ async function runAgentServer(argv) {
12892
12795
  env: processEnvSnapshot()
12893
12796
  });
12894
12797
  if (Boolean(values.supervised) && !nativeClient) {
12895
- console.error(`A supervised Agent Server requires ${NATIVE_TOKEN_ENV}. Start it from MoltNet Agent, or omit --supervised to run it to reconnect Console to an operator already established by Desktop.`);
12798
+ console.error(`A supervised Agent Server requires ${NATIVE_TOKEN_ENV}. Start it from MoltNet Agent, or omit --supervised for an authorized standalone controller.`);
12896
12799
  return 1;
12897
12800
  }
12898
12801
  const shutdownController = new AbortController();
@@ -12916,8 +12819,8 @@ async function runAgentServer(argv) {
12916
12819
  runtimeRegistry,
12917
12820
  runtimeSettings
12918
12821
  });
12919
- const tls = isMacos() ? await ensureTrustedLocalTls(settingsRoot) : void 0;
12920
- const selfOrigin = `${tls ? "https" : "http"}://127.0.0.1:${port}`;
12822
+ if (nativeSocket) await validateNativeSocket(nativeSocket);
12823
+ const selfOrigin = nativeSocket ? void 0 : `http://127.0.0.1:${port}`;
12921
12824
  const app = buildAgentServer({
12922
12825
  operatorOAuth: new OperatorOAuth({
12923
12826
  issuer: connection.issuer,
@@ -12928,6 +12831,7 @@ async function runAgentServer(argv) {
12928
12831
  consoleClientId: connection.consoleClientId,
12929
12832
  callbackPort: OPERATOR_OAUTH.callbackPort
12930
12833
  }, root),
12834
+ nativeOnly: Boolean(nativeSocket),
12931
12835
  connectionSettings,
12932
12836
  operatorApiUrl: connection.apiUrl,
12933
12837
  store,
@@ -12939,12 +12843,8 @@ async function runAgentServer(argv) {
12939
12843
  subscriptions,
12940
12844
  providers,
12941
12845
  runtimeRegistry,
12942
- allowedOrigins,
12943
- selfOrigin,
12944
- ...tls ? { tls: {
12945
- key: tls.key,
12946
- cert: tls.cert
12947
- } } : {},
12846
+ allowedOrigins: nativeSocket ? [] : allowedOrigins,
12847
+ ...selfOrigin ? { selfOrigin } : {},
12948
12848
  defaultApiUrl,
12949
12849
  runtimeSettings,
12950
12850
  ...envConfig.activeIdentity ? { activeIdentity: envConfig.activeIdentity } : {},
@@ -12953,15 +12853,17 @@ async function runAgentServer(argv) {
12953
12853
  shutdownSignal: shutdownController.signal
12954
12854
  });
12955
12855
  try {
12956
- const address = await app.listen({
12856
+ const address = await app.listen(nativeSocket ? { path: nativeSocket } : {
12957
12857
  host: "127.0.0.1",
12958
12858
  port
12959
12859
  });
12860
+ if (nativeSocket) await chmod(nativeSocket, 384);
12960
12861
  console.error(`moltnet-agent server listening on ${address}`);
12961
12862
  console.error(`config root: ${root}`);
12962
- console.error(`allowed origins: ${allowedOrigins.join(", ")}`);
12863
+ if (nativeSocket) console.error(`native control socket: ${nativeSocket}`);
12864
+ else console.error(`allowed origins: ${allowedOrigins.join(", ")}`);
12963
12865
  if (nativeClient) console.error("native desktop client: authorized");
12964
- console.error("Sign in through Desktop, then connect from the Console \"Local runtime\" page.");
12866
+ if (!nativeSocket) console.error("Connect from an allowed local-control client after operator authorization.");
12965
12867
  return await waitForAgentServerShutdown(runs, app, shutdownController, Boolean(values.supervised));
12966
12868
  } catch (cause) {
12967
12869
  await app.close().catch(() => void 0);
@@ -12975,7 +12877,7 @@ async function runAgentServer(argv) {
12975
12877
  } catch (cause) {
12976
12878
  if (cause instanceof AgentServerLockError) {
12977
12879
  console.error(cause.message);
12978
- return 1;
12880
+ return agentServerLockExitCode(cause);
12979
12881
  }
12980
12882
  throw cause;
12981
12883
  }
@@ -12983,99 +12885,6 @@ async function runAgentServer(argv) {
12983
12885
  await shutdownLogger();
12984
12886
  }
12985
12887
  }
12986
- async function runTrustCommand(argv, defaultRoot) {
12987
- try {
12988
- const { values } = parseArgs({
12989
- args: argv,
12990
- options: {
12991
- root: { type: "string" },
12992
- remove: { type: "boolean" },
12993
- status: { type: "boolean" },
12994
- yes: { type: "boolean" },
12995
- json: { type: "boolean" }
12996
- }
12997
- });
12998
- const root = values.root ?? defaultRoot;
12999
- const statusRequested = Boolean(values.status);
13000
- const removeRequested = Boolean(values.remove);
13001
- const yes = Boolean(values.yes);
13002
- const json = Boolean(values.json);
13003
- if (statusRequested && (removeRequested || yes)) {
13004
- console.error("Usage: moltnet-agent server trust --status [--json]");
13005
- return 1;
13006
- }
13007
- if (!isMacos()) {
13008
- if (json) {
13009
- printTrustStatus({
13010
- supported: false,
13011
- trusted: false,
13012
- fingerprint: null
13013
- });
13014
- return 0;
13015
- }
13016
- console.error("Local HTTPS trust setup is currently supported on macOS only.");
13017
- return 1;
13018
- }
13019
- if (statusRequested) {
13020
- const material = await inspectLocalTlsMaterial(root);
13021
- const trusted = material !== null && await isLocalCaTrusted(root);
13022
- if (json) printTrustStatus({
13023
- supported: true,
13024
- trusted,
13025
- fingerprint: material?.fingerprint ?? null
13026
- });
13027
- else console.log(trusted ? `MoltNet local CA ${material?.fingerprint ?? "(not prepared)"} is trusted.` : `MoltNet local CA ${material?.fingerprint ?? "(not prepared)"} is not trusted.`);
13028
- return 0;
13029
- }
13030
- if (json && !yes) {
13031
- console.error("Machine-readable trust changes require --yes after native app consent.");
13032
- return 1;
13033
- }
13034
- if (removeRequested) {
13035
- await removeLocalCa(root);
13036
- if (json) printTrustStatus({
13037
- supported: true,
13038
- trusted: false,
13039
- fingerprint: null
13040
- });
13041
- else console.log("Removed the MoltNet local CA from your login keychain.");
13042
- return 0;
13043
- }
13044
- const material = await ensureLocalTlsMaterial(root);
13045
- if (yes) await trustLocalCa(root);
13046
- else await ensureTrustedLocalTls(root);
13047
- if (json) printTrustStatus({
13048
- supported: true,
13049
- trusted: await isLocalCaTrusted(root),
13050
- fingerprint: material.fingerprint
13051
- });
13052
- else console.log("MoltNet local HTTPS trust is ready for this macOS user.");
13053
- return 0;
13054
- } catch (cause) {
13055
- console.error(`Agent Server trust command failed: ${cause instanceof Error ? cause.message : String(cause)}`);
13056
- return 1;
13057
- }
13058
- }
13059
- function printTrustStatus(status) {
13060
- console.log(JSON.stringify(status));
13061
- }
13062
- async function ensureTrustedLocalTls(root) {
13063
- const material = await ensureLocalTlsMaterial(root);
13064
- if (await isLocalCaTrusted(root)) return material;
13065
- 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.");
13066
- const prompt = createInterface({
13067
- input: process.stdin,
13068
- output: process.stdout
13069
- });
13070
- try {
13071
- const answer = await prompt.question(`Trust MoltNet's local CA (${material.fingerprint}) in this macOS login keychain? [y/N] `);
13072
- if (!/^y(es)?$/i.test(answer.trim())) throw new Error("Local HTTPS trust was not approved.");
13073
- } finally {
13074
- prompt.close();
13075
- }
13076
- await trustLocalCa(root);
13077
- return material;
13078
- }
13079
12888
  function waitForAgentServerShutdown(runs, app, shutdownController, supervised) {
13080
12889
  return new Promise((resolvePromise) => {
13081
12890
  let shuttingDown = false;
@@ -13488,7 +13297,7 @@ async function writeCache(cache) {
13488
13297
  }
13489
13298
  //#endregion
13490
13299
  //#region src/version.ts
13491
- var DAEMON_VERSION = "0.61.0";
13300
+ var DAEMON_VERSION = "0.62.0";
13492
13301
  //#endregion
13493
13302
  //#region src/cli.ts
13494
13303
  async function runAgentDaemonCli(options) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@themoltnet/agent-daemon",
3
- "version": "0.61.0",
3
+ "version": "0.62.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,7 +75,6 @@
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",
79
78
  "fastify": "^5.8.5",
80
79
  "fastify-plugin": "^5.1.0",
81
80
  "jose": "^6.1.3",
@@ -84,11 +83,10 @@
84
83
  "pino-opentelemetry-transport": "^3.0.0",
85
84
  "pino-pretty": "^13.1.3",
86
85
  "proper-lockfile": "4.1.2",
87
- "reflect-metadata": "^0.2.2",
88
86
  "typebox": "^1.2.8",
89
87
  "@themoltnet/agent-runtime": "1.2.0",
90
- "@themoltnet/os-keyring": "0.3.0",
91
88
  "@themoltnet/pi-runtime": "0.18.2",
89
+ "@themoltnet/os-keyring": "0.3.0",
92
90
  "@themoltnet/sdk": "0.144.0"
93
91
  },
94
92
  "devDependencies": {
@@ -99,15 +97,15 @@
99
97
  "vite": "^8.0.0",
100
98
  "vite-plugin-dts": "^4.5.4",
101
99
  "vitest": "^3.0.0",
102
- "@moltnet/agent-eval": "0.1.0",
103
- "@moltnet/crypto-service": "0.1.0",
104
100
  "@moltnet/api-client": "0.1.0",
101
+ "@moltnet/agent-eval": "0.1.0",
105
102
  "@moltnet/bootstrap": "0.1.0",
103
+ "@moltnet/crypto-service": "0.1.0",
106
104
  "@moltnet/execution-integrations": "0.1.0",
107
- "@moltnet/execution-plan": "0.1.0",
108
- "@moltnet/loopback-companion": "0.1.0",
109
105
  "@moltnet/models": "0.1.0",
110
106
  "@moltnet/observability": "0.1.0",
107
+ "@moltnet/loopback-companion": "0.1.0",
108
+ "@moltnet/execution-plan": "0.1.0",
111
109
  "@moltnet/runtime-profiles": "0.1.0",
112
110
  "@moltnet/tasks": "0.1.0"
113
111
  },