@themoltnet/agent-daemon 0.52.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 +53 -26
- package/dist/cli.js +423 -127
- package/package.json +18 -10
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
|
|
@@ -135,14 +154,22 @@ of `MOLTNET_PRIVATE_KEY`.
|
|
|
135
154
|
An agent key used by the daemon needs this least-privilege scope set:
|
|
136
155
|
|
|
137
156
|
```text
|
|
138
|
-
agent:profile runtime:read task:read task:claim task:execute
|
|
157
|
+
agent:profile crypto:sign runtime:read task:read task:claim task:execute
|
|
139
158
|
```
|
|
140
159
|
|
|
141
|
-
The Console selects these
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
160
|
+
The Console selects these scopes by default. Knowledge-enabled workers must add
|
|
161
|
+
`diary:read`, `diary:write`, `pack:read`, and `pack:write` when the key is
|
|
162
|
+
issued.
|
|
163
|
+
|
|
164
|
+
`crypto:sign` is in the minimum because host-capability signing runs on the
|
|
165
|
+
daemon's own credential: the local seed signer calls the signing-request
|
|
166
|
+
endpoints, which require it. Without it the daemon refuses to start rather than
|
|
167
|
+
booting cleanly and failing the first time guest code signs a diary entry or a
|
|
168
|
+
commit. Keys issued with the older five-scope set must be reissued.
|
|
169
|
+
|
|
170
|
+
Runtime policy can narrow key authority but cannot add missing scopes, and
|
|
171
|
+
existing keys are never widened automatically; issue a replacement credential
|
|
172
|
+
when broader authority is required.
|
|
146
173
|
|
|
147
174
|
### Pi provider auth
|
|
148
175
|
|
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
|
|
@@ -779,16 +779,30 @@ var ALL_CREDENTIAL_SCOPES = Object.freeze(Object.values(CREDENTIAL_SCOPES));
|
|
|
779
779
|
/**
|
|
780
780
|
* Minimum grant for the agent daemon. Task credentials attenuate this further
|
|
781
781
|
* to `task:execute` alone.
|
|
782
|
+
*
|
|
783
|
+
* `crypto:sign` is part of the minimum because host-capability signing runs on
|
|
784
|
+
* the daemon's own credential: the local seed signer calls the signing-request
|
|
785
|
+
* endpoints, which require it. A grant without it produces a daemon that boots
|
|
786
|
+
* cleanly and then fails the first time guest code signs a diary entry or a
|
|
787
|
+
* commit.
|
|
782
788
|
*/
|
|
783
789
|
var AGENT_CREDENTIAL_SCOPES = [
|
|
784
790
|
CREDENTIAL_SCOPES.AgentProfile,
|
|
791
|
+
CREDENTIAL_SCOPES.CryptoSign,
|
|
785
792
|
CREDENTIAL_SCOPES.RuntimeRead,
|
|
786
793
|
CREDENTIAL_SCOPES.TaskRead,
|
|
787
794
|
CREDENTIAL_SCOPES.TaskClaim,
|
|
788
795
|
CREDENTIAL_SCOPES.TaskExecute
|
|
789
796
|
];
|
|
790
797
|
Object.freeze(ALL_CREDENTIAL_SCOPES.filter((scope) => scope !== CREDENTIAL_SCOPES.HumanProfile));
|
|
791
|
-
|
|
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 = [
|
|
792
806
|
CREDENTIAL_SCOPES.AgentProfile,
|
|
793
807
|
CREDENTIAL_SCOPES.CryptoSign,
|
|
794
808
|
CREDENTIAL_SCOPES.DiaryManage,
|
|
@@ -802,7 +816,13 @@ Object.freeze(ALL_CREDENTIAL_SCOPES.filter((scope) => scope !== CREDENTIAL_SCOPE
|
|
|
802
816
|
CREDENTIAL_SCOPES.TaskRead,
|
|
803
817
|
CREDENTIAL_SCOPES.TeamManage,
|
|
804
818
|
CREDENTIAL_SCOPES.TeamRead
|
|
805
|
-
]
|
|
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]);
|
|
806
826
|
//#endregion
|
|
807
827
|
//#region ../../libs/models/src/preview-sign.ts
|
|
808
828
|
function schemaRef$2(schema, id) {
|
|
@@ -3444,9 +3464,19 @@ Commands:
|
|
|
3444
3464
|
Run \`agent-daemon <command> --help\` for command-specific flags.
|
|
3445
3465
|
|
|
3446
3466
|
Prerequisites:
|
|
3447
|
-
-
|
|
3467
|
+
- configless: MOLTNET_AGENT_KEY (or MOLTNET_AGENT_KEY_REF) and
|
|
3448
3468
|
MOLTNET_PRIVATE_KEY (or MOLTNET_PRIVATE_KEY_REF); no agent files
|
|
3449
|
-
-
|
|
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
|
|
3450
3480
|
- --profile — remote runtime profile supplies provider/model/sandbox
|
|
3451
3481
|
policy and CWD is used as the VM mountPath.
|
|
3452
3482
|
|
|
@@ -3610,18 +3640,30 @@ function assessIdentityPin(current, expected) {
|
|
|
3610
3640
|
//#endregion
|
|
3611
3641
|
//#region src/lib/agent-context.ts
|
|
3612
3642
|
/**
|
|
3613
|
-
*
|
|
3614
|
-
*
|
|
3615
|
-
*
|
|
3616
|
-
*
|
|
3617
|
-
|
|
3618
|
-
|
|
3619
|
-
|
|
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`).
|
|
3620
3662
|
*
|
|
3621
3663
|
* Pure: `env` is passed in (the config module owns the `process.env` read).
|
|
3622
3664
|
*/
|
|
3623
|
-
function
|
|
3624
|
-
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";
|
|
3625
3667
|
}
|
|
3626
3668
|
/**
|
|
3627
3669
|
* Pure check: may the identity described by `whoami` operate the daemon as
|
|
@@ -3677,40 +3719,111 @@ async function validateStartupBinding(options) {
|
|
|
3677
3719
|
/**
|
|
3678
3720
|
* Resolve the agent's MoltNet credentials directory and connect via SDK.
|
|
3679
3721
|
*
|
|
3680
|
-
*
|
|
3681
|
-
*
|
|
3682
|
-
* 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.
|
|
3683
3724
|
*/
|
|
3684
3725
|
async function resolveAgentContext(agentName, options = {}) {
|
|
3685
|
-
|
|
3686
|
-
const
|
|
3687
|
-
if (options.
|
|
3688
|
-
|
|
3689
|
-
|
|
3690
|
-
|
|
3691
|
-
|
|
3692
|
-
|
|
3693
|
-
|
|
3694
|
-
|
|
3695
|
-
|
|
3696
|
-
|
|
3697
|
-
|
|
3698
|
-
|
|
3699
|
-
|
|
3700
|
-
|
|
3701
|
-
|
|
3702
|
-
|
|
3703
|
-
|
|
3704
|
-
|
|
3705
|
-
|
|
3706
|
-
|
|
3707
|
-
|
|
3708
|
-
|
|
3709
|
-
|
|
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
|
|
3710
3806
|
};
|
|
3711
3807
|
}
|
|
3712
|
-
const
|
|
3713
|
-
|
|
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;
|
|
3714
3827
|
}
|
|
3715
3828
|
function isTransientWhoamiError(error) {
|
|
3716
3829
|
if (error instanceof TypeError) return true;
|
|
@@ -3718,26 +3831,6 @@ function isTransientWhoamiError(error) {
|
|
|
3718
3831
|
const statusCode = error.statusCode;
|
|
3719
3832
|
return typeof statusCode === "number" && (statusCode === 408 || statusCode === 429 || statusCode >= 500);
|
|
3720
3833
|
}
|
|
3721
|
-
function locateAgentConfig(roots, agentName) {
|
|
3722
|
-
for (const rootDir of roots) {
|
|
3723
|
-
const agentDir = join(rootDir, ".moltnet", agentName);
|
|
3724
|
-
if (existsSync(join(agentDir, "moltnet.json"))) return {
|
|
3725
|
-
rootDir,
|
|
3726
|
-
agentDir
|
|
3727
|
-
};
|
|
3728
|
-
}
|
|
3729
|
-
}
|
|
3730
|
-
function resolveCredentialRoots(agentRootDir) {
|
|
3731
|
-
const roots = agentRootDir ? [agentRootDir] : [];
|
|
3732
|
-
try {
|
|
3733
|
-
const gitRoot = execFileSync("git", ["rev-parse", "--show-toplevel"], {
|
|
3734
|
-
encoding: "utf8",
|
|
3735
|
-
stdio: "pipe"
|
|
3736
|
-
}).trim();
|
|
3737
|
-
if (!roots.includes(gitRoot)) roots.push(gitRoot);
|
|
3738
|
-
} catch {}
|
|
3739
|
-
return roots;
|
|
3740
|
-
}
|
|
3741
3834
|
//#endregion
|
|
3742
3835
|
//#region src/config.ts
|
|
3743
3836
|
/**
|
|
@@ -3757,7 +3850,8 @@ function loadConfig() {
|
|
|
3757
3850
|
profilePrerequisiteEnv: process.env,
|
|
3758
3851
|
profilePrerequisitePath: process.env.PATH ?? "",
|
|
3759
3852
|
piCodingAgentDir: process.env["PI_CODING_AGENT_DIR"] ?? "",
|
|
3760
|
-
|
|
3853
|
+
credentialSource: detectCredentialSource(process.env),
|
|
3854
|
+
apiUrl: process.env["MOLTNET_API_URL"] ?? "",
|
|
3761
3855
|
signingPrivateKey: process.env["MOLTNET_PRIVATE_KEY"] ?? "",
|
|
3762
3856
|
signingPrivateKeyRef: process.env["MOLTNET_PRIVATE_KEY_REF"] ?? "",
|
|
3763
3857
|
gitAuthor: process.env["MOLTNET_GIT_AUTHOR"] ?? "",
|
|
@@ -3832,13 +3926,13 @@ async function abortActiveAttemptOnSignal(opts) {
|
|
|
3832
3926
|
//#region src/lib/agent-identity.ts
|
|
3833
3927
|
/**
|
|
3834
3928
|
* Build the non-secret identity projected into guests. Host git config is a
|
|
3835
|
-
* non-secret input and is consulted only on
|
|
3836
|
-
* that configuration for the signing seed; configless
|
|
3837
|
-
*
|
|
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.
|
|
3838
3932
|
*/
|
|
3839
3933
|
async function resolveDaemonAgentIdentity(input) {
|
|
3840
3934
|
let hostGit;
|
|
3841
|
-
if (input.gitAuthor === void 0 && input.
|
|
3935
|
+
if (input.gitAuthor === void 0 && input.credentialSource === "config") {
|
|
3842
3936
|
const config = await readConfig(input.agentDir);
|
|
3843
3937
|
hostGit = config?.git ? {
|
|
3844
3938
|
name: config.git.name,
|
|
@@ -4383,26 +4477,26 @@ function recoverScratchWorkspacePath(producer, stateDirs) {
|
|
|
4383
4477
|
//#region src/lib/executor-attestation.ts
|
|
4384
4478
|
var DAEMON_REQUIRED_SCOPES = AGENT_CREDENTIAL_SCOPES;
|
|
4385
4479
|
async function resolveExecutorSigningPrivateKey(input) {
|
|
4386
|
-
if (input.
|
|
4480
|
+
if (input.credentialSource === "environment") {
|
|
4387
4481
|
const privateKey = input.configuredPrivateKey.trim();
|
|
4388
4482
|
if (privateKey) return privateKey;
|
|
4389
4483
|
const reference = input.configuredPrivateKeyRef?.trim();
|
|
4390
|
-
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.");
|
|
4391
4485
|
let resolved;
|
|
4392
4486
|
try {
|
|
4393
4487
|
resolved = await resolveEnvSecretReference(reference, createNodeSecretProviderRegistry());
|
|
4394
4488
|
} catch (cause) {
|
|
4395
|
-
throw new Error(`
|
|
4489
|
+
throw new Error(`Configless daemon startup could not resolve MOLTNET_PRIVATE_KEY_REF: ${cause.message}`, { cause });
|
|
4396
4490
|
}
|
|
4397
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.");
|
|
4398
4492
|
return resolved;
|
|
4399
4493
|
}
|
|
4400
4494
|
const config = await readConfig(input.agentDir);
|
|
4401
|
-
if (!config) throw new Error(`
|
|
4495
|
+
if (!config) throw new Error(`Config-based daemon startup requires ${input.agentDir}/moltnet.json.`);
|
|
4402
4496
|
try {
|
|
4403
4497
|
return await resolveIdentitySeed(config, createNodeSecretProviderRegistry());
|
|
4404
4498
|
} catch (cause) {
|
|
4405
|
-
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 });
|
|
4406
4500
|
}
|
|
4407
4501
|
}
|
|
4408
4502
|
function validateDaemonScopes(whoami) {
|
|
@@ -5098,7 +5192,7 @@ async function logDaemonStartupFailure(input) {
|
|
|
5098
5192
|
event: "agent-daemon.startup_failed",
|
|
5099
5193
|
gate: input.gate,
|
|
5100
5194
|
agent: input.agent,
|
|
5101
|
-
|
|
5195
|
+
credentialSource: input.credentialSource
|
|
5102
5196
|
}, "Agent daemon startup validation failed");
|
|
5103
5197
|
await shutdown();
|
|
5104
5198
|
}
|
|
@@ -5917,13 +6011,14 @@ async function runPolling(opts) {
|
|
|
5917
6011
|
bindings: cfg.credentialBindings
|
|
5918
6012
|
};
|
|
5919
6013
|
const runtimeCredentialConfig = resolveCredentialEnforcement(cfg.credentialEnforcement, credentialSources) === "off" ? null : loadRuntimeCredentialConfig(credentialSources);
|
|
5920
|
-
const
|
|
6014
|
+
const explicitAgentRootDir = values["agent-root"] ? resolve(process.cwd(), values["agent-root"]) : void 0;
|
|
5921
6015
|
const { ctx, signingPrivateKey, startupWhoami, agentIdentity, hostCapabilitySigner } = await (async () => {
|
|
5922
6016
|
let gate = "resolve_agent_context";
|
|
5923
6017
|
try {
|
|
5924
6018
|
const resolvedContext = await resolveAgentContext(baseCommon.agent, {
|
|
5925
|
-
agentRootDir,
|
|
5926
|
-
|
|
6019
|
+
agentRootDir: explicitAgentRootDir,
|
|
6020
|
+
credentialSource: cfg.credentialSource,
|
|
6021
|
+
envApiUrl: cfg.apiUrl
|
|
5927
6022
|
});
|
|
5928
6023
|
gate = "authenticate_and_bind";
|
|
5929
6024
|
const whoami = await validateStartupBinding({
|
|
@@ -5933,7 +6028,7 @@ async function runPolling(opts) {
|
|
|
5933
6028
|
});
|
|
5934
6029
|
gate = "resolve_signing_material";
|
|
5935
6030
|
const privateKey = await resolveExecutorSigningPrivateKey({
|
|
5936
|
-
|
|
6031
|
+
credentialSource: cfg.credentialSource,
|
|
5937
6032
|
agentDir: resolvedContext.agentDir,
|
|
5938
6033
|
configuredPrivateKey: cfg.signingPrivateKey,
|
|
5939
6034
|
configuredPrivateKeyRef: cfg.signingPrivateKeyRef
|
|
@@ -5949,7 +6044,7 @@ async function runPolling(opts) {
|
|
|
5949
6044
|
const agentIdentity = await resolveDaemonAgentIdentity({
|
|
5950
6045
|
agentName: baseCommon.agent,
|
|
5951
6046
|
whoami,
|
|
5952
|
-
|
|
6047
|
+
credentialSource: cfg.credentialSource,
|
|
5953
6048
|
agentDir: resolvedContext.agentDir,
|
|
5954
6049
|
gitAuthor: values["git-author"] ?? (cfg.gitAuthor || void 0)
|
|
5955
6050
|
});
|
|
@@ -5970,7 +6065,7 @@ async function runPolling(opts) {
|
|
|
5970
6065
|
level: cfg.logLevel || (baseCommon.debug ? "debug" : "info"),
|
|
5971
6066
|
gate,
|
|
5972
6067
|
agent: baseCommon.agent,
|
|
5973
|
-
|
|
6068
|
+
credentialSource: cfg.credentialSource,
|
|
5974
6069
|
error
|
|
5975
6070
|
});
|
|
5976
6071
|
throw error;
|
|
@@ -6074,7 +6169,7 @@ async function runPolling(opts) {
|
|
|
6074
6169
|
resourceAttributes: {
|
|
6075
6170
|
"moltnet.team.id": teamId,
|
|
6076
6171
|
"moltnet.agent.name": baseCommon.agent,
|
|
6077
|
-
"moltnet.
|
|
6172
|
+
"moltnet.credential.source": ctx.credentialSource,
|
|
6078
6173
|
"moltnet.runtime_profile.count": String(profiles.length),
|
|
6079
6174
|
"moltnet.runtime_profile.ids": profiles.map((p) => p.id).join(",")
|
|
6080
6175
|
}
|
|
@@ -6122,7 +6217,7 @@ async function runPolling(opts) {
|
|
|
6122
6217
|
}
|
|
6123
6218
|
});
|
|
6124
6219
|
rootLogger.info({
|
|
6125
|
-
|
|
6220
|
+
credentialSource: cfg.credentialSource,
|
|
6126
6221
|
subjectType: startupWhoami.subjectType,
|
|
6127
6222
|
bindingScope: startupWhoami.credentialBinding?.bindingScope ?? null,
|
|
6128
6223
|
credentialKeyId: startupWhoami.credentialBinding?.keyId ?? null,
|
|
@@ -6574,13 +6669,14 @@ async function runOnce(argv, runtimeAdapter = defaultPiDaemonAdapter) {
|
|
|
6574
6669
|
};
|
|
6575
6670
|
const runtimeCredentialConfig = resolveCredentialEnforcement(cfg.credentialEnforcement, credentialSources) === "off" ? null : loadRuntimeCredentialConfig(credentialSources);
|
|
6576
6671
|
const initialOpts = opts;
|
|
6577
|
-
const
|
|
6672
|
+
const explicitAgentRootDir = values["agent-root"] ? resolve(process.cwd(), values["agent-root"]) : void 0;
|
|
6578
6673
|
const { ctx, signingPrivateKey, agentIdentity, hostCapabilitySigner } = await (async () => {
|
|
6579
6674
|
let gate = "resolve_agent_context";
|
|
6580
6675
|
try {
|
|
6581
6676
|
const resolvedContext = await resolveAgentContext(initialOpts.agent, {
|
|
6582
|
-
agentRootDir,
|
|
6583
|
-
|
|
6677
|
+
agentRootDir: explicitAgentRootDir,
|
|
6678
|
+
credentialSource: cfg.credentialSource,
|
|
6679
|
+
envApiUrl: cfg.apiUrl
|
|
6584
6680
|
});
|
|
6585
6681
|
gate = "authenticate_and_bind";
|
|
6586
6682
|
const whoami = await validateStartupBinding({
|
|
@@ -6590,7 +6686,7 @@ async function runOnce(argv, runtimeAdapter = defaultPiDaemonAdapter) {
|
|
|
6590
6686
|
});
|
|
6591
6687
|
gate = "resolve_signing_material";
|
|
6592
6688
|
const privateKey = await resolveExecutorSigningPrivateKey({
|
|
6593
|
-
|
|
6689
|
+
credentialSource: cfg.credentialSource,
|
|
6594
6690
|
agentDir: resolvedContext.agentDir,
|
|
6595
6691
|
configuredPrivateKey: cfg.signingPrivateKey,
|
|
6596
6692
|
configuredPrivateKeyRef: cfg.signingPrivateKeyRef
|
|
@@ -6606,7 +6702,7 @@ async function runOnce(argv, runtimeAdapter = defaultPiDaemonAdapter) {
|
|
|
6606
6702
|
const agentIdentity = await resolveDaemonAgentIdentity({
|
|
6607
6703
|
agentName: initialOpts.agent,
|
|
6608
6704
|
whoami,
|
|
6609
|
-
|
|
6705
|
+
credentialSource: cfg.credentialSource,
|
|
6610
6706
|
agentDir: resolvedContext.agentDir,
|
|
6611
6707
|
gitAuthor: values["git-author"] ?? (cfg.gitAuthor || void 0)
|
|
6612
6708
|
});
|
|
@@ -6626,7 +6722,7 @@ async function runOnce(argv, runtimeAdapter = defaultPiDaemonAdapter) {
|
|
|
6626
6722
|
level: cfg.logLevel || (initialOpts.debug ? "debug" : "info"),
|
|
6627
6723
|
gate,
|
|
6628
6724
|
agent: initialOpts.agent,
|
|
6629
|
-
|
|
6725
|
+
credentialSource: cfg.credentialSource,
|
|
6630
6726
|
error
|
|
6631
6727
|
});
|
|
6632
6728
|
throw error;
|
|
@@ -6696,7 +6792,7 @@ async function runOnce(argv, runtimeAdapter = defaultPiDaemonAdapter) {
|
|
|
6696
6792
|
resourceAttributes: {
|
|
6697
6793
|
"moltnet.task.id": taskId,
|
|
6698
6794
|
"moltnet.agent.name": opts.agent,
|
|
6699
|
-
"moltnet.
|
|
6795
|
+
"moltnet.credential.source": ctx.credentialSource,
|
|
6700
6796
|
"moltnet.llm.provider": profile.provider,
|
|
6701
6797
|
"moltnet.llm.model": profile.model,
|
|
6702
6798
|
...profile.thinkingLevel ? { "moltnet.llm.thinking_level": profile.thinkingLevel } : {},
|
|
@@ -7745,11 +7841,14 @@ function snapshot(login) {
|
|
|
7745
7841
|
...waitingForAuthorization ? { waitingForAuthorization } : {}
|
|
7746
7842
|
};
|
|
7747
7843
|
}
|
|
7748
|
-
var NAME_RE =
|
|
7844
|
+
var NAME_RE = IDENTITY_ALIAS_PATTERN;
|
|
7749
7845
|
var PROVIDER_ID_RE = /^[a-z0-9][a-z0-9-]{0,63}$/;
|
|
7750
7846
|
function assertStoreName(kind, value) {
|
|
7751
|
-
|
|
7752
|
-
|
|
7847
|
+
try {
|
|
7848
|
+
return assertIdentityAlias(value);
|
|
7849
|
+
} catch {
|
|
7850
|
+
throw new AgentServerStoreError("invalid_name", `${kind} must match ${NAME_RE.source}`);
|
|
7851
|
+
}
|
|
7753
7852
|
}
|
|
7754
7853
|
function assertProviderId(value) {
|
|
7755
7854
|
if (!PROVIDER_ID_RE.test(value)) throw new AgentServerStoreError("invalid_name", `provider id must match ${PROVIDER_ID_RE.source}`);
|
|
@@ -7762,12 +7861,46 @@ var AgentServerStoreError = class extends Error {
|
|
|
7762
7861
|
this.code = code;
|
|
7763
7862
|
}
|
|
7764
7863
|
};
|
|
7765
|
-
/**
|
|
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
|
+
*/
|
|
7766
7876
|
function resolveAgentServerRoot(input) {
|
|
7767
7877
|
const override = input.root?.trim();
|
|
7768
7878
|
if (override) return override;
|
|
7769
|
-
|
|
7770
|
-
|
|
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;
|
|
7771
7904
|
}
|
|
7772
7905
|
function providerEnvName(providerId) {
|
|
7773
7906
|
return `MOLTNET_PROVIDER_${assertProviderId(providerId).replaceAll("-", "_").toUpperCase()}_API_KEY`;
|
|
@@ -7805,14 +7938,14 @@ function writeJsonAtomic(path, value) {
|
|
|
7805
7938
|
}
|
|
7806
7939
|
var AgentServerStore = class {
|
|
7807
7940
|
root;
|
|
7808
|
-
|
|
7941
|
+
identitiesDir;
|
|
7809
7942
|
runsDir;
|
|
7810
7943
|
secretsDir;
|
|
7811
7944
|
/** Shared Pi credential dir; `auth.json` inside is pi-managed (lockfiled). */
|
|
7812
7945
|
piDir;
|
|
7813
7946
|
constructor(root) {
|
|
7814
7947
|
this.root = root;
|
|
7815
|
-
this.
|
|
7948
|
+
this.identitiesDir = join(root, "identities");
|
|
7816
7949
|
this.runsDir = join(root, "runs");
|
|
7817
7950
|
this.secretsDir = join(root, "secrets");
|
|
7818
7951
|
this.piDir = join(root, "pi");
|
|
@@ -7821,18 +7954,85 @@ var AgentServerStore = class {
|
|
|
7821
7954
|
return join(this.piDir, "auth.json");
|
|
7822
7955
|
}
|
|
7823
7956
|
/** Create the directory layout (0700) if missing. Idempotent. */
|
|
7824
|
-
ensure() {
|
|
7957
|
+
ensure(options = {}) {
|
|
7958
|
+
this.adoptLegacyXdgRoot(options.legacyXdgConfigHome ?? "");
|
|
7825
7959
|
for (const dir of [
|
|
7826
7960
|
this.root,
|
|
7827
|
-
this.
|
|
7961
|
+
this.identitiesDir,
|
|
7828
7962
|
this.runsDir,
|
|
7829
7963
|
this.secretsDir
|
|
7830
7964
|
]) mkdirSync(dir, {
|
|
7831
7965
|
recursive: true,
|
|
7832
7966
|
mode: 448
|
|
7833
7967
|
});
|
|
7968
|
+
this.migrateLegacyAgentDocuments();
|
|
7834
7969
|
return this;
|
|
7835
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
|
+
}
|
|
7836
8036
|
get statePath() {
|
|
7837
8037
|
return join(this.root, "agent-server.json");
|
|
7838
8038
|
}
|
|
@@ -7861,16 +8061,58 @@ var AgentServerStore = class {
|
|
|
7861
8061
|
writeJsonAtomic(this.statePath, state);
|
|
7862
8062
|
}
|
|
7863
8063
|
agentPath(name) {
|
|
7864
|
-
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);
|
|
7865
8090
|
}
|
|
7866
8091
|
readAgentConfig(alias) {
|
|
7867
8092
|
return readJson(this.agentPath(alias));
|
|
7868
8093
|
}
|
|
7869
8094
|
writeAgentConfig(alias, config) {
|
|
8095
|
+
mkdirSync(this.identityDir(alias), {
|
|
8096
|
+
recursive: true,
|
|
8097
|
+
mode: 448
|
|
8098
|
+
});
|
|
7870
8099
|
writeJsonAtomic(this.agentPath(alias), config);
|
|
8100
|
+
if (!this.readIdentitySelector()?.default_identity) this.writeIdentitySelector(alias);
|
|
7871
8101
|
}
|
|
7872
8102
|
removeAgentConfig(alias) {
|
|
7873
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 });
|
|
7874
8116
|
}
|
|
7875
8117
|
readActivation(alias) {
|
|
7876
8118
|
return this.readAgentServerState().activations[assertStoreName("agent name", alias)] ?? null;
|
|
@@ -10380,12 +10622,9 @@ async function runAgentServer(argv) {
|
|
|
10380
10622
|
return 1;
|
|
10381
10623
|
}
|
|
10382
10624
|
const allowedOrigins = parseAllowedOrigins(values["allowed-origins"] ?? (envConfig.allowedOrigins || DEFAULT_ALLOWED_ORIGINS));
|
|
10383
|
-
const root = values.root ?? resolveAgentServerRoot({
|
|
10384
|
-
root: envConfig.root,
|
|
10385
|
-
xdgConfigHome: envConfig.xdgConfigHome
|
|
10386
|
-
});
|
|
10625
|
+
const root = values.root ?? resolveAgentServerRoot({ root: envConfig.root });
|
|
10387
10626
|
const defaultApiUrl = values["api-url"] ?? (envConfig.apiUrl || DEFAULT_API_URL);
|
|
10388
|
-
const store = new AgentServerStore(root).ensure();
|
|
10627
|
+
const store = new AgentServerStore(root).ensure({ legacyXdgConfigHome: envConfig.xdgConfigHome });
|
|
10389
10628
|
if (trustRequested) return runTrustCommand(commandArgs, root);
|
|
10390
10629
|
const { logger, shutdown: shutdownLogger } = createRootLogger({
|
|
10391
10630
|
name: "agent-daemon.server",
|
|
@@ -10679,7 +10918,13 @@ async function runSyncSessions(argv) {
|
|
|
10679
10918
|
const state = parseState(values.state);
|
|
10680
10919
|
const limit = parseLimit(values.limit);
|
|
10681
10920
|
const agentRootDir = resolve(process.cwd(), values["agent-root"] ?? process.cwd());
|
|
10682
|
-
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
|
+
});
|
|
10683
10928
|
await validateStartupBinding({
|
|
10684
10929
|
agent: ctx.agent,
|
|
10685
10930
|
teamId: values.team
|
|
@@ -10714,18 +10959,51 @@ function parseLimit(raw) {
|
|
|
10714
10959
|
}
|
|
10715
10960
|
//#endregion
|
|
10716
10961
|
//#region src/lib/update.ts
|
|
10717
|
-
|
|
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";
|
|
10718
10981
|
var UPDATE_CACHE_TTL_MS = 1440 * 60 * 1e3;
|
|
10719
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
|
+
}
|
|
10720
10998
|
function detectDaemonInstallMethod(executable = process.argv[1] ?? "") {
|
|
10721
|
-
const path = executable.replaceAll("\\", "/");
|
|
10999
|
+
const path = resolveDaemonExecutable(executable).replaceAll("\\", "/");
|
|
10722
11000
|
if (path.includes("/node_modules/@themoltnet/agent-daemon/")) return "npm";
|
|
10723
11001
|
if (path.includes("/.local/share/moltnet/") || path.includes("/opt/moltnet/")) return "bundle";
|
|
10724
11002
|
return "direct";
|
|
10725
11003
|
}
|
|
10726
11004
|
function daemonUpdateCommand(method) {
|
|
10727
11005
|
if (method === "npm") return "npm install -g @themoltnet/agent-daemon@latest";
|
|
10728
|
-
return "curl -fsSL https://themolt.net/install/agent | sh";
|
|
11006
|
+
return "curl -fsSL https://themolt.net/install/agent | MOLTNET_AGENT_VERSION=latest sh";
|
|
10729
11007
|
}
|
|
10730
11008
|
async function checkDaemonUpdate(input) {
|
|
10731
11009
|
const installMethod = detectDaemonInstallMethod(input.executable ?? process.argv[1] ?? "moltnet-agent");
|
|
@@ -10746,11 +11024,15 @@ async function checkDaemonUpdate(input) {
|
|
|
10746
11024
|
return result;
|
|
10747
11025
|
}
|
|
10748
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";
|
|
10749
11030
|
try {
|
|
10750
|
-
const response = await (input.fetchFn ?? fetch)(
|
|
10751
|
-
if (!response.ok) throw new Error(
|
|
10752
|
-
const
|
|
10753
|
-
|
|
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`);
|
|
10754
11036
|
await writeCache({
|
|
10755
11037
|
checkedAt: now.toISOString(),
|
|
10756
11038
|
latest
|
|
@@ -10766,13 +11048,31 @@ async function checkDaemonUpdate(input) {
|
|
|
10766
11048
|
throw new Error(`could not check for MoltNet agent updates: ${error instanceof Error ? error.message : String(error)}`);
|
|
10767
11049
|
}
|
|
10768
11050
|
}
|
|
10769
|
-
function
|
|
11051
|
+
function distTagVersion(value) {
|
|
10770
11052
|
if (!value || typeof value !== "object") return void 0;
|
|
10771
|
-
const
|
|
10772
|
-
if (!agent || typeof agent !== "object") return void 0;
|
|
10773
|
-
const version = agent.version;
|
|
11053
|
+
const version = value.version;
|
|
10774
11054
|
return typeof version === "string" && validVersion(version) ? normalizeVersion(version) : void 0;
|
|
10775
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
|
+
}
|
|
10776
11076
|
function normalizeVersion(value) {
|
|
10777
11077
|
return value.trim().replace(/^v/, "");
|
|
10778
11078
|
}
|
|
@@ -10812,7 +11112,7 @@ async function writeCache(cache) {
|
|
|
10812
11112
|
}
|
|
10813
11113
|
//#endregion
|
|
10814
11114
|
//#region src/version.ts
|
|
10815
|
-
var DAEMON_VERSION = "0.
|
|
11115
|
+
var DAEMON_VERSION = "0.54.0";
|
|
10816
11116
|
//#endregion
|
|
10817
11117
|
//#region src/cli.ts
|
|
10818
11118
|
async function runAgentDaemonCli(options) {
|
|
@@ -10870,11 +11170,7 @@ async function runUpdate(argv) {
|
|
|
10870
11170
|
}
|
|
10871
11171
|
}
|
|
10872
11172
|
async function runRuntime(argv) {
|
|
10873
|
-
const
|
|
10874
|
-
const registry = new RuntimeRegistry(resolveAgentServerRoot({
|
|
10875
|
-
root: config.root,
|
|
10876
|
-
xdgConfigHome: config.xdgConfigHome
|
|
10877
|
-
}));
|
|
11173
|
+
const registry = new RuntimeRegistry(resolveAgentServerRoot({ root: loadAgentServerEnvConfig().root }));
|
|
10878
11174
|
const [command, ...rest] = argv;
|
|
10879
11175
|
if (command === "list") {
|
|
10880
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/pi-runtime": "0.14.
|
|
89
|
-
"@themoltnet/agent-runtime": "0.
|
|
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",
|
|
@@ -99,15 +99,15 @@
|
|
|
99
99
|
"vite-plugin-dts": "^4.5.4",
|
|
100
100
|
"vitest": "^3.0.0",
|
|
101
101
|
"@moltnet/bootstrap": "0.1.0",
|
|
102
|
-
"@moltnet/agent-eval": "0.1.0",
|
|
103
102
|
"@moltnet/crypto-service": "0.1.0",
|
|
104
|
-
"@moltnet/execution-integrations": "0.1.0",
|
|
105
103
|
"@moltnet/execution-plan": "0.1.0",
|
|
104
|
+
"@moltnet/execution-integrations": "0.1.0",
|
|
105
|
+
"@moltnet/runtime-profiles": "0.1.0",
|
|
106
|
+
"@moltnet/tasks": "0.1.0",
|
|
106
107
|
"@moltnet/loopback-companion": "0.1.0",
|
|
108
|
+
"@moltnet/agent-eval": "0.1.0",
|
|
107
109
|
"@moltnet/observability": "0.1.0",
|
|
108
|
-
"@moltnet/
|
|
109
|
-
"@moltnet/models": "0.1.0",
|
|
110
|
-
"@moltnet/tasks": "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
|
},
|