@themoltnet/agent-daemon 0.53.0 → 0.55.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 +399 -169
- package/package.json +17 -9
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 { execFile, execFileSync, spawn } from "node:child_process";
|
|
17
16
|
import { constants, createReadStream, createWriteStream, existsSync, lstatSync, mkdirSync, openSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
|
|
18
|
-
import { AuthenticationError, MoltNetError, agentKeyKey, assertTrustedConfigApiUrl, createExecutorAttestor, deriveMcpUrl, formatSecretReferenceString, identitySeedKey, parseSecretReferenceString, readConfig, register, requireSecureCredentialApiUrl, resolveAgentKey, resolveEnvSecretReference, resolveIdentitySeed, resolveOAuth2ClientSecret } from "@themoltnet/sdk";
|
|
17
|
+
import { AuthenticationError, IDENTITY_ALIAS_PATTERN, MoltNetError, agentKeyKey, assertIdentityAlias, assertTrustedConfigApiUrl, createExecutorAttestor, deriveMcpUrl, formatSecretReferenceString, getConfigDir, getIdentityDir, identitySeedKey, isCanonicalConfig, 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) {
|
|
@@ -1350,7 +1363,8 @@ Type.Object({ "x-moltnet-team-id": Type.Optional(Type.String({
|
|
|
1350
1363
|
})) });
|
|
1351
1364
|
Type.Object({
|
|
1352
1365
|
kind: Type.Literal("agent"),
|
|
1353
|
-
|
|
1366
|
+
agentId: UuidSchema,
|
|
1367
|
+
identityId: Type.Union([UuidSchema, Type.Null()]),
|
|
1354
1368
|
fingerprint: FingerprintSchema,
|
|
1355
1369
|
publicKey: PublicKeySchema
|
|
1356
1370
|
}, {
|
|
@@ -1367,7 +1381,8 @@ Type.Object({
|
|
|
1367
1381
|
});
|
|
1368
1382
|
var principalUnionVariants = [Type.Object({
|
|
1369
1383
|
kind: Type.Literal("agent"),
|
|
1370
|
-
|
|
1384
|
+
agentId: UuidSchema,
|
|
1385
|
+
identityId: Type.Union([UuidSchema, Type.Null()]),
|
|
1371
1386
|
fingerprint: FingerprintSchema,
|
|
1372
1387
|
publicKey: PublicKeySchema
|
|
1373
1388
|
}, { additionalProperties: false }), Type.Object({
|
|
@@ -3395,8 +3410,8 @@ var COMMON_OPTIONAL_FLAGS = `\
|
|
|
3395
3410
|
--git-author <"Name <email>">
|
|
3396
3411
|
Non-secret git identity projected into the
|
|
3397
3412
|
guest for host-brokered commit signing. Default:
|
|
3398
|
-
host git config
|
|
3399
|
-
|
|
3413
|
+
host git config. Configless agent-key runs must
|
|
3414
|
+
provide this flag or MOLTNET_GIT_AUTHOR.
|
|
3400
3415
|
Env: MOLTNET_GIT_AUTHOR.
|
|
3401
3416
|
--lease-ttl-sec <n> Sliding liveness window. Silence longer than
|
|
3402
3417
|
this ends the attempt with lease_expired.
|
|
@@ -3451,9 +3466,19 @@ Commands:
|
|
|
3451
3466
|
Run \`agent-daemon <command> --help\` for command-specific flags.
|
|
3452
3467
|
|
|
3453
3468
|
Prerequisites:
|
|
3454
|
-
-
|
|
3469
|
+
- configless: MOLTNET_AGENT_KEY (or MOLTNET_AGENT_KEY_REF) and
|
|
3455
3470
|
MOLTNET_PRIVATE_KEY (or MOLTNET_PRIVATE_KEY_REF); no agent files
|
|
3456
|
-
-
|
|
3471
|
+
- config-based and sync-sessions: <agent-root>/.moltnet/<agent>/moltnet.json
|
|
3472
|
+
carrying agent_key_ref (OAuth2 client credentials are not accepted)
|
|
3473
|
+
|
|
3474
|
+
No key yet? Mint one with the CLI (--store writes agent_key_ref into
|
|
3475
|
+
moltnet.json and keeps the secret in a provider):
|
|
3476
|
+
|
|
3477
|
+
moltnet teams list # find the team id
|
|
3478
|
+
moltnet agents keys create --team-id <team-uuid> \\
|
|
3479
|
+
--name <agent>-daemon --store
|
|
3480
|
+
|
|
3481
|
+
https://docs.themolt.net/operate/agent-keys#run-the-daemon-with-an-agent-key
|
|
3457
3482
|
- --profile — remote runtime profile supplies provider/model/sandbox
|
|
3458
3483
|
policy and CWD is used as the VM mountPath.
|
|
3459
3484
|
|
|
@@ -3603,8 +3628,17 @@ remove that exact CA. Linux continues to use the Chromium PNA HTTP path.
|
|
|
3603
3628
|
//#region src/lib/identity-pin.ts
|
|
3604
3629
|
/** Compare every pinned field without choosing a caller-specific error type. */
|
|
3605
3630
|
function assessIdentityPin(current, expected) {
|
|
3631
|
+
for (const [field, label] of [["publicKey", "public key"], ["fingerprint", "fingerprint"]]) if (!current[field] || current[field] !== expected[field]) return {
|
|
3632
|
+
ok: false,
|
|
3633
|
+
field,
|
|
3634
|
+
label
|
|
3635
|
+
};
|
|
3636
|
+
return { ok: true };
|
|
3637
|
+
}
|
|
3638
|
+
function assessAgentStartupPin(current, expected) {
|
|
3606
3639
|
for (const [field, label] of [
|
|
3607
|
-
["
|
|
3640
|
+
["subjectId", "subject id"],
|
|
3641
|
+
["subjectType", "subject type"],
|
|
3608
3642
|
["publicKey", "public key"],
|
|
3609
3643
|
["fingerprint", "fingerprint"]
|
|
3610
3644
|
]) if (!current[field] || current[field] !== expected[field]) return {
|
|
@@ -3617,18 +3651,30 @@ function assessIdentityPin(current, expected) {
|
|
|
3617
3651
|
//#endregion
|
|
3618
3652
|
//#region src/lib/agent-context.ts
|
|
3619
3653
|
/**
|
|
3620
|
-
*
|
|
3621
|
-
*
|
|
3622
|
-
*
|
|
3623
|
-
*
|
|
3624
|
-
|
|
3625
|
-
|
|
3626
|
-
|
|
3654
|
+
* Where an operator goes after the daemon refuses to start. The published site
|
|
3655
|
+
* rather than a GitHub blob: it renders, and it tracks the deployed docs
|
|
3656
|
+
* instead of whatever `main` happens to say. Sidebar entry lives in
|
|
3657
|
+
* `docs/.vitepress/config.ts`.
|
|
3658
|
+
*/
|
|
3659
|
+
var AGENT_KEYS_DOC_URL = "https://docs.themolt.net/operate/agent-keys#run-the-daemon-with-an-agent-key";
|
|
3660
|
+
/** Scopes a daemon key must carry; mirrors `DAEMON_REQUIRED_SCOPES`. */
|
|
3661
|
+
var DAEMON_KEY_SCOPES = AGENT_CREDENTIAL_SCOPES;
|
|
3662
|
+
/**
|
|
3663
|
+
* Report where `connect()` will find the key, without ever reading the secret
|
|
3664
|
+
* value into anything logged. A non-blank `MOLTNET_AGENT_KEY` or
|
|
3665
|
+
* `MOLTNET_AGENT_KEY_REF` means configless; otherwise the key comes from
|
|
3666
|
+
* `moltnet.json`. This mirrors the SDK precedence, where an environment key
|
|
3667
|
+
* wins over the config file.
|
|
3668
|
+
*
|
|
3669
|
+
* It reports the *source*, not what `connect()` would pick on its own: the
|
|
3670
|
+
* config path resolves the key and passes it explicitly, because ambient
|
|
3671
|
+
* resolution ranks environment OAuth2 credentials above a configured
|
|
3672
|
+
* `agent_key_ref` (see `resolveAgentContext`).
|
|
3627
3673
|
*
|
|
3628
3674
|
* Pure: `env` is passed in (the config module owns the `process.env` read).
|
|
3629
3675
|
*/
|
|
3630
|
-
function
|
|
3631
|
-
return env.MOLTNET_AGENT_KEY?.trim() || env.MOLTNET_AGENT_KEY_REF?.trim() ? "
|
|
3676
|
+
function detectCredentialSource(env) {
|
|
3677
|
+
return env.MOLTNET_AGENT_KEY?.trim() || env.MOLTNET_AGENT_KEY_REF?.trim() ? "environment" : "config";
|
|
3632
3678
|
}
|
|
3633
3679
|
/**
|
|
3634
3680
|
* Pure check: may the identity described by `whoami` operate the daemon as
|
|
@@ -3677,47 +3723,118 @@ async function validateStartupBinding(options) {
|
|
|
3677
3723
|
}
|
|
3678
3724
|
const assessment = assessStartupBinding(whoami, options.teamId);
|
|
3679
3725
|
if (!assessment.ok) throw new Error(`Daemon startup validation failed: ${assessment.reason}`);
|
|
3680
|
-
const expected = options.
|
|
3681
|
-
if (expected && !
|
|
3726
|
+
const expected = options.expectedAgent;
|
|
3727
|
+
if (expected && !assessAgentStartupPin(whoami, expected).ok) throw new Error("Daemon startup validation failed: authenticated agent does not match the Agent Server activation.");
|
|
3682
3728
|
return whoami;
|
|
3683
3729
|
}
|
|
3684
3730
|
/**
|
|
3685
3731
|
* Resolve the agent's MoltNet credentials directory and connect via SDK.
|
|
3686
3732
|
*
|
|
3687
|
-
*
|
|
3688
|
-
*
|
|
3689
|
-
* the daemon never falls back to unauthenticated calls.
|
|
3733
|
+
* The daemon selects the same central identity directory as the CLI. It never
|
|
3734
|
+
* inspects a repository, Git state, or legacy agent bundle for credentials.
|
|
3690
3735
|
*/
|
|
3691
3736
|
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
|
-
|
|
3737
|
+
assertIdentityAlias(agentName);
|
|
3738
|
+
const { agentDir, agentRootDir } = resolveIdentityLocation(agentName, options.agentRootDir, { requireConfig: options.credentialSource !== "environment" });
|
|
3739
|
+
if (options.credentialSource === "environment") return {
|
|
3740
|
+
agentDir,
|
|
3741
|
+
agentRootDir,
|
|
3742
|
+
agent: await connect({ secretProviders: createNodeSecretProviderRegistry() }),
|
|
3743
|
+
credentialSource: "environment"
|
|
3744
|
+
};
|
|
3745
|
+
const config = await readConfig(agentDir);
|
|
3746
|
+
if (!config?.agent_key_ref) throw new Error(agentKeyRequiredMessage(agentDir, agentName));
|
|
3747
|
+
const secretProviders = createNodeSecretProviderRegistry();
|
|
3748
|
+
const agentKey = await resolveAgentKey(config, secretProviders);
|
|
3749
|
+
if (!agentKey) throw new Error(agentKeyRequiredMessage(agentDir, agentName));
|
|
3750
|
+
return {
|
|
3751
|
+
agentDir,
|
|
3752
|
+
agentRootDir,
|
|
3753
|
+
agent: await connect({
|
|
3754
|
+
configDir: agentDir,
|
|
3755
|
+
secretProviders,
|
|
3756
|
+
agentKey,
|
|
3757
|
+
apiUrl: resolveConfigApiUrl(config, options.envApiUrl)
|
|
3758
|
+
}),
|
|
3759
|
+
credentialSource: "config"
|
|
3760
|
+
};
|
|
3761
|
+
}
|
|
3762
|
+
/**
|
|
3763
|
+
* The daemon runs on an agent key only. A `moltnet.json` carrying OAuth2
|
|
3764
|
+
* client credentials but no `agent_key_ref` is the pre-#2160 shape, and
|
|
3765
|
+
* `connect()` would happily authenticate it — so this has to be refused here
|
|
3766
|
+
* rather than left to surface as an over-scoped token later.
|
|
3767
|
+
*/
|
|
3768
|
+
function agentKeyRequiredMessage(agentDir, agentName) {
|
|
3769
|
+
return [
|
|
3770
|
+
`${join(agentDir, "moltnet.json")} has no "agent_key_ref". The daemon`,
|
|
3771
|
+
`requires a team-bound agent key; OAuth2 client_credentials is no longer`,
|
|
3772
|
+
`accepted.`,
|
|
3773
|
+
``,
|
|
3774
|
+
`Mint one (--agent-id defaults to the agent you authenticate as):`,
|
|
3775
|
+
``,
|
|
3776
|
+
` moltnet teams list # find the team id`,
|
|
3777
|
+
` moltnet agents keys create \\`,
|
|
3778
|
+
` --team-id <team-uuid> \\`,
|
|
3779
|
+
` --name ${agentName}-daemon \\`,
|
|
3780
|
+
` --scopes ${DAEMON_KEY_SCOPES.join(",")} \\`,
|
|
3781
|
+
` --store`,
|
|
3782
|
+
``,
|
|
3783
|
+
`--store writes "agent_key_ref" into moltnet.json and keeps the secret in`,
|
|
3784
|
+
`a provider, so the key itself never lands in the file. Omit --scopes to`,
|
|
3785
|
+
`get the same daemon minimum by default.`,
|
|
3786
|
+
``,
|
|
3787
|
+
`To run configless instead, set MOLTNET_AGENT_KEY (or`,
|
|
3788
|
+
`MOLTNET_AGENT_KEY_REF) and skip the config entirely.`,
|
|
3789
|
+
``,
|
|
3790
|
+
`Full guide: ${AGENT_KEYS_DOC_URL}`
|
|
3791
|
+
].join("\n");
|
|
3792
|
+
}
|
|
3793
|
+
/**
|
|
3794
|
+
* The central identity directory, unless an explicit `--agent-root` names a
|
|
3795
|
+
* legacy bundle that actually exists.
|
|
3796
|
+
*
|
|
3797
|
+
* The flag is documented as "Directory that owns .moltnet/<agent>" and is still
|
|
3798
|
+
* accepted by `once`, `poll` and `sync-sessions`. Ignoring it silently sent
|
|
3799
|
+
* every caller that passes one — sandboxed runs, the e2e harness — to a
|
|
3800
|
+
* central store they never populated, and failed with a bare "No credentials
|
|
3801
|
+
* found".
|
|
3802
|
+
*/
|
|
3803
|
+
/**
|
|
3804
|
+
* `agentDir` is where credentials live; `agentRootDir` is the directory that
|
|
3805
|
+
* OWNS it and is mounted into the sandbox. They differ for a legacy bundle
|
|
3806
|
+
* (`<root>/.moltnet/<agent>` inside `<root>`) and coincide for a central
|
|
3807
|
+
* identity, which owns nothing above itself. Collapsing them mounted the
|
|
3808
|
+
* credentials directory itself into the guest.
|
|
3809
|
+
*/
|
|
3810
|
+
function resolveIdentityLocation(agentName, explicitRootDir, { requireConfig }) {
|
|
3811
|
+
const root = explicitRootDir?.trim();
|
|
3812
|
+
if (root) {
|
|
3813
|
+
const bundle = join(root, ".moltnet", agentName);
|
|
3814
|
+
if (!requireConfig || existsSync(join(bundle, "moltnet.json"))) return {
|
|
3815
|
+
agentDir: bundle,
|
|
3816
|
+
agentRootDir: root
|
|
3717
3817
|
};
|
|
3718
3818
|
}
|
|
3719
|
-
const
|
|
3720
|
-
|
|
3819
|
+
const central = getIdentityDir(agentName);
|
|
3820
|
+
return {
|
|
3821
|
+
agentDir: central,
|
|
3822
|
+
agentRootDir: central
|
|
3823
|
+
};
|
|
3824
|
+
}
|
|
3825
|
+
/**
|
|
3826
|
+
* Pick the API URL for an explicitly-keyed connect, preserving the checks
|
|
3827
|
+
* ambient config resolution would have applied. An explicit `apiUrl` skips
|
|
3828
|
+
* ambient's own normalisation, so a URL taken from `moltnet.json` still has to
|
|
3829
|
+
* clear the config-trust and transport checks before it is used.
|
|
3830
|
+
*/
|
|
3831
|
+
function resolveConfigApiUrl(config, envApiUrl) {
|
|
3832
|
+
if (envApiUrl?.trim()) return void 0;
|
|
3833
|
+
const fromConfig = config.endpoints?.api?.trim();
|
|
3834
|
+
if (!fromConfig) return void 0;
|
|
3835
|
+
assertTrustedConfigApiUrl(fromConfig);
|
|
3836
|
+
requireSecureCredentialApiUrl(fromConfig);
|
|
3837
|
+
return fromConfig;
|
|
3721
3838
|
}
|
|
3722
3839
|
function isTransientWhoamiError(error) {
|
|
3723
3840
|
if (error instanceof TypeError) return true;
|
|
@@ -3725,26 +3842,6 @@ function isTransientWhoamiError(error) {
|
|
|
3725
3842
|
const statusCode = error.statusCode;
|
|
3726
3843
|
return typeof statusCode === "number" && (statusCode === 408 || statusCode === 429 || statusCode >= 500);
|
|
3727
3844
|
}
|
|
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
3845
|
//#endregion
|
|
3749
3846
|
//#region src/config.ts
|
|
3750
3847
|
/**
|
|
@@ -3757,14 +3854,15 @@ function resolveCredentialRoots(agentRootDir) {
|
|
|
3757
3854
|
function loadConfig() {
|
|
3758
3855
|
assertSingleCredentialForm("MOLTNET_AGENT_KEY", "MOLTNET_AGENT_KEY_REF");
|
|
3759
3856
|
assertSingleCredentialForm("MOLTNET_PRIVATE_KEY", "MOLTNET_PRIVATE_KEY_REF");
|
|
3760
|
-
const
|
|
3857
|
+
const expectedAgent = readExpectedAgent();
|
|
3761
3858
|
return {
|
|
3762
3859
|
otelEndpoint: process.env["MOLTNET_OTEL_ENDPOINT"] ?? "",
|
|
3763
3860
|
logLevel: process.env["LOG_LEVEL"] ?? "",
|
|
3764
3861
|
profilePrerequisiteEnv: process.env,
|
|
3765
3862
|
profilePrerequisitePath: process.env.PATH ?? "",
|
|
3766
3863
|
piCodingAgentDir: process.env["PI_CODING_AGENT_DIR"] ?? "",
|
|
3767
|
-
|
|
3864
|
+
credentialSource: detectCredentialSource(process.env),
|
|
3865
|
+
apiUrl: process.env["MOLTNET_API_URL"] ?? "",
|
|
3768
3866
|
signingPrivateKey: process.env["MOLTNET_PRIVATE_KEY"] ?? "",
|
|
3769
3867
|
signingPrivateKeyRef: process.env["MOLTNET_PRIVATE_KEY_REF"] ?? "",
|
|
3770
3868
|
gitAuthor: process.env["MOLTNET_GIT_AUTHOR"] ?? "",
|
|
@@ -3772,22 +3870,27 @@ function loadConfig() {
|
|
|
3772
3870
|
credentialBindings: process.env["MOLTNET_CREDENTIAL_BINDINGS"] ?? "",
|
|
3773
3871
|
credentialEnforcement: process.env["MOLTNET_CREDENTIAL_ENFORCEMENT"] ?? "",
|
|
3774
3872
|
traceIdlePolling: readBoolean("MOLTNET_TRACE_IDLE_POLLING", process.env["MOLTNET_TRACE_IDLE_POLLING"]),
|
|
3775
|
-
...
|
|
3873
|
+
...expectedAgent ? { expectedAgent } : {}
|
|
3776
3874
|
};
|
|
3777
3875
|
}
|
|
3778
|
-
function
|
|
3779
|
-
|
|
3876
|
+
function readExpectedAgent() {
|
|
3877
|
+
if (process.env["MOLTNET_EXPECTED_IDENTITY_ID"]?.trim()) throw new Error("MOLTNET_EXPECTED_IDENTITY_ID is no longer supported; set the complete MOLTNET_EXPECTED_SUBJECT_ID, MOLTNET_EXPECTED_SUBJECT_TYPE=agent, MOLTNET_EXPECTED_PUBLIC_KEY, and MOLTNET_EXPECTED_FINGERPRINT pin");
|
|
3878
|
+
const subjectId = process.env["MOLTNET_EXPECTED_SUBJECT_ID"]?.trim() ?? "";
|
|
3879
|
+
const subjectType = process.env["MOLTNET_EXPECTED_SUBJECT_TYPE"]?.trim() ?? "";
|
|
3780
3880
|
const publicKey = process.env["MOLTNET_EXPECTED_PUBLIC_KEY"]?.trim() ?? "";
|
|
3781
3881
|
const fingerprint = process.env["MOLTNET_EXPECTED_FINGERPRINT"]?.trim() ?? "";
|
|
3782
3882
|
const present = [
|
|
3783
|
-
|
|
3883
|
+
subjectId,
|
|
3884
|
+
subjectType,
|
|
3784
3885
|
publicKey,
|
|
3785
3886
|
fingerprint
|
|
3786
3887
|
].filter(Boolean).length;
|
|
3787
3888
|
if (present === 0) return void 0;
|
|
3788
|
-
if (present !==
|
|
3889
|
+
if (present !== 4) throw new Error("MOLTNET_EXPECTED_SUBJECT_ID, MOLTNET_EXPECTED_SUBJECT_TYPE, MOLTNET_EXPECTED_PUBLIC_KEY, and MOLTNET_EXPECTED_FINGERPRINT must be set together");
|
|
3890
|
+
if (subjectType !== "agent") throw new Error("MOLTNET_EXPECTED_SUBJECT_TYPE must be agent");
|
|
3789
3891
|
return {
|
|
3790
|
-
|
|
3892
|
+
subjectId,
|
|
3893
|
+
subjectType,
|
|
3791
3894
|
publicKey,
|
|
3792
3895
|
fingerprint
|
|
3793
3896
|
};
|
|
@@ -3809,7 +3912,6 @@ function loadAgentServerEnvConfig() {
|
|
|
3809
3912
|
port: process.env["MOLTNET_AGENT_SERVER_PORT"] ?? "",
|
|
3810
3913
|
allowedOrigins: process.env["MOLTNET_AGENT_SERVER_ALLOWED_ORIGINS"] ?? "",
|
|
3811
3914
|
root: process.env["MOLTNET_AGENT_SERVER_ROOT"] ?? "",
|
|
3812
|
-
xdgConfigHome: process.env["XDG_CONFIG_HOME"] ?? "",
|
|
3813
3915
|
apiUrl: process.env["MOLTNET_API_URL"] ?? "",
|
|
3814
3916
|
logLevel: process.env["LOG_LEVEL"] ?? ""
|
|
3815
3917
|
};
|
|
@@ -3839,13 +3941,13 @@ async function abortActiveAttemptOnSignal(opts) {
|
|
|
3839
3941
|
//#region src/lib/agent-identity.ts
|
|
3840
3942
|
/**
|
|
3841
3943
|
* 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
|
-
*
|
|
3944
|
+
* non-secret input and is consulted only on config-based hosts, which already
|
|
3945
|
+
* read that configuration for the signing seed; configless hosts never touch a
|
|
3946
|
+
* config directory.
|
|
3845
3947
|
*/
|
|
3846
3948
|
async function resolveDaemonAgentIdentity(input) {
|
|
3847
3949
|
let hostGit;
|
|
3848
|
-
if (input.gitAuthor === void 0 && input.
|
|
3950
|
+
if (input.gitAuthor === void 0 && input.credentialSource === "config") {
|
|
3849
3951
|
const config = await readConfig(input.agentDir);
|
|
3850
3952
|
hostGit = config?.git ? {
|
|
3851
3953
|
name: config.git.name,
|
|
@@ -4390,26 +4492,26 @@ function recoverScratchWorkspacePath(producer, stateDirs) {
|
|
|
4390
4492
|
//#region src/lib/executor-attestation.ts
|
|
4391
4493
|
var DAEMON_REQUIRED_SCOPES = AGENT_CREDENTIAL_SCOPES;
|
|
4392
4494
|
async function resolveExecutorSigningPrivateKey(input) {
|
|
4393
|
-
if (input.
|
|
4495
|
+
if (input.credentialSource === "environment") {
|
|
4394
4496
|
const privateKey = input.configuredPrivateKey.trim();
|
|
4395
4497
|
if (privateKey) return privateKey;
|
|
4396
4498
|
const reference = input.configuredPrivateKeyRef?.trim();
|
|
4397
|
-
if (!reference) throw new Error("
|
|
4499
|
+
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
4500
|
let resolved;
|
|
4399
4501
|
try {
|
|
4400
4502
|
resolved = await resolveEnvSecretReference(reference, createNodeSecretProviderRegistry());
|
|
4401
4503
|
} catch (cause) {
|
|
4402
|
-
throw new Error(`
|
|
4504
|
+
throw new Error(`Configless daemon startup could not resolve MOLTNET_PRIVATE_KEY_REF: ${cause.message}`, { cause });
|
|
4403
4505
|
}
|
|
4404
4506
|
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
4507
|
return resolved;
|
|
4406
4508
|
}
|
|
4407
4509
|
const config = await readConfig(input.agentDir);
|
|
4408
|
-
if (!config) throw new Error(`
|
|
4510
|
+
if (!config) throw new Error(`Config-based daemon startup requires ${input.agentDir}/moltnet.json.`);
|
|
4409
4511
|
try {
|
|
4410
4512
|
return await resolveIdentitySeed(config, createNodeSecretProviderRegistry());
|
|
4411
4513
|
} catch (cause) {
|
|
4412
|
-
throw new Error(`
|
|
4514
|
+
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
4515
|
}
|
|
4414
4516
|
}
|
|
4415
4517
|
function validateDaemonScopes(whoami) {
|
|
@@ -5105,7 +5207,7 @@ async function logDaemonStartupFailure(input) {
|
|
|
5105
5207
|
event: "agent-daemon.startup_failed",
|
|
5106
5208
|
gate: input.gate,
|
|
5107
5209
|
agent: input.agent,
|
|
5108
|
-
|
|
5210
|
+
credentialSource: input.credentialSource
|
|
5109
5211
|
}, "Agent daemon startup validation failed");
|
|
5110
5212
|
await shutdown();
|
|
5111
5213
|
}
|
|
@@ -5924,23 +6026,24 @@ async function runPolling(opts) {
|
|
|
5924
6026
|
bindings: cfg.credentialBindings
|
|
5925
6027
|
};
|
|
5926
6028
|
const runtimeCredentialConfig = resolveCredentialEnforcement(cfg.credentialEnforcement, credentialSources) === "off" ? null : loadRuntimeCredentialConfig(credentialSources);
|
|
5927
|
-
const
|
|
6029
|
+
const explicitAgentRootDir = values["agent-root"] ? resolve(process.cwd(), values["agent-root"]) : void 0;
|
|
5928
6030
|
const { ctx, signingPrivateKey, startupWhoami, agentIdentity, hostCapabilitySigner } = await (async () => {
|
|
5929
6031
|
let gate = "resolve_agent_context";
|
|
5930
6032
|
try {
|
|
5931
6033
|
const resolvedContext = await resolveAgentContext(baseCommon.agent, {
|
|
5932
|
-
agentRootDir,
|
|
5933
|
-
|
|
6034
|
+
agentRootDir: explicitAgentRootDir,
|
|
6035
|
+
credentialSource: cfg.credentialSource,
|
|
6036
|
+
envApiUrl: cfg.apiUrl
|
|
5934
6037
|
});
|
|
5935
6038
|
gate = "authenticate_and_bind";
|
|
5936
6039
|
const whoami = await validateStartupBinding({
|
|
5937
6040
|
agent: resolvedContext.agent,
|
|
5938
6041
|
teamId,
|
|
5939
|
-
|
|
6042
|
+
expectedAgent: cfg.expectedAgent
|
|
5940
6043
|
});
|
|
5941
6044
|
gate = "resolve_signing_material";
|
|
5942
6045
|
const privateKey = await resolveExecutorSigningPrivateKey({
|
|
5943
|
-
|
|
6046
|
+
credentialSource: cfg.credentialSource,
|
|
5944
6047
|
agentDir: resolvedContext.agentDir,
|
|
5945
6048
|
configuredPrivateKey: cfg.signingPrivateKey,
|
|
5946
6049
|
configuredPrivateKeyRef: cfg.signingPrivateKeyRef
|
|
@@ -5956,7 +6059,7 @@ async function runPolling(opts) {
|
|
|
5956
6059
|
const agentIdentity = await resolveDaemonAgentIdentity({
|
|
5957
6060
|
agentName: baseCommon.agent,
|
|
5958
6061
|
whoami,
|
|
5959
|
-
|
|
6062
|
+
credentialSource: cfg.credentialSource,
|
|
5960
6063
|
agentDir: resolvedContext.agentDir,
|
|
5961
6064
|
gitAuthor: values["git-author"] ?? (cfg.gitAuthor || void 0)
|
|
5962
6065
|
});
|
|
@@ -5977,7 +6080,7 @@ async function runPolling(opts) {
|
|
|
5977
6080
|
level: cfg.logLevel || (baseCommon.debug ? "debug" : "info"),
|
|
5978
6081
|
gate,
|
|
5979
6082
|
agent: baseCommon.agent,
|
|
5980
|
-
|
|
6083
|
+
credentialSource: cfg.credentialSource,
|
|
5981
6084
|
error
|
|
5982
6085
|
});
|
|
5983
6086
|
throw error;
|
|
@@ -6081,7 +6184,7 @@ async function runPolling(opts) {
|
|
|
6081
6184
|
resourceAttributes: {
|
|
6082
6185
|
"moltnet.team.id": teamId,
|
|
6083
6186
|
"moltnet.agent.name": baseCommon.agent,
|
|
6084
|
-
"moltnet.
|
|
6187
|
+
"moltnet.credential.source": ctx.credentialSource,
|
|
6085
6188
|
"moltnet.runtime_profile.count": String(profiles.length),
|
|
6086
6189
|
"moltnet.runtime_profile.ids": profiles.map((p) => p.id).join(",")
|
|
6087
6190
|
}
|
|
@@ -6129,7 +6232,7 @@ async function runPolling(opts) {
|
|
|
6129
6232
|
}
|
|
6130
6233
|
});
|
|
6131
6234
|
rootLogger.info({
|
|
6132
|
-
|
|
6235
|
+
credentialSource: cfg.credentialSource,
|
|
6133
6236
|
subjectType: startupWhoami.subjectType,
|
|
6134
6237
|
bindingScope: startupWhoami.credentialBinding?.bindingScope ?? null,
|
|
6135
6238
|
credentialKeyId: startupWhoami.credentialBinding?.keyId ?? null,
|
|
@@ -6581,23 +6684,24 @@ async function runOnce(argv, runtimeAdapter = defaultPiDaemonAdapter) {
|
|
|
6581
6684
|
};
|
|
6582
6685
|
const runtimeCredentialConfig = resolveCredentialEnforcement(cfg.credentialEnforcement, credentialSources) === "off" ? null : loadRuntimeCredentialConfig(credentialSources);
|
|
6583
6686
|
const initialOpts = opts;
|
|
6584
|
-
const
|
|
6687
|
+
const explicitAgentRootDir = values["agent-root"] ? resolve(process.cwd(), values["agent-root"]) : void 0;
|
|
6585
6688
|
const { ctx, signingPrivateKey, agentIdentity, hostCapabilitySigner } = await (async () => {
|
|
6586
6689
|
let gate = "resolve_agent_context";
|
|
6587
6690
|
try {
|
|
6588
6691
|
const resolvedContext = await resolveAgentContext(initialOpts.agent, {
|
|
6589
|
-
agentRootDir,
|
|
6590
|
-
|
|
6692
|
+
agentRootDir: explicitAgentRootDir,
|
|
6693
|
+
credentialSource: cfg.credentialSource,
|
|
6694
|
+
envApiUrl: cfg.apiUrl
|
|
6591
6695
|
});
|
|
6592
6696
|
gate = "authenticate_and_bind";
|
|
6593
6697
|
const whoami = await validateStartupBinding({
|
|
6594
6698
|
agent: resolvedContext.agent,
|
|
6595
6699
|
teamId: values.team,
|
|
6596
|
-
|
|
6700
|
+
expectedAgent: cfg.expectedAgent
|
|
6597
6701
|
});
|
|
6598
6702
|
gate = "resolve_signing_material";
|
|
6599
6703
|
const privateKey = await resolveExecutorSigningPrivateKey({
|
|
6600
|
-
|
|
6704
|
+
credentialSource: cfg.credentialSource,
|
|
6601
6705
|
agentDir: resolvedContext.agentDir,
|
|
6602
6706
|
configuredPrivateKey: cfg.signingPrivateKey,
|
|
6603
6707
|
configuredPrivateKeyRef: cfg.signingPrivateKeyRef
|
|
@@ -6613,7 +6717,7 @@ async function runOnce(argv, runtimeAdapter = defaultPiDaemonAdapter) {
|
|
|
6613
6717
|
const agentIdentity = await resolveDaemonAgentIdentity({
|
|
6614
6718
|
agentName: initialOpts.agent,
|
|
6615
6719
|
whoami,
|
|
6616
|
-
|
|
6720
|
+
credentialSource: cfg.credentialSource,
|
|
6617
6721
|
agentDir: resolvedContext.agentDir,
|
|
6618
6722
|
gitAuthor: values["git-author"] ?? (cfg.gitAuthor || void 0)
|
|
6619
6723
|
});
|
|
@@ -6633,7 +6737,7 @@ async function runOnce(argv, runtimeAdapter = defaultPiDaemonAdapter) {
|
|
|
6633
6737
|
level: cfg.logLevel || (initialOpts.debug ? "debug" : "info"),
|
|
6634
6738
|
gate,
|
|
6635
6739
|
agent: initialOpts.agent,
|
|
6636
|
-
|
|
6740
|
+
credentialSource: cfg.credentialSource,
|
|
6637
6741
|
error
|
|
6638
6742
|
});
|
|
6639
6743
|
throw error;
|
|
@@ -6703,7 +6807,7 @@ async function runOnce(argv, runtimeAdapter = defaultPiDaemonAdapter) {
|
|
|
6703
6807
|
resourceAttributes: {
|
|
6704
6808
|
"moltnet.task.id": taskId,
|
|
6705
6809
|
"moltnet.agent.name": opts.agent,
|
|
6706
|
-
"moltnet.
|
|
6810
|
+
"moltnet.credential.source": ctx.credentialSource,
|
|
6707
6811
|
"moltnet.llm.provider": profile.provider,
|
|
6708
6812
|
"moltnet.llm.model": profile.model,
|
|
6709
6813
|
...profile.thinkingLevel ? { "moltnet.llm.thinking_level": profile.thinkingLevel } : {},
|
|
@@ -7752,11 +7856,14 @@ function snapshot(login) {
|
|
|
7752
7856
|
...waitingForAuthorization ? { waitingForAuthorization } : {}
|
|
7753
7857
|
};
|
|
7754
7858
|
}
|
|
7755
|
-
var NAME_RE =
|
|
7859
|
+
var NAME_RE = IDENTITY_ALIAS_PATTERN;
|
|
7756
7860
|
var PROVIDER_ID_RE = /^[a-z0-9][a-z0-9-]{0,63}$/;
|
|
7757
7861
|
function assertStoreName(kind, value) {
|
|
7758
|
-
|
|
7759
|
-
|
|
7862
|
+
try {
|
|
7863
|
+
return assertIdentityAlias(value);
|
|
7864
|
+
} catch {
|
|
7865
|
+
throw new AgentServerStoreError("invalid_name", `${kind} must match ${NAME_RE.source}`);
|
|
7866
|
+
}
|
|
7760
7867
|
}
|
|
7761
7868
|
function assertProviderId(value) {
|
|
7762
7869
|
if (!PROVIDER_ID_RE.test(value)) throw new AgentServerStoreError("invalid_name", `provider id must match ${PROVIDER_ID_RE.source}`);
|
|
@@ -7769,12 +7876,20 @@ var AgentServerStoreError = class extends Error {
|
|
|
7769
7876
|
this.code = code;
|
|
7770
7877
|
}
|
|
7771
7878
|
};
|
|
7772
|
-
/**
|
|
7879
|
+
/**
|
|
7880
|
+
* `MOLTNET_AGENT_SERVER_ROOT` override, else `~/.config/moltnet`.
|
|
7881
|
+
*
|
|
7882
|
+
* Deliberately does NOT consult `XDG_CONFIG_HOME`. The Go CLI's GetConfigDir
|
|
7883
|
+
* and @moltnet/agent-config's getConfigDir both resolve `~/.config/moltnet`,
|
|
7884
|
+
* so honouring XDG here gave one application two config roots: on a machine
|
|
7885
|
+
* with the variable set, the daemon wrote identities the CLI and SDK could not
|
|
7886
|
+
* read. `MOLTNET_AGENT_SERVER_ROOT` remains the explicit escape hatch for a
|
|
7887
|
+
* genuinely custom location.
|
|
7888
|
+
*/
|
|
7773
7889
|
function resolveAgentServerRoot(input) {
|
|
7774
7890
|
const override = input.root?.trim();
|
|
7775
7891
|
if (override) return override;
|
|
7776
|
-
|
|
7777
|
-
return join(xdg || join(homedir(), ".config"), "moltnet");
|
|
7892
|
+
return getConfigDir();
|
|
7778
7893
|
}
|
|
7779
7894
|
function providerEnvName(providerId) {
|
|
7780
7895
|
return `MOLTNET_PROVIDER_${assertProviderId(providerId).replaceAll("-", "_").toUpperCase()}_API_KEY`;
|
|
@@ -7812,14 +7927,14 @@ function writeJsonAtomic(path, value) {
|
|
|
7812
7927
|
}
|
|
7813
7928
|
var AgentServerStore = class {
|
|
7814
7929
|
root;
|
|
7815
|
-
|
|
7930
|
+
identitiesDir;
|
|
7816
7931
|
runsDir;
|
|
7817
7932
|
secretsDir;
|
|
7818
7933
|
/** Shared Pi credential dir; `auth.json` inside is pi-managed (lockfiled). */
|
|
7819
7934
|
piDir;
|
|
7820
7935
|
constructor(root) {
|
|
7821
7936
|
this.root = root;
|
|
7822
|
-
this.
|
|
7937
|
+
this.identitiesDir = join(root, "identities");
|
|
7823
7938
|
this.runsDir = join(root, "runs");
|
|
7824
7939
|
this.secretsDir = join(root, "secrets");
|
|
7825
7940
|
this.piDir = join(root, "pi");
|
|
@@ -7831,7 +7946,7 @@ var AgentServerStore = class {
|
|
|
7831
7946
|
ensure() {
|
|
7832
7947
|
for (const dir of [
|
|
7833
7948
|
this.root,
|
|
7834
|
-
this.
|
|
7949
|
+
this.identitiesDir,
|
|
7835
7950
|
this.runsDir,
|
|
7836
7951
|
this.secretsDir
|
|
7837
7952
|
]) mkdirSync(dir, {
|
|
@@ -7846,20 +7961,20 @@ var AgentServerStore = class {
|
|
|
7846
7961
|
readAgentServerState() {
|
|
7847
7962
|
const state = readJson(this.statePath);
|
|
7848
7963
|
if (!state) return {
|
|
7849
|
-
version:
|
|
7964
|
+
version: 2,
|
|
7850
7965
|
pendingRegistrations: {},
|
|
7851
7966
|
activations: {}
|
|
7852
7967
|
};
|
|
7853
|
-
if (!isRecord$1(state) || state.version !==
|
|
7854
|
-
if ("pairedOrigins" in state) throw new AgentServerStoreError("invalid_state", "agent-server.json uses the obsolete pairing format;
|
|
7855
|
-
if (!isRecord$1(state.pendingRegistrations) || !isRecord$1(state.activations)) throw new AgentServerStoreError("invalid_state", "agent-server.json is missing the version
|
|
7968
|
+
if (!isRecord$1(state) || state.version !== 2) throw new AgentServerStoreError("invalid_state", `agent-server.json version ${String(isRecord$1(state) ? state.version : void 0)} is not supported; move agent-server.json aside, run \`moltnet config migrate\`, then add or attach the agents again`);
|
|
7969
|
+
if ("pairedOrigins" in state) throw new AgentServerStoreError("invalid_state", "agent-server.json uses the obsolete pairing format; move agent-server.json aside and configure the agent server again");
|
|
7970
|
+
if (!isRecord$1(state.pendingRegistrations) || !isRecord$1(state.activations)) throw new AgentServerStoreError("invalid_state", "agent-server.json is missing the version 2 activation map; move agent-server.json aside, run `moltnet config migrate`, then add or attach the agents again");
|
|
7856
7971
|
for (const [alias, activation] of Object.entries(state.activations)) validateActivation(alias, activation);
|
|
7857
7972
|
for (const [alias, registration] of Object.entries(state.pendingRegistrations)) {
|
|
7858
7973
|
assertStoreName("agent name", alias);
|
|
7859
7974
|
if (!isRecord$1(registration) || typeof registration.apiUrl !== "string" || registration.apiUrl.length === 0 || typeof registration.createdAt !== "string" || registration.createdAt.length === 0) throw new AgentServerStoreError("invalid_state", `pending registration "${alias}" is not valid`);
|
|
7860
7975
|
}
|
|
7861
7976
|
return {
|
|
7862
|
-
version:
|
|
7977
|
+
version: 2,
|
|
7863
7978
|
pendingRegistrations: state.pendingRegistrations,
|
|
7864
7979
|
activations: state.activations
|
|
7865
7980
|
};
|
|
@@ -7868,16 +7983,58 @@ var AgentServerStore = class {
|
|
|
7868
7983
|
writeJsonAtomic(this.statePath, state);
|
|
7869
7984
|
}
|
|
7870
7985
|
agentPath(name) {
|
|
7871
|
-
return storeChildPath(this.
|
|
7986
|
+
return join(storeChildPath(this.identitiesDir, "identity alias", name), "moltnet.json");
|
|
7987
|
+
}
|
|
7988
|
+
/** Central identity directory, shared with the Go CLI layout. */
|
|
7989
|
+
identityDir(name) {
|
|
7990
|
+
return storeChildPath(this.identitiesDir, "identity alias", name);
|
|
7991
|
+
}
|
|
7992
|
+
get identitySelectorPath() {
|
|
7993
|
+
return join(this.root, "identity-selector.json");
|
|
7994
|
+
}
|
|
7995
|
+
readIdentitySelector() {
|
|
7996
|
+
const selector = readJson(this.identitySelectorPath);
|
|
7997
|
+
if (!selector) return null;
|
|
7998
|
+
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");
|
|
7999
|
+
if (selector.default_identity) assertStoreName("identity alias", selector.default_identity);
|
|
8000
|
+
return selector;
|
|
8001
|
+
}
|
|
8002
|
+
writeIdentitySelector(alias) {
|
|
8003
|
+
writeJsonAtomic(this.identitySelectorPath, {
|
|
8004
|
+
version: 1,
|
|
8005
|
+
default_identity: assertStoreName("identity alias", alias)
|
|
8006
|
+
});
|
|
8007
|
+
}
|
|
8008
|
+
resolveIdentityAlias(explicit, active) {
|
|
8009
|
+
const alias = explicit?.trim() || active?.trim() || this.readIdentitySelector()?.default_identity;
|
|
8010
|
+
if (!alias) throw new AgentServerStoreError("not_found", "no active identity selected");
|
|
8011
|
+
return assertStoreName("identity alias", alias);
|
|
7872
8012
|
}
|
|
7873
8013
|
readAgentConfig(alias) {
|
|
7874
8014
|
return readJson(this.agentPath(alias));
|
|
7875
8015
|
}
|
|
7876
8016
|
writeAgentConfig(alias, config) {
|
|
8017
|
+
mkdirSync(this.identityDir(alias), {
|
|
8018
|
+
recursive: true,
|
|
8019
|
+
mode: 448
|
|
8020
|
+
});
|
|
7877
8021
|
writeJsonAtomic(this.agentPath(alias), config);
|
|
8022
|
+
if (!this.readIdentitySelector()?.default_identity) this.writeIdentitySelector(alias);
|
|
7878
8023
|
}
|
|
7879
8024
|
removeAgentConfig(alias) {
|
|
7880
8025
|
rmSync(this.agentPath(alias), { force: true });
|
|
8026
|
+
this.clearIdentitySelectorIfDefault(alias);
|
|
8027
|
+
}
|
|
8028
|
+
/** Clears the persisted default when it names `alias`. */
|
|
8029
|
+
clearIdentitySelectorIfDefault(alias) {
|
|
8030
|
+
let selector = null;
|
|
8031
|
+
try {
|
|
8032
|
+
selector = this.readIdentitySelector();
|
|
8033
|
+
} catch {
|
|
8034
|
+
return;
|
|
8035
|
+
}
|
|
8036
|
+
if (!selector || selector.default_identity !== alias) return;
|
|
8037
|
+
writeJsonAtomic(this.identitySelectorPath, { version: 1 });
|
|
7881
8038
|
}
|
|
7882
8039
|
readActivation(alias) {
|
|
7883
8040
|
return this.readAgentServerState().activations[assertStoreName("agent name", alias)] ?? null;
|
|
@@ -8046,13 +8203,13 @@ function isStrictDescendant(root, candidate) {
|
|
|
8046
8203
|
}
|
|
8047
8204
|
function validateActivation(alias, value) {
|
|
8048
8205
|
const invalid = () => {
|
|
8049
|
-
throw new AgentServerStoreError("invalid_state", `activation "${alias}" is not a valid version
|
|
8206
|
+
throw new AgentServerStoreError("invalid_state", `activation "${alias}" is not a valid version 2 activation`);
|
|
8050
8207
|
};
|
|
8051
8208
|
if (!isRecord$1(value)) invalid();
|
|
8052
8209
|
const activation = value;
|
|
8053
8210
|
if (activation.alias !== alias) invalid();
|
|
8054
8211
|
if (![
|
|
8055
|
-
"
|
|
8212
|
+
"subjectId",
|
|
8056
8213
|
"publicKey",
|
|
8057
8214
|
"fingerprint",
|
|
8058
8215
|
"createdAt"
|
|
@@ -8097,7 +8254,7 @@ async function createManagedAgent(store, secrets, input, connectAgent = connect)
|
|
|
8097
8254
|
const alias = assertStoreName("agent name", input.name);
|
|
8098
8255
|
if (!input.enrollmentToken.trim()) throw new AgentServerIdentityError("enrollment_required", "an enrollment token from the target team is required — a self-registered agent would be stranded in its own personal team");
|
|
8099
8256
|
const releaseAlias = reserveAlias(store, alias);
|
|
8100
|
-
let
|
|
8257
|
+
let registered = false;
|
|
8101
8258
|
try {
|
|
8102
8259
|
let apiUrl;
|
|
8103
8260
|
try {
|
|
@@ -8114,18 +8271,19 @@ async function createManagedAgent(store, secrets, input, connectAgent = connect)
|
|
|
8114
8271
|
});
|
|
8115
8272
|
if (result.credentials.type !== "agent_key") throw new AgentServerIdentityError("unsupported_credential", `registration returned credential type "${result.credentials.type}"; agent server manages agent-key credentials only`);
|
|
8116
8273
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
8117
|
-
const {
|
|
8118
|
-
|
|
8274
|
+
const { subjectId, fingerprint, publicKey, privateKey } = result.identity;
|
|
8275
|
+
registered = true;
|
|
8119
8276
|
const agentKeyReference = {
|
|
8120
8277
|
provider: FILE_SECRET_PROVIDER,
|
|
8121
|
-
key: agentKeyKey(
|
|
8278
|
+
key: agentKeyKey(subjectId)
|
|
8122
8279
|
};
|
|
8123
8280
|
const seedReference = {
|
|
8124
8281
|
provider: FILE_SECRET_PROVIDER,
|
|
8125
8282
|
key: identitySeedKey(fingerprint)
|
|
8126
8283
|
};
|
|
8127
8284
|
const config = {
|
|
8128
|
-
|
|
8285
|
+
subject_id: subjectId,
|
|
8286
|
+
subject_type: "agent",
|
|
8129
8287
|
registered_at: now,
|
|
8130
8288
|
agent_key_ref: agentKeyReference,
|
|
8131
8289
|
keys: {
|
|
@@ -8146,15 +8304,15 @@ async function createManagedAgent(store, secrets, input, connectAgent = connect)
|
|
|
8146
8304
|
apiUrl: result.apiUrl
|
|
8147
8305
|
}, store.agentPath(alias), input.signal);
|
|
8148
8306
|
assertIdentityMatches(whoami, {
|
|
8149
|
-
identityId,
|
|
8150
8307
|
publicKey,
|
|
8151
8308
|
fingerprint
|
|
8152
8309
|
}, "authenticated whoami", `new managed agent "${alias}"`);
|
|
8310
|
+
assertSubjectMatches(whoami, config, "authenticated whoami", `managed config ${store.agentPath(alias)}`);
|
|
8153
8311
|
const boundTeamId = boundTeamIdFromWhoami(whoami);
|
|
8154
8312
|
const activation = {
|
|
8155
8313
|
alias,
|
|
8156
8314
|
source: "managed",
|
|
8157
|
-
|
|
8315
|
+
subjectId: whoami.subjectId,
|
|
8158
8316
|
publicKey,
|
|
8159
8317
|
fingerprint,
|
|
8160
8318
|
...boundTeamId ? { boundTeamId } : {},
|
|
@@ -8168,11 +8326,11 @@ async function createManagedAgent(store, secrets, input, connectAgent = connect)
|
|
|
8168
8326
|
...boundTeamId ? { boundTeamId } : {}
|
|
8169
8327
|
};
|
|
8170
8328
|
} catch (cause) {
|
|
8171
|
-
if (!
|
|
8329
|
+
if (!registered && cause instanceof MoltNetError && cause.statusCode !== void 0 && cause.statusCode >= 400 && cause.statusCode < 500) {
|
|
8172
8330
|
store.clearPendingRegistration(alias);
|
|
8173
8331
|
throw new AgentServerIdentityError("registration_failed", registrationRejectionMessage(alias, cause), { cause });
|
|
8174
8332
|
}
|
|
8175
|
-
if (store.hasPendingRegistration(alias)) throw new AgentServerIdentityError("registration_incomplete",
|
|
8333
|
+
if (store.hasPendingRegistration(alias)) throw new AgentServerIdentityError("registration_incomplete", registered ? `the remote agent was registered but local activation is incomplete; reconcile or clear its pending Agent Server record before retrying` : `registration for "${alias}" may be incomplete; inspect the remote API before changing its pending Agent Server record`, { cause });
|
|
8176
8334
|
throw cause;
|
|
8177
8335
|
} finally {
|
|
8178
8336
|
releaseAlias();
|
|
@@ -8203,17 +8361,19 @@ async function reconcileManagedRegistration(store, secrets, aliasInput, action,
|
|
|
8203
8361
|
if (!config?.agent_key_ref || config.agent_key_ref.provider !== FILE_SECRET_PROVIDER || config.keys.private_key_ref?.provider !== FILE_SECRET_PROVIDER) throw new AgentServerIdentityError("registration_incomplete", `pending registration for "${alias}" does not have complete managed references`);
|
|
8204
8362
|
const [agentKey, privateKeyState] = await Promise.all([secrets.read(config.agent_key_ref.key), secrets.probe(config.keys.private_key_ref.key)]);
|
|
8205
8363
|
if (!agentKey || privateKeyState !== "present") throw new AgentServerIdentityError("registration_incomplete", `pending registration for "${alias}" is missing persisted secret material`);
|
|
8206
|
-
const identity = identityFromConfig(config);
|
|
8207
8364
|
const apiUrl = requireConfigApiUrl(config, store.agentPath(alias));
|
|
8208
8365
|
const whoami = await callWhoami(connectAgent, {
|
|
8209
8366
|
agentKey,
|
|
8210
8367
|
apiUrl
|
|
8211
8368
|
}, store.agentPath(alias), signal);
|
|
8369
|
+
assertSubjectMatches(whoami, config, "authenticated whoami", `pending registration "${alias}" config`);
|
|
8370
|
+
const identity = identityFromConfig(config);
|
|
8212
8371
|
assertIdentityMatches(whoami, identity, "authenticated whoami", `pending registration "${alias}" config`);
|
|
8213
8372
|
const boundTeamId = boundTeamIdFromWhoami(whoami);
|
|
8214
8373
|
const recovered = {
|
|
8215
8374
|
alias,
|
|
8216
8375
|
source: "managed",
|
|
8376
|
+
subjectId: whoami.subjectId,
|
|
8217
8377
|
...identity,
|
|
8218
8378
|
...boundTeamId ? { boundTeamId } : {},
|
|
8219
8379
|
createdAt: config.registered_at,
|
|
@@ -8239,10 +8399,12 @@ async function attachExternalAgent(store, secretProviders, input, connectAgent =
|
|
|
8239
8399
|
const whoami = await authenticateConfig(input.configDir, effectiveApiUrl, secretProviders, connectAgent, input.signal);
|
|
8240
8400
|
const identity = identityFromConfig(config);
|
|
8241
8401
|
assertIdentityMatches(identity, whoami, `external config ${configPath}`, "authenticated whoami");
|
|
8402
|
+
assertSubjectMatches(whoami, config, "authenticated whoami", `external config ${configPath}`);
|
|
8242
8403
|
const boundTeamId = boundTeamIdFromWhoami(whoami);
|
|
8243
8404
|
const activation = {
|
|
8244
8405
|
alias,
|
|
8245
8406
|
source: "external",
|
|
8407
|
+
subjectId: whoami.subjectId,
|
|
8246
8408
|
...identity,
|
|
8247
8409
|
...boundTeamId ? { boundTeamId } : {},
|
|
8248
8410
|
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
@@ -8260,15 +8422,26 @@ async function attachExternalAgent(store, secretProviders, input, connectAgent =
|
|
|
8260
8422
|
releaseAlias();
|
|
8261
8423
|
}
|
|
8262
8424
|
}
|
|
8263
|
-
/** Load and authenticate the current config, then
|
|
8425
|
+
/** Load and authenticate the current config, then refresh its derived pin. */
|
|
8264
8426
|
async function verifyAgentActivation(store, alias, managedSecretProviders, externalSecretProviders, connectAgent = connect, signal) {
|
|
8265
8427
|
const activation = requireActivation(store, alias);
|
|
8266
8428
|
const verified = activation.source === "managed" ? await verifyManagedActivation(store, activation, managedSecretProviders, connectAgent, signal) : await verifyExternalActivation(activation, externalSecretProviders, connectAgent, signal);
|
|
8267
|
-
|
|
8429
|
+
assertSubjectMatches(verified.whoami, verified.config, "authenticated whoami", `agent "${activation.alias}" config`);
|
|
8430
|
+
if (verified.whoami.subjectId !== activation.subjectId) throw new AgentServerIdentityError("verification_failed", `authenticated whoami subject does not match agent "${activation.alias}" pinned activation`);
|
|
8431
|
+
const identity = identityFromConfig(verified.config);
|
|
8432
|
+
assertIdentityMatches(verified.whoami, identity, "authenticated whoami", `agent "${activation.alias}" config`);
|
|
8268
8433
|
const boundTeamId = boundTeamIdFromWhoami(verified.whoami);
|
|
8269
8434
|
if (activation.boundTeamId !== boundTeamId) throw new AgentServerIdentityError("verification_failed", `authenticated whoami team binding does not match agent "${activation.alias}" pinned activation`);
|
|
8435
|
+
const refreshed = {
|
|
8436
|
+
...activation,
|
|
8437
|
+
...identity
|
|
8438
|
+
};
|
|
8439
|
+
if (activation.publicKey !== refreshed.publicKey || activation.fingerprint !== refreshed.fingerprint) {
|
|
8440
|
+
store.writeActivation(refreshed);
|
|
8441
|
+
process.stderr.write(`agent-server: refreshed the authenticated signing identity for ${JSON.stringify(activation.alias)}\n`);
|
|
8442
|
+
}
|
|
8270
8443
|
return {
|
|
8271
|
-
activation,
|
|
8444
|
+
activation: refreshed,
|
|
8272
8445
|
config: verified.config,
|
|
8273
8446
|
...boundTeamId ? { boundTeamId } : {}
|
|
8274
8447
|
};
|
|
@@ -8311,7 +8484,7 @@ function requireTrustedApiOverride(override, configApiUrl, configPath) {
|
|
|
8311
8484
|
}
|
|
8312
8485
|
function assertActivatedConfig(config, activation, configPath, currentApiUrl, pinnedApiUrl) {
|
|
8313
8486
|
if (currentApiUrl !== pinnedApiUrl) throw new AgentServerIdentityError("verification_failed", `agent config at ${configPath} API endpoint does not match its pinned activation`);
|
|
8314
|
-
|
|
8487
|
+
if (config.subject_id !== activation.subjectId) throw new AgentServerIdentityError("verification_failed", `agent config at ${configPath} subject does not match its pinned activation`);
|
|
8315
8488
|
}
|
|
8316
8489
|
function requireConfigApiUrl(config, configPath) {
|
|
8317
8490
|
const apiUrl = config?.endpoints?.api?.trim();
|
|
@@ -8383,16 +8556,18 @@ function boundedIdentitySignal(signal) {
|
|
|
8383
8556
|
return signal ? AbortSignal.any([signal, timeout]) : timeout;
|
|
8384
8557
|
}
|
|
8385
8558
|
function identityFromConfig(config) {
|
|
8386
|
-
const identityId = config?.identity_id?.trim();
|
|
8387
8559
|
const publicKey = config?.keys?.public_key?.trim();
|
|
8388
8560
|
const fingerprint = config?.keys?.fingerprint?.trim();
|
|
8389
|
-
if (!
|
|
8561
|
+
if (!publicKey || !fingerprint) throw new AgentServerIdentityError("verification_failed", "agent config is missing keys.public_key or keys.fingerprint");
|
|
8390
8562
|
return {
|
|
8391
|
-
identityId,
|
|
8392
8563
|
publicKey,
|
|
8393
8564
|
fingerprint
|
|
8394
8565
|
};
|
|
8395
8566
|
}
|
|
8567
|
+
function assertSubjectMatches(current, expected, currentLabel, expectedLabel) {
|
|
8568
|
+
if (!isCanonicalConfig(expected)) throw new AgentServerIdentityError("verification_failed", `${expectedLabel} is missing canonical subject_type=agent and subject_id; run \`moltnet config migrate\` first`);
|
|
8569
|
+
if (current.subjectType !== "agent" || current.subjectId !== expected.subject_id) throw new AgentServerIdentityError("verification_failed", `${currentLabel} subject does not match ${expectedLabel}`);
|
|
8570
|
+
}
|
|
8396
8571
|
function assertIdentityMatches(current, expected, currentLabel, expectedLabel) {
|
|
8397
8572
|
const assessment = assessIdentityPin(current, expected);
|
|
8398
8573
|
if (!assessment.ok) throw new AgentServerIdentityError("verification_failed", `${currentLabel} ${assessment.label} does not match ${expectedLabel}`);
|
|
@@ -8407,7 +8582,7 @@ function publicAgentView(store, activation) {
|
|
|
8407
8582
|
return {
|
|
8408
8583
|
kind: "managed",
|
|
8409
8584
|
agentName: activation.alias,
|
|
8410
|
-
|
|
8585
|
+
subjectId: activation.subjectId,
|
|
8411
8586
|
fingerprint: activation.fingerprint,
|
|
8412
8587
|
...activation.boundTeamId ? { teamId: activation.boundTeamId } : {},
|
|
8413
8588
|
apiUrl: activation.apiUrl,
|
|
@@ -8421,7 +8596,7 @@ function publicAgentView(store, activation) {
|
|
|
8421
8596
|
agentName: activation.alias,
|
|
8422
8597
|
configDir: dirname(activation.configPath),
|
|
8423
8598
|
...activation.apiUrl ? { apiUrl: activation.apiUrl } : {},
|
|
8424
|
-
|
|
8599
|
+
subjectId: activation.subjectId,
|
|
8425
8600
|
fingerprint: activation.fingerprint,
|
|
8426
8601
|
...activation.boundTeamId ? { teamId: activation.boundTeamId } : {},
|
|
8427
8602
|
createdAt: activation.createdAt
|
|
@@ -8567,7 +8742,8 @@ var RunManager = class {
|
|
|
8567
8742
|
throw new AgentServerRunError("invalid_spec", `external credentials for "${activation.alias}" could not be projected`);
|
|
8568
8743
|
}
|
|
8569
8744
|
}
|
|
8570
|
-
env["
|
|
8745
|
+
env["MOLTNET_EXPECTED_SUBJECT_ID"] = activation.subjectId;
|
|
8746
|
+
env["MOLTNET_EXPECTED_SUBJECT_TYPE"] = "agent";
|
|
8571
8747
|
env["MOLTNET_EXPECTED_PUBLIC_KEY"] = activation.publicKey;
|
|
8572
8748
|
env["MOLTNET_EXPECTED_FINGERPRINT"] = activation.fingerprint;
|
|
8573
8749
|
env["MOLTNET_SUPERVISED_RUN"] = "1";
|
|
@@ -9224,7 +9400,7 @@ var AgentServerProblemSchema = Type.Object({
|
|
|
9224
9400
|
var AgentServerAgentSchema = Type.Object({
|
|
9225
9401
|
kind: Type.Union([Type.Literal("managed"), Type.Literal("external")]),
|
|
9226
9402
|
agentName: Type.String(),
|
|
9227
|
-
|
|
9403
|
+
subjectId: Type.String(),
|
|
9228
9404
|
fingerprint: Type.Optional(Type.String()),
|
|
9229
9405
|
apiUrl: Type.Optional(Type.String()),
|
|
9230
9406
|
teamId: Type.Optional(Type.String()),
|
|
@@ -10387,10 +10563,7 @@ async function runAgentServer(argv) {
|
|
|
10387
10563
|
return 1;
|
|
10388
10564
|
}
|
|
10389
10565
|
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
|
-
});
|
|
10566
|
+
const root = values.root ?? resolveAgentServerRoot({ root: envConfig.root });
|
|
10394
10567
|
const defaultApiUrl = values["api-url"] ?? (envConfig.apiUrl || DEFAULT_API_URL);
|
|
10395
10568
|
const store = new AgentServerStore(root).ensure();
|
|
10396
10569
|
if (trustRequested) return runTrustCommand(commandArgs, root);
|
|
@@ -10686,7 +10859,13 @@ async function runSyncSessions(argv) {
|
|
|
10686
10859
|
const state = parseState(values.state);
|
|
10687
10860
|
const limit = parseLimit(values.limit);
|
|
10688
10861
|
const agentRootDir = resolve(process.cwd(), values["agent-root"] ?? process.cwd());
|
|
10689
|
-
const
|
|
10862
|
+
const explicitAgentRootDir = values["agent-root"] ? resolve(process.cwd(), values["agent-root"]) : void 0;
|
|
10863
|
+
const cfg = loadConfig();
|
|
10864
|
+
const ctx = await resolveAgentContext(opts.agent, {
|
|
10865
|
+
credentialSource: cfg.credentialSource,
|
|
10866
|
+
envApiUrl: cfg.apiUrl,
|
|
10867
|
+
agentRootDir: explicitAgentRootDir
|
|
10868
|
+
});
|
|
10690
10869
|
await validateStartupBinding({
|
|
10691
10870
|
agent: ctx.agent,
|
|
10692
10871
|
teamId: values.team
|
|
@@ -10721,18 +10900,51 @@ function parseLimit(raw) {
|
|
|
10721
10900
|
}
|
|
10722
10901
|
//#endregion
|
|
10723
10902
|
//#region src/lib/update.ts
|
|
10724
|
-
|
|
10903
|
+
/**
|
|
10904
|
+
* `npm install -g @themoltnet/agent-daemon@latest` resolves the registry's own
|
|
10905
|
+
* dist-tag, so for an npm install the registry is the precise answer — the
|
|
10906
|
+
* manifest serves the themolt.net pin, which gates the bundle installer's
|
|
10907
|
+
* default and says nothing about what npm already has. It is also the origin an
|
|
10908
|
+
* npm install already talks to, so this adds no new dependency in CI, where the
|
|
10909
|
+
* daemon runs as `npx @themoltnet/agent-daemon` and would otherwise reach for
|
|
10910
|
+
* api.github.com from shared runner IPs.
|
|
10911
|
+
*
|
|
10912
|
+
* Bundle and direct installs ask the release listing, and their upgrade command
|
|
10913
|
+
* passes `MOLTNET_AGENT_VERSION=latest` so the installer resolves the same thing
|
|
10914
|
+
* rather than falling back to its own pinned default. Reporting the pin here
|
|
10915
|
+
* would hide releases that are already downloadable; reporting the release
|
|
10916
|
+
* without the sentinel would advertise a version the command could not deliver.
|
|
10917
|
+
* The pin still governs a fresh `curl .../install/agent | sh`.
|
|
10918
|
+
*/
|
|
10919
|
+
var UPDATE_RELEASES_URL = "https://api.github.com/repos/getlarge/themoltnet/releases?per_page=100";
|
|
10920
|
+
var RELEASE_TAG_PREFIX = "agent-daemon-v";
|
|
10921
|
+
var UPDATE_NPM_REGISTRY_URL = "https://registry.npmjs.org/@themoltnet/agent-daemon/latest";
|
|
10725
10922
|
var UPDATE_CACHE_TTL_MS = 1440 * 60 * 1e3;
|
|
10726
10923
|
var UPDATE_ERROR_CACHE_TTL_MS = 300 * 1e3;
|
|
10924
|
+
/**
|
|
10925
|
+
* Node does not resolve `process.argv[1]`: invoked through npm's bin shim it
|
|
10926
|
+
* reports `/usr/local/bin/moltnet-agent`, not the `node_modules` path this
|
|
10927
|
+
* matches on. Every global npm install therefore looked like `direct` and was
|
|
10928
|
+
* offered the bundle installer, which would drop a bundle on top of an npm
|
|
10929
|
+
* install. Resolve first; a missing path falls back to the input.
|
|
10930
|
+
*/
|
|
10931
|
+
function resolveDaemonExecutable(executable) {
|
|
10932
|
+
if (!executable) return executable;
|
|
10933
|
+
try {
|
|
10934
|
+
return realpathSync(executable);
|
|
10935
|
+
} catch {
|
|
10936
|
+
return executable;
|
|
10937
|
+
}
|
|
10938
|
+
}
|
|
10727
10939
|
function detectDaemonInstallMethod(executable = process.argv[1] ?? "") {
|
|
10728
|
-
const path = executable.replaceAll("\\", "/");
|
|
10940
|
+
const path = resolveDaemonExecutable(executable).replaceAll("\\", "/");
|
|
10729
10941
|
if (path.includes("/node_modules/@themoltnet/agent-daemon/")) return "npm";
|
|
10730
10942
|
if (path.includes("/.local/share/moltnet/") || path.includes("/opt/moltnet/")) return "bundle";
|
|
10731
10943
|
return "direct";
|
|
10732
10944
|
}
|
|
10733
10945
|
function daemonUpdateCommand(method) {
|
|
10734
10946
|
if (method === "npm") return "npm install -g @themoltnet/agent-daemon@latest";
|
|
10735
|
-
return "curl -fsSL https://themolt.net/install/agent | sh";
|
|
10947
|
+
return "curl -fsSL https://themolt.net/install/agent | MOLTNET_AGENT_VERSION=latest sh";
|
|
10736
10948
|
}
|
|
10737
10949
|
async function checkDaemonUpdate(input) {
|
|
10738
10950
|
const installMethod = detectDaemonInstallMethod(input.executable ?? process.argv[1] ?? "moltnet-agent");
|
|
@@ -10753,11 +10965,15 @@ async function checkDaemonUpdate(input) {
|
|
|
10753
10965
|
return result;
|
|
10754
10966
|
}
|
|
10755
10967
|
}
|
|
10968
|
+
const viaNpm = installMethod === "npm";
|
|
10969
|
+
const source = viaNpm ? UPDATE_NPM_REGISTRY_URL : UPDATE_RELEASES_URL;
|
|
10970
|
+
const label = viaNpm ? "npm registry" : "release listing";
|
|
10756
10971
|
try {
|
|
10757
|
-
const response = await (input.fetchFn ?? fetch)(
|
|
10758
|
-
if (!response.ok) throw new Error(
|
|
10759
|
-
const
|
|
10760
|
-
|
|
10972
|
+
const response = await (input.fetchFn ?? fetch)(source, { signal: AbortSignal.timeout(5e3) });
|
|
10973
|
+
if (!response.ok) throw new Error(`${label} returned HTTP ${response.status}`);
|
|
10974
|
+
const body = await response.json();
|
|
10975
|
+
const latest = viaNpm ? distTagVersion(body) : newestReleaseVersion(body);
|
|
10976
|
+
if (!latest) throw new Error(`${label} has no valid agent version`);
|
|
10761
10977
|
await writeCache({
|
|
10762
10978
|
checkedAt: now.toISOString(),
|
|
10763
10979
|
latest
|
|
@@ -10773,13 +10989,31 @@ async function checkDaemonUpdate(input) {
|
|
|
10773
10989
|
throw new Error(`could not check for MoltNet agent updates: ${error instanceof Error ? error.message : String(error)}`);
|
|
10774
10990
|
}
|
|
10775
10991
|
}
|
|
10776
|
-
function
|
|
10992
|
+
function distTagVersion(value) {
|
|
10777
10993
|
if (!value || typeof value !== "object") return void 0;
|
|
10778
|
-
const
|
|
10779
|
-
if (!agent || typeof agent !== "object") return void 0;
|
|
10780
|
-
const version = agent.version;
|
|
10994
|
+
const version = value.version;
|
|
10781
10995
|
return typeof version === "string" && validVersion(version) ? normalizeVersion(version) : void 0;
|
|
10782
10996
|
}
|
|
10997
|
+
/**
|
|
10998
|
+
* The listing is ordered by creation date, not version, so compare every
|
|
10999
|
+
* candidate rather than trusting position. Drafts are filtered explicitly:
|
|
11000
|
+
* unauthenticated callers never see them, but a token would, and this
|
|
11001
|
+
* repository carries stuck drafts that would otherwise look newest.
|
|
11002
|
+
*/
|
|
11003
|
+
function newestReleaseVersion(value) {
|
|
11004
|
+
if (!Array.isArray(value)) return void 0;
|
|
11005
|
+
let newest;
|
|
11006
|
+
for (const entry of value) {
|
|
11007
|
+
if (!entry || typeof entry !== "object") continue;
|
|
11008
|
+
const { tag_name: tag, draft, prerelease } = entry;
|
|
11009
|
+
if (draft === true || prerelease === true) continue;
|
|
11010
|
+
if (typeof tag !== "string" || !tag.startsWith(RELEASE_TAG_PREFIX)) continue;
|
|
11011
|
+
const candidate = tag.slice(14);
|
|
11012
|
+
if (!validVersion(candidate)) continue;
|
|
11013
|
+
if (!newest || compareVersions(candidate, newest) > 0) newest = normalizeVersion(candidate);
|
|
11014
|
+
}
|
|
11015
|
+
return newest;
|
|
11016
|
+
}
|
|
10783
11017
|
function normalizeVersion(value) {
|
|
10784
11018
|
return value.trim().replace(/^v/, "");
|
|
10785
11019
|
}
|
|
@@ -10819,7 +11053,7 @@ async function writeCache(cache) {
|
|
|
10819
11053
|
}
|
|
10820
11054
|
//#endregion
|
|
10821
11055
|
//#region src/version.ts
|
|
10822
|
-
var DAEMON_VERSION = "0.
|
|
11056
|
+
var DAEMON_VERSION = "0.55.0";
|
|
10823
11057
|
//#endregion
|
|
10824
11058
|
//#region src/cli.ts
|
|
10825
11059
|
async function runAgentDaemonCli(options) {
|
|
@@ -10877,11 +11111,7 @@ async function runUpdate(argv) {
|
|
|
10877
11111
|
}
|
|
10878
11112
|
}
|
|
10879
11113
|
async function runRuntime(argv) {
|
|
10880
|
-
const
|
|
10881
|
-
const registry = new RuntimeRegistry(resolveAgentServerRoot({
|
|
10882
|
-
root: config.root,
|
|
10883
|
-
xdgConfigHome: config.xdgConfigHome
|
|
10884
|
-
}));
|
|
11114
|
+
const registry = new RuntimeRegistry(resolveAgentServerRoot({ root: loadAgentServerEnvConfig().root }));
|
|
10885
11115
|
const [command, ...rest] = argv;
|
|
10886
11116
|
if (command === "list") {
|
|
10887
11117
|
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.55.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/agent-runtime": "0.
|
|
88
|
+
"@themoltnet/agent-runtime": "1.0.0",
|
|
89
|
+
"@themoltnet/pi-runtime": "0.15.0",
|
|
89
90
|
"@themoltnet/os-keyring": "0.3.0",
|
|
90
|
-
"@themoltnet/
|
|
91
|
-
"@themoltnet/sdk": "0.140.1"
|
|
91
|
+
"@themoltnet/sdk": "0.141.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/
|
|
101
|
+
"@moltnet/bootstrap": "0.1.0",
|
|
102
102
|
"@moltnet/crypto-service": "0.1.0",
|
|
103
103
|
"@moltnet/execution-integrations": "0.1.0",
|
|
104
|
-
"@moltnet/
|
|
104
|
+
"@moltnet/agent-eval": "0.1.0",
|
|
105
105
|
"@moltnet/execution-plan": "0.1.0",
|
|
106
|
+
"@moltnet/loopback-companion": "0.1.0",
|
|
106
107
|
"@moltnet/models": "0.1.0",
|
|
108
|
+
"@moltnet/tasks": "0.1.0",
|
|
107
109
|
"@moltnet/observability": "0.1.0",
|
|
108
|
-
"@moltnet/
|
|
109
|
-
"@moltnet/runtime-profiles": "0.1.0",
|
|
110
|
-
"@moltnet/tasks": "0.1.0"
|
|
110
|
+
"@moltnet/runtime-profiles": "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
|
},
|