@themoltnet/agent-daemon 0.53.0 → 0.54.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 +39 -20
- package/dist/cli.js +416 -127
- package/package.json +20 -12
package/README.md
CHANGED
|
@@ -81,31 +81,50 @@ All config flows from environment variables. The daemon reads them in
|
|
|
81
81
|
|
|
82
82
|
### MoltNet identity
|
|
83
83
|
|
|
84
|
-
| Var | Required
|
|
85
|
-
| --------------------- |
|
|
86
|
-
| `GIT_CONFIG_GLOBAL` |
|
|
87
|
-
| `MOLTNET_AGENT_NAME` | yes
|
|
88
|
-
| `MOLTNET_API_URL` |
|
|
89
|
-
| `MOLTNET_AGENT_KEY` | no
|
|
90
|
-
| `MOLTNET_PRIVATE_KEY` |
|
|
91
|
-
|
|
92
|
-
For
|
|
84
|
+
| Var | Required | Purpose |
|
|
85
|
+
| --------------------- | ---------------------------------- | ------------------------------------------------------------------------- |
|
|
86
|
+
| `GIT_CONFIG_GLOBAL` | config-based | Optional git identity path; not needed for configless startup. |
|
|
87
|
+
| `MOLTNET_AGENT_NAME` | yes | Agent name (matches `.moltnet/<name>/`). |
|
|
88
|
+
| `MOLTNET_API_URL` | configless only | Explicit API endpoint; configless runs never read it from `moltnet.json`. |
|
|
89
|
+
| `MOLTNET_AGENT_KEY` | no | Team- or identity-scoped agent key. Overrides `moltnet.json`. |
|
|
90
|
+
| `MOLTNET_PRIVATE_KEY` | configless `once`, `poll`, `drain` | Base64 Ed25519 seed used by daemon-owned executor attestation. |
|
|
91
|
+
|
|
92
|
+
For config-based runs, the agent's `moltnet.json` and gitconfig live next to
|
|
93
93
|
each other in `.moltnet/<agent>/`. Provision them once via
|
|
94
94
|
[`moltnet agents init`](../../docs/start/install-and-initialize.md#initialize-an-autonomous-agent).
|
|
95
95
|
|
|
96
|
-
**
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
96
|
+
**The daemon runs on an agent key only.** OAuth2 client_credentials is not
|
|
97
|
+
accepted: it hands the daemon the full 17-scope agent grant against a six-scope
|
|
98
|
+
need, and a Hydra token cannot be a Talos derivation parent. A `moltnet.json`
|
|
99
|
+
without `agent_key_ref` is refused at startup with the command that fixes it.
|
|
100
|
+
|
|
101
|
+
The key reaches the daemon two ways, and `MOLTNET_AGENT_KEY` wins when both are
|
|
102
|
+
present:
|
|
103
|
+
|
|
104
|
+
- **Configless** — `MOLTNET_AGENT_KEY` (or `MOLTNET_AGENT_KEY_REF`) in the
|
|
105
|
+
environment. No agent files are read at all.
|
|
106
|
+
- **From `moltnet.json`** — an `agent_key_ref` pointing at a secret provider,
|
|
107
|
+
which is what `moltnet agents keys create --store` writes. The plaintext
|
|
108
|
+
secret never lands in the file.
|
|
109
|
+
|
|
110
|
+
Mint or rotate the key with the CLI, which is separate operator tooling and
|
|
111
|
+
keeps using OAuth2 for its own authentication:
|
|
112
|
+
|
|
113
|
+
```bash
|
|
114
|
+
moltnet agents keys create --agent-id <uuid> --team-id <uuid> \
|
|
115
|
+
--name <agent>-daemon --store
|
|
116
|
+
moltnet agents keys rotate <key-id> --team-id <uuid> --store
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
The daemon reconciles a team-bound key against `--team` at startup; an
|
|
120
|
+
identity-scoped key may select any team where the agent is authorized. It fails
|
|
121
|
+
fast if the key is rejected, is not an agent, or a team binding mismatches. See
|
|
103
122
|
[Run the daemon with an agent key](../../docs/operate/agent-keys.md#run-the-daemon-with-an-agent-key).
|
|
104
123
|
|
|
105
|
-
Daemon authentication and the guest boundary are two separate concerns.
|
|
106
|
-
|
|
107
|
-
`.moltnet/<agent>/moltnet.json` through the host secret provider)
|
|
108
|
-
host-side SDK `Agent` is built. The guest boundary is fixed: **the guest never
|
|
124
|
+
Daemon authentication and the guest boundary are two separate concerns. Where
|
|
125
|
+
the agent key comes from (the environment, or an `agent_key_ref` in
|
|
126
|
+
`.moltnet/<agent>/moltnet.json` resolved through the host secret provider)
|
|
127
|
+
decides how the host-side SDK `Agent` is built. The guest boundary is fixed: **the guest never
|
|
109
128
|
receives MoltNet credential material.** No `.moltnet` file, gitconfig, SSH
|
|
110
129
|
signing key, GitHub App PEM, or MoltNet environment credential is injected into
|
|
111
130
|
Gondolin, and mounted `.moltnet` paths are hidden. Structured MoltNet tools
|
package/dist/cli.js
CHANGED
|
@@ -13,9 +13,9 @@ import { parseArgs, promisify } from "node:util";
|
|
|
13
13
|
import { AgentRuntime, ApiTaskReporter, ApiTaskSource, PollingApiTaskSource, createLocalSeedSigner, resolveAgentIdentity, resolveProfileWarmSessionTtlSec, resolveRuntimeProfile, resolveRuntimeProfiles, validateRuntimeProfilePrerequisites } from "@themoltnet/agent-runtime";
|
|
14
14
|
import { GuestEnvironmentBoundaryError, assertGuestEnvironmentBoundary, createPiRetryTriage, findMainWorktree, isResolvedPathInsideRoot, normalizeRetryTriageResult, redactRetryTriageSecrets, resolveRuntimeProfileModel } from "@themoltnet/pi-runtime";
|
|
15
15
|
import { FILE_SECRET_PROVIDER, FileSecretProvider, connect, createNodeSecretProviderRegistry } from "@themoltnet/sdk/node";
|
|
16
|
-
import {
|
|
17
|
-
import {
|
|
18
|
-
import {
|
|
16
|
+
import { constants, cpSync, createReadStream, createWriteStream, existsSync, lstatSync, mkdirSync, openSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
|
|
17
|
+
import { AuthenticationError, IDENTITY_ALIAS_PATTERN, MoltNetError, agentKeyKey, assertIdentityAlias, assertTrustedConfigApiUrl, createExecutorAttestor, deriveMcpUrl, formatSecretReferenceString, getConfigDir, getIdentityDir, identitySeedKey, parseSecretReferenceString, readConfig, register, requireSecureCredentialApiUrl, resolveAgentKey, resolveEnvSecretReference, resolveIdentitySeed, resolveOAuth2ClientSecret } from "@themoltnet/sdk";
|
|
18
|
+
import { execFile, spawn } from "node:child_process";
|
|
19
19
|
import { X509Certificate, createHash, createPrivateKey, createPublicKey, randomBytes, randomUUID, timingSafeEqual, webcrypto } from "node:crypto";
|
|
20
20
|
import { once } from "node:events";
|
|
21
21
|
import { metrics } from "@opentelemetry/api";
|
|
@@ -36,13 +36,13 @@ import { lock, lockSync } from "proper-lockfile";
|
|
|
36
36
|
import { ModelRuntime, readStoredCredential } from "@earendil-works/pi-coding-agent";
|
|
37
37
|
import { Transform, Writable } from "node:stream";
|
|
38
38
|
import { writePiConfig } from "@themoltnet/pi-runtime/pi-config";
|
|
39
|
-
import { homedir, platform } from "node:os";
|
|
40
39
|
import { pathToFileURL } from "node:url";
|
|
41
40
|
import { StringDecoder } from "node:string_decoder";
|
|
42
41
|
import rateLimit from "@fastify/rate-limit";
|
|
43
42
|
import Fastify from "fastify";
|
|
44
43
|
import { isIP } from "node:net";
|
|
45
44
|
import "reflect-metadata";
|
|
45
|
+
import { homedir, platform } from "node:os";
|
|
46
46
|
import { BasicConstraintsExtension, ExtendedKeyUsage, ExtendedKeyUsageExtension, IP, KeyUsageFlags, KeyUsagesExtension, SubjectAlternativeNameExtension, X509CertificateGenerator } from "@peculiar/x509";
|
|
47
47
|
import { createGzip } from "node:zlib";
|
|
48
48
|
//#region ../../libs/tasks/src/rubric.ts
|
|
@@ -795,7 +795,14 @@ var AGENT_CREDENTIAL_SCOPES = [
|
|
|
795
795
|
CREDENTIAL_SCOPES.TaskExecute
|
|
796
796
|
];
|
|
797
797
|
Object.freeze(ALL_CREDENTIAL_SCOPES.filter((scope) => scope !== CREDENTIAL_SCOPES.HumanProfile));
|
|
798
|
-
|
|
798
|
+
/**
|
|
799
|
+
* REST capabilities exercised by the current MCP tool surface.
|
|
800
|
+
*
|
|
801
|
+
* Intentionally excludes connector invocation, key management, runtime
|
|
802
|
+
* management/read, and task claiming because MCP exposes none of those
|
|
803
|
+
* operations.
|
|
804
|
+
*/
|
|
805
|
+
var MCP_CLIENT_SCOPES = [
|
|
799
806
|
CREDENTIAL_SCOPES.AgentProfile,
|
|
800
807
|
CREDENTIAL_SCOPES.CryptoSign,
|
|
801
808
|
CREDENTIAL_SCOPES.DiaryManage,
|
|
@@ -809,7 +816,13 @@ Object.freeze(ALL_CREDENTIAL_SCOPES.filter((scope) => scope !== CREDENTIAL_SCOPE
|
|
|
809
816
|
CREDENTIAL_SCOPES.TaskRead,
|
|
810
817
|
CREDENTIAL_SCOPES.TeamManage,
|
|
811
818
|
CREDENTIAL_SCOPES.TeamRead
|
|
812
|
-
]
|
|
819
|
+
];
|
|
820
|
+
MCP_CLIENT_SCOPES.filter((scope) => scope !== CREDENTIAL_SCOPES.HumanProfile);
|
|
821
|
+
Object.freeze([...[
|
|
822
|
+
"openid",
|
|
823
|
+
"offline",
|
|
824
|
+
"offline_access"
|
|
825
|
+
], ...MCP_CLIENT_SCOPES]);
|
|
813
826
|
//#endregion
|
|
814
827
|
//#region ../../libs/models/src/preview-sign.ts
|
|
815
828
|
function schemaRef$2(schema, id) {
|
|
@@ -3451,9 +3464,19 @@ Commands:
|
|
|
3451
3464
|
Run \`agent-daemon <command> --help\` for command-specific flags.
|
|
3452
3465
|
|
|
3453
3466
|
Prerequisites:
|
|
3454
|
-
-
|
|
3467
|
+
- configless: MOLTNET_AGENT_KEY (or MOLTNET_AGENT_KEY_REF) and
|
|
3455
3468
|
MOLTNET_PRIVATE_KEY (or MOLTNET_PRIVATE_KEY_REF); no agent files
|
|
3456
|
-
-
|
|
3469
|
+
- config-based and sync-sessions: <agent-root>/.moltnet/<agent>/moltnet.json
|
|
3470
|
+
carrying agent_key_ref (OAuth2 client credentials are not accepted)
|
|
3471
|
+
|
|
3472
|
+
No key yet? Mint one with the CLI (--store writes agent_key_ref into
|
|
3473
|
+
moltnet.json and keeps the secret in a provider):
|
|
3474
|
+
|
|
3475
|
+
moltnet teams list # find the team id
|
|
3476
|
+
moltnet agents keys create --team-id <team-uuid> \\
|
|
3477
|
+
--name <agent>-daemon --store
|
|
3478
|
+
|
|
3479
|
+
https://docs.themolt.net/operate/agent-keys#run-the-daemon-with-an-agent-key
|
|
3457
3480
|
- --profile — remote runtime profile supplies provider/model/sandbox
|
|
3458
3481
|
policy and CWD is used as the VM mountPath.
|
|
3459
3482
|
|
|
@@ -3617,18 +3640,30 @@ function assessIdentityPin(current, expected) {
|
|
|
3617
3640
|
//#endregion
|
|
3618
3641
|
//#region src/lib/agent-context.ts
|
|
3619
3642
|
/**
|
|
3620
|
-
*
|
|
3621
|
-
*
|
|
3622
|
-
*
|
|
3623
|
-
*
|
|
3624
|
-
|
|
3625
|
-
|
|
3626
|
-
|
|
3643
|
+
* Where an operator goes after the daemon refuses to start. The published site
|
|
3644
|
+
* rather than a GitHub blob: it renders, and it tracks the deployed docs
|
|
3645
|
+
* instead of whatever `main` happens to say. Sidebar entry lives in
|
|
3646
|
+
* `docs/.vitepress/config.ts`.
|
|
3647
|
+
*/
|
|
3648
|
+
var AGENT_KEYS_DOC_URL = "https://docs.themolt.net/operate/agent-keys#run-the-daemon-with-an-agent-key";
|
|
3649
|
+
/** Scopes a daemon key must carry; mirrors `DAEMON_REQUIRED_SCOPES`. */
|
|
3650
|
+
var DAEMON_KEY_SCOPES = AGENT_CREDENTIAL_SCOPES;
|
|
3651
|
+
/**
|
|
3652
|
+
* Report where `connect()` will find the key, without ever reading the secret
|
|
3653
|
+
* value into anything logged. A non-blank `MOLTNET_AGENT_KEY` or
|
|
3654
|
+
* `MOLTNET_AGENT_KEY_REF` means configless; otherwise the key comes from
|
|
3655
|
+
* `moltnet.json`. This mirrors the SDK precedence, where an environment key
|
|
3656
|
+
* wins over the config file.
|
|
3657
|
+
*
|
|
3658
|
+
* It reports the *source*, not what `connect()` would pick on its own: the
|
|
3659
|
+
* config path resolves the key and passes it explicitly, because ambient
|
|
3660
|
+
* resolution ranks environment OAuth2 credentials above a configured
|
|
3661
|
+
* `agent_key_ref` (see `resolveAgentContext`).
|
|
3627
3662
|
*
|
|
3628
3663
|
* Pure: `env` is passed in (the config module owns the `process.env` read).
|
|
3629
3664
|
*/
|
|
3630
|
-
function
|
|
3631
|
-
return env.MOLTNET_AGENT_KEY?.trim() || env.MOLTNET_AGENT_KEY_REF?.trim() ? "
|
|
3665
|
+
function detectCredentialSource(env) {
|
|
3666
|
+
return env.MOLTNET_AGENT_KEY?.trim() || env.MOLTNET_AGENT_KEY_REF?.trim() ? "environment" : "config";
|
|
3632
3667
|
}
|
|
3633
3668
|
/**
|
|
3634
3669
|
* Pure check: may the identity described by `whoami` operate the daemon as
|
|
@@ -3684,40 +3719,111 @@ async function validateStartupBinding(options) {
|
|
|
3684
3719
|
/**
|
|
3685
3720
|
* Resolve the agent's MoltNet credentials directory and connect via SDK.
|
|
3686
3721
|
*
|
|
3687
|
-
*
|
|
3688
|
-
*
|
|
3689
|
-
* the daemon never falls back to unauthenticated calls.
|
|
3722
|
+
* The daemon selects the same central identity directory as the CLI. It never
|
|
3723
|
+
* inspects a repository, Git state, or legacy agent bundle for credentials.
|
|
3690
3724
|
*/
|
|
3691
3725
|
async function resolveAgentContext(agentName, options = {}) {
|
|
3692
|
-
|
|
3693
|
-
const
|
|
3694
|
-
if (options.
|
|
3695
|
-
|
|
3696
|
-
|
|
3697
|
-
|
|
3698
|
-
|
|
3699
|
-
|
|
3700
|
-
|
|
3701
|
-
|
|
3702
|
-
|
|
3703
|
-
|
|
3704
|
-
|
|
3705
|
-
|
|
3706
|
-
|
|
3707
|
-
|
|
3708
|
-
|
|
3709
|
-
|
|
3710
|
-
|
|
3711
|
-
|
|
3712
|
-
|
|
3713
|
-
|
|
3714
|
-
|
|
3715
|
-
|
|
3716
|
-
|
|
3726
|
+
assertIdentityAlias(agentName);
|
|
3727
|
+
const { agentDir, agentRootDir } = resolveIdentityLocation(agentName, options.agentRootDir, { requireConfig: options.credentialSource !== "environment" });
|
|
3728
|
+
if (options.credentialSource === "environment") return {
|
|
3729
|
+
agentDir,
|
|
3730
|
+
agentRootDir,
|
|
3731
|
+
agent: await connect({ secretProviders: createNodeSecretProviderRegistry() }),
|
|
3732
|
+
credentialSource: "environment"
|
|
3733
|
+
};
|
|
3734
|
+
const config = await readConfig(agentDir);
|
|
3735
|
+
if (!config?.agent_key_ref) throw new Error(agentKeyRequiredMessage(agentDir, agentName));
|
|
3736
|
+
const secretProviders = createNodeSecretProviderRegistry();
|
|
3737
|
+
const agentKey = await resolveAgentKey(config, secretProviders);
|
|
3738
|
+
if (!agentKey) throw new Error(agentKeyRequiredMessage(agentDir, agentName));
|
|
3739
|
+
return {
|
|
3740
|
+
agentDir,
|
|
3741
|
+
agentRootDir,
|
|
3742
|
+
agent: await connect({
|
|
3743
|
+
configDir: agentDir,
|
|
3744
|
+
secretProviders,
|
|
3745
|
+
agentKey,
|
|
3746
|
+
apiUrl: resolveConfigApiUrl(config, options.envApiUrl)
|
|
3747
|
+
}),
|
|
3748
|
+
credentialSource: "config"
|
|
3749
|
+
};
|
|
3750
|
+
}
|
|
3751
|
+
/**
|
|
3752
|
+
* The daemon runs on an agent key only. A `moltnet.json` carrying OAuth2
|
|
3753
|
+
* client credentials but no `agent_key_ref` is the pre-#2160 shape, and
|
|
3754
|
+
* `connect()` would happily authenticate it — so this has to be refused here
|
|
3755
|
+
* rather than left to surface as an over-scoped token later.
|
|
3756
|
+
*/
|
|
3757
|
+
function agentKeyRequiredMessage(agentDir, agentName) {
|
|
3758
|
+
return [
|
|
3759
|
+
`${join(agentDir, "moltnet.json")} has no "agent_key_ref". The daemon`,
|
|
3760
|
+
`requires a team-bound agent key; OAuth2 client_credentials is no longer`,
|
|
3761
|
+
`accepted.`,
|
|
3762
|
+
``,
|
|
3763
|
+
`Mint one (--agent-id defaults to the agent you authenticate as):`,
|
|
3764
|
+
``,
|
|
3765
|
+
` moltnet teams list # find the team id`,
|
|
3766
|
+
` moltnet agents keys create \\`,
|
|
3767
|
+
` --team-id <team-uuid> \\`,
|
|
3768
|
+
` --name ${agentName}-daemon \\`,
|
|
3769
|
+
` --scopes ${DAEMON_KEY_SCOPES.join(",")} \\`,
|
|
3770
|
+
` --store`,
|
|
3771
|
+
``,
|
|
3772
|
+
`--store writes "agent_key_ref" into moltnet.json and keeps the secret in`,
|
|
3773
|
+
`a provider, so the key itself never lands in the file. Omit --scopes to`,
|
|
3774
|
+
`get the same daemon minimum by default.`,
|
|
3775
|
+
``,
|
|
3776
|
+
`To run configless instead, set MOLTNET_AGENT_KEY (or`,
|
|
3777
|
+
`MOLTNET_AGENT_KEY_REF) and skip the config entirely.`,
|
|
3778
|
+
``,
|
|
3779
|
+
`Full guide: ${AGENT_KEYS_DOC_URL}`
|
|
3780
|
+
].join("\n");
|
|
3781
|
+
}
|
|
3782
|
+
/**
|
|
3783
|
+
* The central identity directory, unless an explicit `--agent-root` names a
|
|
3784
|
+
* legacy bundle that actually exists.
|
|
3785
|
+
*
|
|
3786
|
+
* The flag is documented as "Directory that owns .moltnet/<agent>" and is still
|
|
3787
|
+
* accepted by `once`, `poll` and `sync-sessions`. Ignoring it silently sent
|
|
3788
|
+
* every caller that passes one — sandboxed runs, the e2e harness — to a
|
|
3789
|
+
* central store they never populated, and failed with a bare "No credentials
|
|
3790
|
+
* found".
|
|
3791
|
+
*/
|
|
3792
|
+
/**
|
|
3793
|
+
* `agentDir` is where credentials live; `agentRootDir` is the directory that
|
|
3794
|
+
* OWNS it and is mounted into the sandbox. They differ for a legacy bundle
|
|
3795
|
+
* (`<root>/.moltnet/<agent>` inside `<root>`) and coincide for a central
|
|
3796
|
+
* identity, which owns nothing above itself. Collapsing them mounted the
|
|
3797
|
+
* credentials directory itself into the guest.
|
|
3798
|
+
*/
|
|
3799
|
+
function resolveIdentityLocation(agentName, explicitRootDir, { requireConfig }) {
|
|
3800
|
+
const root = explicitRootDir?.trim();
|
|
3801
|
+
if (root) {
|
|
3802
|
+
const bundle = join(root, ".moltnet", agentName);
|
|
3803
|
+
if (!requireConfig || existsSync(join(bundle, "moltnet.json"))) return {
|
|
3804
|
+
agentDir: bundle,
|
|
3805
|
+
agentRootDir: root
|
|
3717
3806
|
};
|
|
3718
3807
|
}
|
|
3719
|
-
const
|
|
3720
|
-
|
|
3808
|
+
const central = getIdentityDir(agentName);
|
|
3809
|
+
return {
|
|
3810
|
+
agentDir: central,
|
|
3811
|
+
agentRootDir: central
|
|
3812
|
+
};
|
|
3813
|
+
}
|
|
3814
|
+
/**
|
|
3815
|
+
* Pick the API URL for an explicitly-keyed connect, preserving the checks
|
|
3816
|
+
* ambient config resolution would have applied. An explicit `apiUrl` skips
|
|
3817
|
+
* ambient's own normalisation, so a URL taken from `moltnet.json` still has to
|
|
3818
|
+
* clear the config-trust and transport checks before it is used.
|
|
3819
|
+
*/
|
|
3820
|
+
function resolveConfigApiUrl(config, envApiUrl) {
|
|
3821
|
+
if (envApiUrl?.trim()) return void 0;
|
|
3822
|
+
const fromConfig = config.endpoints?.api?.trim();
|
|
3823
|
+
if (!fromConfig) return void 0;
|
|
3824
|
+
assertTrustedConfigApiUrl(fromConfig);
|
|
3825
|
+
requireSecureCredentialApiUrl(fromConfig);
|
|
3826
|
+
return fromConfig;
|
|
3721
3827
|
}
|
|
3722
3828
|
function isTransientWhoamiError(error) {
|
|
3723
3829
|
if (error instanceof TypeError) return true;
|
|
@@ -3725,26 +3831,6 @@ function isTransientWhoamiError(error) {
|
|
|
3725
3831
|
const statusCode = error.statusCode;
|
|
3726
3832
|
return typeof statusCode === "number" && (statusCode === 408 || statusCode === 429 || statusCode >= 500);
|
|
3727
3833
|
}
|
|
3728
|
-
function locateAgentConfig(roots, agentName) {
|
|
3729
|
-
for (const rootDir of roots) {
|
|
3730
|
-
const agentDir = join(rootDir, ".moltnet", agentName);
|
|
3731
|
-
if (existsSync(join(agentDir, "moltnet.json"))) return {
|
|
3732
|
-
rootDir,
|
|
3733
|
-
agentDir
|
|
3734
|
-
};
|
|
3735
|
-
}
|
|
3736
|
-
}
|
|
3737
|
-
function resolveCredentialRoots(agentRootDir) {
|
|
3738
|
-
const roots = agentRootDir ? [agentRootDir] : [];
|
|
3739
|
-
try {
|
|
3740
|
-
const gitRoot = execFileSync("git", ["rev-parse", "--show-toplevel"], {
|
|
3741
|
-
encoding: "utf8",
|
|
3742
|
-
stdio: "pipe"
|
|
3743
|
-
}).trim();
|
|
3744
|
-
if (!roots.includes(gitRoot)) roots.push(gitRoot);
|
|
3745
|
-
} catch {}
|
|
3746
|
-
return roots;
|
|
3747
|
-
}
|
|
3748
3834
|
//#endregion
|
|
3749
3835
|
//#region src/config.ts
|
|
3750
3836
|
/**
|
|
@@ -3764,7 +3850,8 @@ function loadConfig() {
|
|
|
3764
3850
|
profilePrerequisiteEnv: process.env,
|
|
3765
3851
|
profilePrerequisitePath: process.env.PATH ?? "",
|
|
3766
3852
|
piCodingAgentDir: process.env["PI_CODING_AGENT_DIR"] ?? "",
|
|
3767
|
-
|
|
3853
|
+
credentialSource: detectCredentialSource(process.env),
|
|
3854
|
+
apiUrl: process.env["MOLTNET_API_URL"] ?? "",
|
|
3768
3855
|
signingPrivateKey: process.env["MOLTNET_PRIVATE_KEY"] ?? "",
|
|
3769
3856
|
signingPrivateKeyRef: process.env["MOLTNET_PRIVATE_KEY_REF"] ?? "",
|
|
3770
3857
|
gitAuthor: process.env["MOLTNET_GIT_AUTHOR"] ?? "",
|
|
@@ -3839,13 +3926,13 @@ async function abortActiveAttemptOnSignal(opts) {
|
|
|
3839
3926
|
//#region src/lib/agent-identity.ts
|
|
3840
3927
|
/**
|
|
3841
3928
|
* Build the non-secret identity projected into guests. Host git config is a
|
|
3842
|
-
* non-secret input and is consulted only on
|
|
3843
|
-
* that configuration for the signing seed; configless
|
|
3844
|
-
*
|
|
3929
|
+
* non-secret input and is consulted only on config-based hosts, which already
|
|
3930
|
+
* read that configuration for the signing seed; configless hosts never touch a
|
|
3931
|
+
* config directory.
|
|
3845
3932
|
*/
|
|
3846
3933
|
async function resolveDaemonAgentIdentity(input) {
|
|
3847
3934
|
let hostGit;
|
|
3848
|
-
if (input.gitAuthor === void 0 && input.
|
|
3935
|
+
if (input.gitAuthor === void 0 && input.credentialSource === "config") {
|
|
3849
3936
|
const config = await readConfig(input.agentDir);
|
|
3850
3937
|
hostGit = config?.git ? {
|
|
3851
3938
|
name: config.git.name,
|
|
@@ -4390,26 +4477,26 @@ function recoverScratchWorkspacePath(producer, stateDirs) {
|
|
|
4390
4477
|
//#region src/lib/executor-attestation.ts
|
|
4391
4478
|
var DAEMON_REQUIRED_SCOPES = AGENT_CREDENTIAL_SCOPES;
|
|
4392
4479
|
async function resolveExecutorSigningPrivateKey(input) {
|
|
4393
|
-
if (input.
|
|
4480
|
+
if (input.credentialSource === "environment") {
|
|
4394
4481
|
const privateKey = input.configuredPrivateKey.trim();
|
|
4395
4482
|
if (privateKey) return privateKey;
|
|
4396
4483
|
const reference = input.configuredPrivateKeyRef?.trim();
|
|
4397
|
-
if (!reference) throw new Error("
|
|
4484
|
+
if (!reference) throw new Error("Configless daemon startup requires MOLTNET_PRIVATE_KEY (or MOLTNET_PRIVATE_KEY_REF) containing the base64-encoded Ed25519 private key seed.");
|
|
4398
4485
|
let resolved;
|
|
4399
4486
|
try {
|
|
4400
4487
|
resolved = await resolveEnvSecretReference(reference, createNodeSecretProviderRegistry());
|
|
4401
4488
|
} catch (cause) {
|
|
4402
|
-
throw new Error(`
|
|
4489
|
+
throw new Error(`Configless daemon startup could not resolve MOLTNET_PRIVATE_KEY_REF: ${cause.message}`, { cause });
|
|
4403
4490
|
}
|
|
4404
4491
|
if (Buffer.from(resolved, "base64").length !== 32) throw new Error("MOLTNET_PRIVATE_KEY_REF must resolve to a base64-encoded 32-byte Ed25519 seed.");
|
|
4405
4492
|
return resolved;
|
|
4406
4493
|
}
|
|
4407
4494
|
const config = await readConfig(input.agentDir);
|
|
4408
|
-
if (!config) throw new Error(`
|
|
4495
|
+
if (!config) throw new Error(`Config-based daemon startup requires ${input.agentDir}/moltnet.json.`);
|
|
4409
4496
|
try {
|
|
4410
4497
|
return await resolveIdentitySeed(config, createNodeSecretProviderRegistry());
|
|
4411
4498
|
} catch (cause) {
|
|
4412
|
-
throw new Error(`
|
|
4499
|
+
throw new Error(`Config-based daemon startup could not resolve the signing seed from ${input.agentDir}/moltnet.json (keys.private_key or keys.private_key_ref): ${cause.message}`, { cause });
|
|
4413
4500
|
}
|
|
4414
4501
|
}
|
|
4415
4502
|
function validateDaemonScopes(whoami) {
|
|
@@ -5105,7 +5192,7 @@ async function logDaemonStartupFailure(input) {
|
|
|
5105
5192
|
event: "agent-daemon.startup_failed",
|
|
5106
5193
|
gate: input.gate,
|
|
5107
5194
|
agent: input.agent,
|
|
5108
|
-
|
|
5195
|
+
credentialSource: input.credentialSource
|
|
5109
5196
|
}, "Agent daemon startup validation failed");
|
|
5110
5197
|
await shutdown();
|
|
5111
5198
|
}
|
|
@@ -5924,13 +6011,14 @@ async function runPolling(opts) {
|
|
|
5924
6011
|
bindings: cfg.credentialBindings
|
|
5925
6012
|
};
|
|
5926
6013
|
const runtimeCredentialConfig = resolveCredentialEnforcement(cfg.credentialEnforcement, credentialSources) === "off" ? null : loadRuntimeCredentialConfig(credentialSources);
|
|
5927
|
-
const
|
|
6014
|
+
const explicitAgentRootDir = values["agent-root"] ? resolve(process.cwd(), values["agent-root"]) : void 0;
|
|
5928
6015
|
const { ctx, signingPrivateKey, startupWhoami, agentIdentity, hostCapabilitySigner } = await (async () => {
|
|
5929
6016
|
let gate = "resolve_agent_context";
|
|
5930
6017
|
try {
|
|
5931
6018
|
const resolvedContext = await resolveAgentContext(baseCommon.agent, {
|
|
5932
|
-
agentRootDir,
|
|
5933
|
-
|
|
6019
|
+
agentRootDir: explicitAgentRootDir,
|
|
6020
|
+
credentialSource: cfg.credentialSource,
|
|
6021
|
+
envApiUrl: cfg.apiUrl
|
|
5934
6022
|
});
|
|
5935
6023
|
gate = "authenticate_and_bind";
|
|
5936
6024
|
const whoami = await validateStartupBinding({
|
|
@@ -5940,7 +6028,7 @@ async function runPolling(opts) {
|
|
|
5940
6028
|
});
|
|
5941
6029
|
gate = "resolve_signing_material";
|
|
5942
6030
|
const privateKey = await resolveExecutorSigningPrivateKey({
|
|
5943
|
-
|
|
6031
|
+
credentialSource: cfg.credentialSource,
|
|
5944
6032
|
agentDir: resolvedContext.agentDir,
|
|
5945
6033
|
configuredPrivateKey: cfg.signingPrivateKey,
|
|
5946
6034
|
configuredPrivateKeyRef: cfg.signingPrivateKeyRef
|
|
@@ -5956,7 +6044,7 @@ async function runPolling(opts) {
|
|
|
5956
6044
|
const agentIdentity = await resolveDaemonAgentIdentity({
|
|
5957
6045
|
agentName: baseCommon.agent,
|
|
5958
6046
|
whoami,
|
|
5959
|
-
|
|
6047
|
+
credentialSource: cfg.credentialSource,
|
|
5960
6048
|
agentDir: resolvedContext.agentDir,
|
|
5961
6049
|
gitAuthor: values["git-author"] ?? (cfg.gitAuthor || void 0)
|
|
5962
6050
|
});
|
|
@@ -5977,7 +6065,7 @@ async function runPolling(opts) {
|
|
|
5977
6065
|
level: cfg.logLevel || (baseCommon.debug ? "debug" : "info"),
|
|
5978
6066
|
gate,
|
|
5979
6067
|
agent: baseCommon.agent,
|
|
5980
|
-
|
|
6068
|
+
credentialSource: cfg.credentialSource,
|
|
5981
6069
|
error
|
|
5982
6070
|
});
|
|
5983
6071
|
throw error;
|
|
@@ -6081,7 +6169,7 @@ async function runPolling(opts) {
|
|
|
6081
6169
|
resourceAttributes: {
|
|
6082
6170
|
"moltnet.team.id": teamId,
|
|
6083
6171
|
"moltnet.agent.name": baseCommon.agent,
|
|
6084
|
-
"moltnet.
|
|
6172
|
+
"moltnet.credential.source": ctx.credentialSource,
|
|
6085
6173
|
"moltnet.runtime_profile.count": String(profiles.length),
|
|
6086
6174
|
"moltnet.runtime_profile.ids": profiles.map((p) => p.id).join(",")
|
|
6087
6175
|
}
|
|
@@ -6129,7 +6217,7 @@ async function runPolling(opts) {
|
|
|
6129
6217
|
}
|
|
6130
6218
|
});
|
|
6131
6219
|
rootLogger.info({
|
|
6132
|
-
|
|
6220
|
+
credentialSource: cfg.credentialSource,
|
|
6133
6221
|
subjectType: startupWhoami.subjectType,
|
|
6134
6222
|
bindingScope: startupWhoami.credentialBinding?.bindingScope ?? null,
|
|
6135
6223
|
credentialKeyId: startupWhoami.credentialBinding?.keyId ?? null,
|
|
@@ -6581,13 +6669,14 @@ async function runOnce(argv, runtimeAdapter = defaultPiDaemonAdapter) {
|
|
|
6581
6669
|
};
|
|
6582
6670
|
const runtimeCredentialConfig = resolveCredentialEnforcement(cfg.credentialEnforcement, credentialSources) === "off" ? null : loadRuntimeCredentialConfig(credentialSources);
|
|
6583
6671
|
const initialOpts = opts;
|
|
6584
|
-
const
|
|
6672
|
+
const explicitAgentRootDir = values["agent-root"] ? resolve(process.cwd(), values["agent-root"]) : void 0;
|
|
6585
6673
|
const { ctx, signingPrivateKey, agentIdentity, hostCapabilitySigner } = await (async () => {
|
|
6586
6674
|
let gate = "resolve_agent_context";
|
|
6587
6675
|
try {
|
|
6588
6676
|
const resolvedContext = await resolveAgentContext(initialOpts.agent, {
|
|
6589
|
-
agentRootDir,
|
|
6590
|
-
|
|
6677
|
+
agentRootDir: explicitAgentRootDir,
|
|
6678
|
+
credentialSource: cfg.credentialSource,
|
|
6679
|
+
envApiUrl: cfg.apiUrl
|
|
6591
6680
|
});
|
|
6592
6681
|
gate = "authenticate_and_bind";
|
|
6593
6682
|
const whoami = await validateStartupBinding({
|
|
@@ -6597,7 +6686,7 @@ async function runOnce(argv, runtimeAdapter = defaultPiDaemonAdapter) {
|
|
|
6597
6686
|
});
|
|
6598
6687
|
gate = "resolve_signing_material";
|
|
6599
6688
|
const privateKey = await resolveExecutorSigningPrivateKey({
|
|
6600
|
-
|
|
6689
|
+
credentialSource: cfg.credentialSource,
|
|
6601
6690
|
agentDir: resolvedContext.agentDir,
|
|
6602
6691
|
configuredPrivateKey: cfg.signingPrivateKey,
|
|
6603
6692
|
configuredPrivateKeyRef: cfg.signingPrivateKeyRef
|
|
@@ -6613,7 +6702,7 @@ async function runOnce(argv, runtimeAdapter = defaultPiDaemonAdapter) {
|
|
|
6613
6702
|
const agentIdentity = await resolveDaemonAgentIdentity({
|
|
6614
6703
|
agentName: initialOpts.agent,
|
|
6615
6704
|
whoami,
|
|
6616
|
-
|
|
6705
|
+
credentialSource: cfg.credentialSource,
|
|
6617
6706
|
agentDir: resolvedContext.agentDir,
|
|
6618
6707
|
gitAuthor: values["git-author"] ?? (cfg.gitAuthor || void 0)
|
|
6619
6708
|
});
|
|
@@ -6633,7 +6722,7 @@ async function runOnce(argv, runtimeAdapter = defaultPiDaemonAdapter) {
|
|
|
6633
6722
|
level: cfg.logLevel || (initialOpts.debug ? "debug" : "info"),
|
|
6634
6723
|
gate,
|
|
6635
6724
|
agent: initialOpts.agent,
|
|
6636
|
-
|
|
6725
|
+
credentialSource: cfg.credentialSource,
|
|
6637
6726
|
error
|
|
6638
6727
|
});
|
|
6639
6728
|
throw error;
|
|
@@ -6703,7 +6792,7 @@ async function runOnce(argv, runtimeAdapter = defaultPiDaemonAdapter) {
|
|
|
6703
6792
|
resourceAttributes: {
|
|
6704
6793
|
"moltnet.task.id": taskId,
|
|
6705
6794
|
"moltnet.agent.name": opts.agent,
|
|
6706
|
-
"moltnet.
|
|
6795
|
+
"moltnet.credential.source": ctx.credentialSource,
|
|
6707
6796
|
"moltnet.llm.provider": profile.provider,
|
|
6708
6797
|
"moltnet.llm.model": profile.model,
|
|
6709
6798
|
...profile.thinkingLevel ? { "moltnet.llm.thinking_level": profile.thinkingLevel } : {},
|
|
@@ -7752,11 +7841,14 @@ function snapshot(login) {
|
|
|
7752
7841
|
...waitingForAuthorization ? { waitingForAuthorization } : {}
|
|
7753
7842
|
};
|
|
7754
7843
|
}
|
|
7755
|
-
var NAME_RE =
|
|
7844
|
+
var NAME_RE = IDENTITY_ALIAS_PATTERN;
|
|
7756
7845
|
var PROVIDER_ID_RE = /^[a-z0-9][a-z0-9-]{0,63}$/;
|
|
7757
7846
|
function assertStoreName(kind, value) {
|
|
7758
|
-
|
|
7759
|
-
|
|
7847
|
+
try {
|
|
7848
|
+
return assertIdentityAlias(value);
|
|
7849
|
+
} catch {
|
|
7850
|
+
throw new AgentServerStoreError("invalid_name", `${kind} must match ${NAME_RE.source}`);
|
|
7851
|
+
}
|
|
7760
7852
|
}
|
|
7761
7853
|
function assertProviderId(value) {
|
|
7762
7854
|
if (!PROVIDER_ID_RE.test(value)) throw new AgentServerStoreError("invalid_name", `provider id must match ${PROVIDER_ID_RE.source}`);
|
|
@@ -7769,12 +7861,46 @@ var AgentServerStoreError = class extends Error {
|
|
|
7769
7861
|
this.code = code;
|
|
7770
7862
|
}
|
|
7771
7863
|
};
|
|
7772
|
-
/**
|
|
7864
|
+
/**
|
|
7865
|
+
* `MOLTNET_AGENT_SERVER_ROOT` override, else `~/.config/moltnet`.
|
|
7866
|
+
*
|
|
7867
|
+
* Deliberately does NOT consult `XDG_CONFIG_HOME`. The Go CLI's GetConfigDir
|
|
7868
|
+
* and @moltnet/agent-config's getConfigDir both resolve `~/.config/moltnet`,
|
|
7869
|
+
* so honouring XDG here gave one application two config roots: on a machine
|
|
7870
|
+
* with the variable set, the daemon wrote identities the CLI and SDK could not
|
|
7871
|
+
* read. `MOLTNET_AGENT_SERVER_ROOT` remains the explicit escape hatch for a
|
|
7872
|
+
* genuinely custom location. The XDG parameter is gone rather than ignored: an
|
|
7873
|
+
* accepted-but-discarded argument reads like it still works. Callers adopting
|
|
7874
|
+
* state from the old root pass it to ensure() instead.
|
|
7875
|
+
*/
|
|
7773
7876
|
function resolveAgentServerRoot(input) {
|
|
7774
7877
|
const override = input.root?.trim();
|
|
7775
7878
|
if (override) return override;
|
|
7776
|
-
|
|
7777
|
-
|
|
7879
|
+
return getConfigDir();
|
|
7880
|
+
}
|
|
7881
|
+
/** The pre-#1834 root that honoured XDG_CONFIG_HOME, if it differs. */
|
|
7882
|
+
function legacyXdgAgentServerRoot(xdgConfigHome) {
|
|
7883
|
+
const xdg = xdgConfigHome.trim();
|
|
7884
|
+
if (!xdg) return null;
|
|
7885
|
+
const legacy = join(xdg, "moltnet");
|
|
7886
|
+
return legacy === resolveAgentServerRoot({}) ? null : legacy;
|
|
7887
|
+
}
|
|
7888
|
+
/** True when a root holds agent-server state worth preserving. */
|
|
7889
|
+
function hasAgentServerState(root) {
|
|
7890
|
+
for (const child of [
|
|
7891
|
+
"agent-server.json",
|
|
7892
|
+
"identity-selector.json",
|
|
7893
|
+
"identities",
|
|
7894
|
+
"agents"
|
|
7895
|
+
]) try {
|
|
7896
|
+
if (readdirSync(join(root, child)).length > 0) return true;
|
|
7897
|
+
} catch {
|
|
7898
|
+
try {
|
|
7899
|
+
readFileSync(join(root, child));
|
|
7900
|
+
return true;
|
|
7901
|
+
} catch {}
|
|
7902
|
+
}
|
|
7903
|
+
return false;
|
|
7778
7904
|
}
|
|
7779
7905
|
function providerEnvName(providerId) {
|
|
7780
7906
|
return `MOLTNET_PROVIDER_${assertProviderId(providerId).replaceAll("-", "_").toUpperCase()}_API_KEY`;
|
|
@@ -7812,14 +7938,14 @@ function writeJsonAtomic(path, value) {
|
|
|
7812
7938
|
}
|
|
7813
7939
|
var AgentServerStore = class {
|
|
7814
7940
|
root;
|
|
7815
|
-
|
|
7941
|
+
identitiesDir;
|
|
7816
7942
|
runsDir;
|
|
7817
7943
|
secretsDir;
|
|
7818
7944
|
/** Shared Pi credential dir; `auth.json` inside is pi-managed (lockfiled). */
|
|
7819
7945
|
piDir;
|
|
7820
7946
|
constructor(root) {
|
|
7821
7947
|
this.root = root;
|
|
7822
|
-
this.
|
|
7948
|
+
this.identitiesDir = join(root, "identities");
|
|
7823
7949
|
this.runsDir = join(root, "runs");
|
|
7824
7950
|
this.secretsDir = join(root, "secrets");
|
|
7825
7951
|
this.piDir = join(root, "pi");
|
|
@@ -7828,18 +7954,85 @@ var AgentServerStore = class {
|
|
|
7828
7954
|
return join(this.piDir, "auth.json");
|
|
7829
7955
|
}
|
|
7830
7956
|
/** Create the directory layout (0700) if missing. Idempotent. */
|
|
7831
|
-
ensure() {
|
|
7957
|
+
ensure(options = {}) {
|
|
7958
|
+
this.adoptLegacyXdgRoot(options.legacyXdgConfigHome ?? "");
|
|
7832
7959
|
for (const dir of [
|
|
7833
7960
|
this.root,
|
|
7834
|
-
this.
|
|
7961
|
+
this.identitiesDir,
|
|
7835
7962
|
this.runsDir,
|
|
7836
7963
|
this.secretsDir
|
|
7837
7964
|
]) mkdirSync(dir, {
|
|
7838
7965
|
recursive: true,
|
|
7839
7966
|
mode: 448
|
|
7840
7967
|
});
|
|
7968
|
+
this.migrateLegacyAgentDocuments();
|
|
7841
7969
|
return this;
|
|
7842
7970
|
}
|
|
7971
|
+
/**
|
|
7972
|
+
* Adopt state left at the pre-#1834 XDG root.
|
|
7973
|
+
*
|
|
7974
|
+
* The daemon used to resolve `$XDG_CONFIG_HOME/moltnet` while the CLI and SDK
|
|
7975
|
+
* resolved `~/.config/moltnet`. Aligning the root without this would silently
|
|
7976
|
+
* orphan an existing daemon's entire state — it would come up reporting zero
|
|
7977
|
+
* managed agents.
|
|
7978
|
+
*/
|
|
7979
|
+
adoptLegacyXdgRoot(xdgConfigHome) {
|
|
7980
|
+
const legacyRoot = legacyXdgAgentServerRoot(xdgConfigHome);
|
|
7981
|
+
if (!legacyRoot || legacyRoot === this.root) return;
|
|
7982
|
+
if (!hasAgentServerState(legacyRoot)) return;
|
|
7983
|
+
if (hasAgentServerState(this.root)) throw new AgentServerStoreError("invalid_state", `agent server state exists at both ${legacyRoot} (the pre-1834 XDG_CONFIG_HOME location) and ${this.root}. The daemon now shares ${this.root} with the CLI and SDK. Merge or remove one of them, or set MOLTNET_AGENT_SERVER_ROOT to choose explicitly.`);
|
|
7984
|
+
mkdirSync(resolve(this.root, ".."), {
|
|
7985
|
+
recursive: true,
|
|
7986
|
+
mode: 448
|
|
7987
|
+
});
|
|
7988
|
+
try {
|
|
7989
|
+
if (readdirSync(this.root).length === 0) rmSync(this.root, { recursive: true });
|
|
7990
|
+
} catch {}
|
|
7991
|
+
try {
|
|
7992
|
+
renameSync(legacyRoot, this.root);
|
|
7993
|
+
} catch (error) {
|
|
7994
|
+
if (error.code !== "EXDEV") throw new AgentServerStoreError("io_error", `could not adopt the pre-1834 agent server state at ${legacyRoot}: ${String(error.message)}. Move it to ${this.root} manually, or set MOLTNET_AGENT_SERVER_ROOT to choose a root explicitly.`);
|
|
7995
|
+
cpSync(legacyRoot, this.root, { recursive: true });
|
|
7996
|
+
rmSync(legacyRoot, {
|
|
7997
|
+
recursive: true,
|
|
7998
|
+
force: true
|
|
7999
|
+
});
|
|
8000
|
+
}
|
|
8001
|
+
}
|
|
8002
|
+
/**
|
|
8003
|
+
* Migrate managed documents from the pre-#1834 `agents/<alias>.json` layout
|
|
8004
|
+
* to `identities/<alias>/moltnet.json`. Without this an upgraded daemon sees
|
|
8005
|
+
* zero managed agents and reports a plain "not found".
|
|
8006
|
+
*/
|
|
8007
|
+
migrateLegacyAgentDocuments() {
|
|
8008
|
+
const legacyDir = join(this.root, "agents");
|
|
8009
|
+
let entries;
|
|
8010
|
+
try {
|
|
8011
|
+
entries = readdirSync(legacyDir);
|
|
8012
|
+
} catch {
|
|
8013
|
+
return;
|
|
8014
|
+
}
|
|
8015
|
+
for (const entry of entries) {
|
|
8016
|
+
if (!entry.endsWith(".json")) continue;
|
|
8017
|
+
const alias = entry.slice(0, -5);
|
|
8018
|
+
if (!NAME_RE.test(alias)) continue;
|
|
8019
|
+
const legacyPath = join(legacyDir, entry);
|
|
8020
|
+
const target = this.agentPath(alias);
|
|
8021
|
+
if (readJson(target)) continue;
|
|
8022
|
+
const config = readJson(legacyPath);
|
|
8023
|
+
if (!config) continue;
|
|
8024
|
+
mkdirSync(join(this.identitiesDir, alias), {
|
|
8025
|
+
recursive: true,
|
|
8026
|
+
mode: 448
|
|
8027
|
+
});
|
|
8028
|
+
writeJsonAtomic(target, config);
|
|
8029
|
+
rmSync(legacyPath, { force: true });
|
|
8030
|
+
}
|
|
8031
|
+
if (readdirSync(legacyDir).length === 0) rmSync(legacyDir, {
|
|
8032
|
+
recursive: true,
|
|
8033
|
+
force: true
|
|
8034
|
+
});
|
|
8035
|
+
}
|
|
7843
8036
|
get statePath() {
|
|
7844
8037
|
return join(this.root, "agent-server.json");
|
|
7845
8038
|
}
|
|
@@ -7868,16 +8061,58 @@ var AgentServerStore = class {
|
|
|
7868
8061
|
writeJsonAtomic(this.statePath, state);
|
|
7869
8062
|
}
|
|
7870
8063
|
agentPath(name) {
|
|
7871
|
-
return storeChildPath(this.
|
|
8064
|
+
return join(storeChildPath(this.identitiesDir, "identity alias", name), "moltnet.json");
|
|
8065
|
+
}
|
|
8066
|
+
/** Central identity directory, shared with the Go CLI layout. */
|
|
8067
|
+
identityDir(name) {
|
|
8068
|
+
return storeChildPath(this.identitiesDir, "identity alias", name);
|
|
8069
|
+
}
|
|
8070
|
+
get identitySelectorPath() {
|
|
8071
|
+
return join(this.root, "identity-selector.json");
|
|
8072
|
+
}
|
|
8073
|
+
readIdentitySelector() {
|
|
8074
|
+
const selector = readJson(this.identitySelectorPath);
|
|
8075
|
+
if (!selector) return null;
|
|
8076
|
+
if (!isRecord$1(selector) || selector.version !== 1 || selector.default_identity !== void 0 && typeof selector.default_identity !== "string") throw new AgentServerStoreError("invalid_state", "identity-selector.json is not a supported selector document");
|
|
8077
|
+
if (selector.default_identity) assertStoreName("identity alias", selector.default_identity);
|
|
8078
|
+
return selector;
|
|
8079
|
+
}
|
|
8080
|
+
writeIdentitySelector(alias) {
|
|
8081
|
+
writeJsonAtomic(this.identitySelectorPath, {
|
|
8082
|
+
version: 1,
|
|
8083
|
+
default_identity: assertStoreName("identity alias", alias)
|
|
8084
|
+
});
|
|
8085
|
+
}
|
|
8086
|
+
resolveIdentityAlias(explicit, active) {
|
|
8087
|
+
const alias = explicit?.trim() || active?.trim() || this.readIdentitySelector()?.default_identity;
|
|
8088
|
+
if (!alias) throw new AgentServerStoreError("not_found", "no active identity selected");
|
|
8089
|
+
return assertStoreName("identity alias", alias);
|
|
7872
8090
|
}
|
|
7873
8091
|
readAgentConfig(alias) {
|
|
7874
8092
|
return readJson(this.agentPath(alias));
|
|
7875
8093
|
}
|
|
7876
8094
|
writeAgentConfig(alias, config) {
|
|
8095
|
+
mkdirSync(this.identityDir(alias), {
|
|
8096
|
+
recursive: true,
|
|
8097
|
+
mode: 448
|
|
8098
|
+
});
|
|
7877
8099
|
writeJsonAtomic(this.agentPath(alias), config);
|
|
8100
|
+
if (!this.readIdentitySelector()?.default_identity) this.writeIdentitySelector(alias);
|
|
7878
8101
|
}
|
|
7879
8102
|
removeAgentConfig(alias) {
|
|
7880
8103
|
rmSync(this.agentPath(alias), { force: true });
|
|
8104
|
+
this.clearIdentitySelectorIfDefault(alias);
|
|
8105
|
+
}
|
|
8106
|
+
/** Clears the persisted default when it names `alias`. */
|
|
8107
|
+
clearIdentitySelectorIfDefault(alias) {
|
|
8108
|
+
let selector = null;
|
|
8109
|
+
try {
|
|
8110
|
+
selector = this.readIdentitySelector();
|
|
8111
|
+
} catch {
|
|
8112
|
+
return;
|
|
8113
|
+
}
|
|
8114
|
+
if (!selector || selector.default_identity !== alias) return;
|
|
8115
|
+
writeJsonAtomic(this.identitySelectorPath, { version: 1 });
|
|
7881
8116
|
}
|
|
7882
8117
|
readActivation(alias) {
|
|
7883
8118
|
return this.readAgentServerState().activations[assertStoreName("agent name", alias)] ?? null;
|
|
@@ -10387,12 +10622,9 @@ async function runAgentServer(argv) {
|
|
|
10387
10622
|
return 1;
|
|
10388
10623
|
}
|
|
10389
10624
|
const allowedOrigins = parseAllowedOrigins(values["allowed-origins"] ?? (envConfig.allowedOrigins || DEFAULT_ALLOWED_ORIGINS));
|
|
10390
|
-
const root = values.root ?? resolveAgentServerRoot({
|
|
10391
|
-
root: envConfig.root,
|
|
10392
|
-
xdgConfigHome: envConfig.xdgConfigHome
|
|
10393
|
-
});
|
|
10625
|
+
const root = values.root ?? resolveAgentServerRoot({ root: envConfig.root });
|
|
10394
10626
|
const defaultApiUrl = values["api-url"] ?? (envConfig.apiUrl || DEFAULT_API_URL);
|
|
10395
|
-
const store = new AgentServerStore(root).ensure();
|
|
10627
|
+
const store = new AgentServerStore(root).ensure({ legacyXdgConfigHome: envConfig.xdgConfigHome });
|
|
10396
10628
|
if (trustRequested) return runTrustCommand(commandArgs, root);
|
|
10397
10629
|
const { logger, shutdown: shutdownLogger } = createRootLogger({
|
|
10398
10630
|
name: "agent-daemon.server",
|
|
@@ -10686,7 +10918,13 @@ async function runSyncSessions(argv) {
|
|
|
10686
10918
|
const state = parseState(values.state);
|
|
10687
10919
|
const limit = parseLimit(values.limit);
|
|
10688
10920
|
const agentRootDir = resolve(process.cwd(), values["agent-root"] ?? process.cwd());
|
|
10689
|
-
const
|
|
10921
|
+
const explicitAgentRootDir = values["agent-root"] ? resolve(process.cwd(), values["agent-root"]) : void 0;
|
|
10922
|
+
const cfg = loadConfig();
|
|
10923
|
+
const ctx = await resolveAgentContext(opts.agent, {
|
|
10924
|
+
credentialSource: cfg.credentialSource,
|
|
10925
|
+
envApiUrl: cfg.apiUrl,
|
|
10926
|
+
agentRootDir: explicitAgentRootDir
|
|
10927
|
+
});
|
|
10690
10928
|
await validateStartupBinding({
|
|
10691
10929
|
agent: ctx.agent,
|
|
10692
10930
|
teamId: values.team
|
|
@@ -10721,18 +10959,51 @@ function parseLimit(raw) {
|
|
|
10721
10959
|
}
|
|
10722
10960
|
//#endregion
|
|
10723
10961
|
//#region src/lib/update.ts
|
|
10724
|
-
|
|
10962
|
+
/**
|
|
10963
|
+
* `npm install -g @themoltnet/agent-daemon@latest` resolves the registry's own
|
|
10964
|
+
* dist-tag, so for an npm install the registry is the precise answer — the
|
|
10965
|
+
* manifest serves the themolt.net pin, which gates the bundle installer's
|
|
10966
|
+
* default and says nothing about what npm already has. It is also the origin an
|
|
10967
|
+
* npm install already talks to, so this adds no new dependency in CI, where the
|
|
10968
|
+
* daemon runs as `npx @themoltnet/agent-daemon` and would otherwise reach for
|
|
10969
|
+
* api.github.com from shared runner IPs.
|
|
10970
|
+
*
|
|
10971
|
+
* Bundle and direct installs ask the release listing, and their upgrade command
|
|
10972
|
+
* passes `MOLTNET_AGENT_VERSION=latest` so the installer resolves the same thing
|
|
10973
|
+
* rather than falling back to its own pinned default. Reporting the pin here
|
|
10974
|
+
* would hide releases that are already downloadable; reporting the release
|
|
10975
|
+
* without the sentinel would advertise a version the command could not deliver.
|
|
10976
|
+
* The pin still governs a fresh `curl .../install/agent | sh`.
|
|
10977
|
+
*/
|
|
10978
|
+
var UPDATE_RELEASES_URL = "https://api.github.com/repos/getlarge/themoltnet/releases?per_page=100";
|
|
10979
|
+
var RELEASE_TAG_PREFIX = "agent-daemon-v";
|
|
10980
|
+
var UPDATE_NPM_REGISTRY_URL = "https://registry.npmjs.org/@themoltnet/agent-daemon/latest";
|
|
10725
10981
|
var UPDATE_CACHE_TTL_MS = 1440 * 60 * 1e3;
|
|
10726
10982
|
var UPDATE_ERROR_CACHE_TTL_MS = 300 * 1e3;
|
|
10983
|
+
/**
|
|
10984
|
+
* Node does not resolve `process.argv[1]`: invoked through npm's bin shim it
|
|
10985
|
+
* reports `/usr/local/bin/moltnet-agent`, not the `node_modules` path this
|
|
10986
|
+
* matches on. Every global npm install therefore looked like `direct` and was
|
|
10987
|
+
* offered the bundle installer, which would drop a bundle on top of an npm
|
|
10988
|
+
* install. Resolve first; a missing path falls back to the input.
|
|
10989
|
+
*/
|
|
10990
|
+
function resolveDaemonExecutable(executable) {
|
|
10991
|
+
if (!executable) return executable;
|
|
10992
|
+
try {
|
|
10993
|
+
return realpathSync(executable);
|
|
10994
|
+
} catch {
|
|
10995
|
+
return executable;
|
|
10996
|
+
}
|
|
10997
|
+
}
|
|
10727
10998
|
function detectDaemonInstallMethod(executable = process.argv[1] ?? "") {
|
|
10728
|
-
const path = executable.replaceAll("\\", "/");
|
|
10999
|
+
const path = resolveDaemonExecutable(executable).replaceAll("\\", "/");
|
|
10729
11000
|
if (path.includes("/node_modules/@themoltnet/agent-daemon/")) return "npm";
|
|
10730
11001
|
if (path.includes("/.local/share/moltnet/") || path.includes("/opt/moltnet/")) return "bundle";
|
|
10731
11002
|
return "direct";
|
|
10732
11003
|
}
|
|
10733
11004
|
function daemonUpdateCommand(method) {
|
|
10734
11005
|
if (method === "npm") return "npm install -g @themoltnet/agent-daemon@latest";
|
|
10735
|
-
return "curl -fsSL https://themolt.net/install/agent | sh";
|
|
11006
|
+
return "curl -fsSL https://themolt.net/install/agent | MOLTNET_AGENT_VERSION=latest sh";
|
|
10736
11007
|
}
|
|
10737
11008
|
async function checkDaemonUpdate(input) {
|
|
10738
11009
|
const installMethod = detectDaemonInstallMethod(input.executable ?? process.argv[1] ?? "moltnet-agent");
|
|
@@ -10753,11 +11024,15 @@ async function checkDaemonUpdate(input) {
|
|
|
10753
11024
|
return result;
|
|
10754
11025
|
}
|
|
10755
11026
|
}
|
|
11027
|
+
const viaNpm = installMethod === "npm";
|
|
11028
|
+
const source = viaNpm ? UPDATE_NPM_REGISTRY_URL : UPDATE_RELEASES_URL;
|
|
11029
|
+
const label = viaNpm ? "npm registry" : "release listing";
|
|
10756
11030
|
try {
|
|
10757
|
-
const response = await (input.fetchFn ?? fetch)(
|
|
10758
|
-
if (!response.ok) throw new Error(
|
|
10759
|
-
const
|
|
10760
|
-
|
|
11031
|
+
const response = await (input.fetchFn ?? fetch)(source, { signal: AbortSignal.timeout(5e3) });
|
|
11032
|
+
if (!response.ok) throw new Error(`${label} returned HTTP ${response.status}`);
|
|
11033
|
+
const body = await response.json();
|
|
11034
|
+
const latest = viaNpm ? distTagVersion(body) : newestReleaseVersion(body);
|
|
11035
|
+
if (!latest) throw new Error(`${label} has no valid agent version`);
|
|
10761
11036
|
await writeCache({
|
|
10762
11037
|
checkedAt: now.toISOString(),
|
|
10763
11038
|
latest
|
|
@@ -10773,13 +11048,31 @@ async function checkDaemonUpdate(input) {
|
|
|
10773
11048
|
throw new Error(`could not check for MoltNet agent updates: ${error instanceof Error ? error.message : String(error)}`);
|
|
10774
11049
|
}
|
|
10775
11050
|
}
|
|
10776
|
-
function
|
|
11051
|
+
function distTagVersion(value) {
|
|
10777
11052
|
if (!value || typeof value !== "object") return void 0;
|
|
10778
|
-
const
|
|
10779
|
-
if (!agent || typeof agent !== "object") return void 0;
|
|
10780
|
-
const version = agent.version;
|
|
11053
|
+
const version = value.version;
|
|
10781
11054
|
return typeof version === "string" && validVersion(version) ? normalizeVersion(version) : void 0;
|
|
10782
11055
|
}
|
|
11056
|
+
/**
|
|
11057
|
+
* The listing is ordered by creation date, not version, so compare every
|
|
11058
|
+
* candidate rather than trusting position. Drafts are filtered explicitly:
|
|
11059
|
+
* unauthenticated callers never see them, but a token would, and this
|
|
11060
|
+
* repository carries stuck drafts that would otherwise look newest.
|
|
11061
|
+
*/
|
|
11062
|
+
function newestReleaseVersion(value) {
|
|
11063
|
+
if (!Array.isArray(value)) return void 0;
|
|
11064
|
+
let newest;
|
|
11065
|
+
for (const entry of value) {
|
|
11066
|
+
if (!entry || typeof entry !== "object") continue;
|
|
11067
|
+
const { tag_name: tag, draft, prerelease } = entry;
|
|
11068
|
+
if (draft === true || prerelease === true) continue;
|
|
11069
|
+
if (typeof tag !== "string" || !tag.startsWith(RELEASE_TAG_PREFIX)) continue;
|
|
11070
|
+
const candidate = tag.slice(14);
|
|
11071
|
+
if (!validVersion(candidate)) continue;
|
|
11072
|
+
if (!newest || compareVersions(candidate, newest) > 0) newest = normalizeVersion(candidate);
|
|
11073
|
+
}
|
|
11074
|
+
return newest;
|
|
11075
|
+
}
|
|
10783
11076
|
function normalizeVersion(value) {
|
|
10784
11077
|
return value.trim().replace(/^v/, "");
|
|
10785
11078
|
}
|
|
@@ -10819,7 +11112,7 @@ async function writeCache(cache) {
|
|
|
10819
11112
|
}
|
|
10820
11113
|
//#endregion
|
|
10821
11114
|
//#region src/version.ts
|
|
10822
|
-
var DAEMON_VERSION = "0.
|
|
11115
|
+
var DAEMON_VERSION = "0.54.0";
|
|
10823
11116
|
//#endregion
|
|
10824
11117
|
//#region src/cli.ts
|
|
10825
11118
|
async function runAgentDaemonCli(options) {
|
|
@@ -10877,11 +11170,7 @@ async function runUpdate(argv) {
|
|
|
10877
11170
|
}
|
|
10878
11171
|
}
|
|
10879
11172
|
async function runRuntime(argv) {
|
|
10880
|
-
const
|
|
10881
|
-
const registry = new RuntimeRegistry(resolveAgentServerRoot({
|
|
10882
|
-
root: config.root,
|
|
10883
|
-
xdgConfigHome: config.xdgConfigHome
|
|
10884
|
-
}));
|
|
11173
|
+
const registry = new RuntimeRegistry(resolveAgentServerRoot({ root: loadAgentServerEnvConfig().root }));
|
|
10885
11174
|
const [command, ...rest] = argv;
|
|
10886
11175
|
if (command === "list") {
|
|
10887
11176
|
console.log(JSON.stringify(registry.list(), null, 2));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@themoltnet/agent-daemon",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.54.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.",
|
|
@@ -85,10 +85,10 @@
|
|
|
85
85
|
"proper-lockfile": "4.1.2",
|
|
86
86
|
"reflect-metadata": "^0.2.2",
|
|
87
87
|
"typebox": "^1.2.8",
|
|
88
|
-
"@themoltnet/
|
|
89
|
-
"@themoltnet/
|
|
90
|
-
"@themoltnet/
|
|
91
|
-
"@themoltnet/
|
|
88
|
+
"@themoltnet/pi-runtime": "0.14.3",
|
|
89
|
+
"@themoltnet/agent-runtime": "0.46.0",
|
|
90
|
+
"@themoltnet/sdk": "0.140.2",
|
|
91
|
+
"@themoltnet/os-keyring": "0.3.0"
|
|
92
92
|
},
|
|
93
93
|
"devDependencies": {
|
|
94
94
|
"@fastify/swagger": "^9.6.1",
|
|
@@ -98,16 +98,16 @@
|
|
|
98
98
|
"vite": "^8.0.0",
|
|
99
99
|
"vite-plugin-dts": "^4.5.4",
|
|
100
100
|
"vitest": "^3.0.0",
|
|
101
|
-
"@moltnet/agent-eval": "0.1.0",
|
|
102
|
-
"@moltnet/crypto-service": "0.1.0",
|
|
103
|
-
"@moltnet/execution-integrations": "0.1.0",
|
|
104
101
|
"@moltnet/bootstrap": "0.1.0",
|
|
102
|
+
"@moltnet/crypto-service": "0.1.0",
|
|
105
103
|
"@moltnet/execution-plan": "0.1.0",
|
|
106
|
-
"@moltnet/
|
|
107
|
-
"@moltnet/observability": "0.1.0",
|
|
108
|
-
"@moltnet/loopback-companion": "0.1.0",
|
|
104
|
+
"@moltnet/execution-integrations": "0.1.0",
|
|
109
105
|
"@moltnet/runtime-profiles": "0.1.0",
|
|
110
|
-
"@moltnet/tasks": "0.1.0"
|
|
106
|
+
"@moltnet/tasks": "0.1.0",
|
|
107
|
+
"@moltnet/loopback-companion": "0.1.0",
|
|
108
|
+
"@moltnet/agent-eval": "0.1.0",
|
|
109
|
+
"@moltnet/observability": "0.1.0",
|
|
110
|
+
"@moltnet/models": "0.1.0"
|
|
111
111
|
},
|
|
112
112
|
"nx": {
|
|
113
113
|
"projectType": "application",
|
|
@@ -132,6 +132,14 @@
|
|
|
132
132
|
"cwd": "{workspaceRoot}",
|
|
133
133
|
"command": "node tools/release/agent-bundle/build.mjs --skip-daemon-build --out dist/agent-bundle"
|
|
134
134
|
}
|
|
135
|
+
},
|
|
136
|
+
"test": {
|
|
137
|
+
"inputs": [
|
|
138
|
+
"default",
|
|
139
|
+
"^production",
|
|
140
|
+
"{workspaceRoot}/docs/operate/*.md",
|
|
141
|
+
"{workspaceRoot}/docs/.vitepress/config.ts"
|
|
142
|
+
]
|
|
135
143
|
}
|
|
136
144
|
}
|
|
137
145
|
},
|