@themoltnet/agent-daemon 0.30.5 → 0.31.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 +14 -4
- package/dist/main.js +93 -13
- package/package.json +6 -6
package/README.md
CHANGED
|
@@ -41,15 +41,25 @@ All config flows from environment variables. The daemon reads them in
|
|
|
41
41
|
|
|
42
42
|
### MoltNet identity
|
|
43
43
|
|
|
44
|
-
| Var | Required | Purpose
|
|
45
|
-
| -------------------- | -------- |
|
|
46
|
-
| `GIT_CONFIG_GLOBAL` | yes | Path to the agent's gitconfig (resolves the `.moltnet/<agent>/` dir).
|
|
47
|
-
| `MOLTNET_AGENT_NAME` | yes | Agent name (matches `.moltnet/<name>/`).
|
|
44
|
+
| Var | Required | Purpose |
|
|
45
|
+
| -------------------- | -------- | ------------------------------------------------------------------------- |
|
|
46
|
+
| `GIT_CONFIG_GLOBAL` | yes | Path to the agent's gitconfig (resolves the `.moltnet/<agent>/` dir). |
|
|
47
|
+
| `MOLTNET_AGENT_NAME` | yes | Agent name (matches `.moltnet/<name>/`). |
|
|
48
|
+
| `MOLTNET_AGENT_KEY` | no | Team-bound agent key. Set to authenticate with the key instead of OAuth2. |
|
|
48
49
|
|
|
49
50
|
The agent's `moltnet.json` and gitconfig live next to each other in
|
|
50
51
|
`.moltnet/<agent>/`. Provision them once via
|
|
51
52
|
[`legreffier init`](../../docs/start/install-and-initialize.md).
|
|
52
53
|
|
|
54
|
+
**Auth mode.** When `MOLTNET_AGENT_KEY` is set the daemon authenticates with
|
|
55
|
+
that key as an opaque bearer token (no OAuth2 exchange); otherwise it uses the
|
|
56
|
+
OAuth2 client-credentials from `moltnet.json`. The key is read from the
|
|
57
|
+
environment only — never store it in `moltnet.json`. Because a key is bound to
|
|
58
|
+
exactly one team, the daemon reconciles `--team` against the key at startup and
|
|
59
|
+
fails fast if the key is rejected, is not an agent, or is bound to a different
|
|
60
|
+
team. See
|
|
61
|
+
[Run the daemon with an agent key](../../docs/operate/running-agents.md#run-the-daemon-with-an-agent-key).
|
|
62
|
+
|
|
53
63
|
### Pi provider auth
|
|
54
64
|
|
|
55
65
|
The daemon resolves Pi config from the repository-local `.pi` directory by
|
package/dist/main.js
CHANGED
|
@@ -13,7 +13,7 @@ import { AgentRuntime, ApiTaskReporter, ApiTaskSource, PollingApiTaskSource } fr
|
|
|
13
13
|
import { createPiRetryTriage, createPiTaskExecutor, findMainWorktree, normalizeRetryTriageResult, redactRetryTriageSecrets } from "@themoltnet/pi-extension";
|
|
14
14
|
import { execFile, execFileSync } from "node:child_process";
|
|
15
15
|
import { accessSync, constants, createReadStream, createWriteStream, existsSync, mkdirSync, readdirSync } from "node:fs";
|
|
16
|
-
import { MoltNetError, connect } from "@themoltnet/sdk";
|
|
16
|
+
import { AuthenticationError, MoltNetError, connect } from "@themoltnet/sdk";
|
|
17
17
|
import { once } from "node:events";
|
|
18
18
|
import { pino, transport } from "pino";
|
|
19
19
|
import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-proto";
|
|
@@ -9627,21 +9627,64 @@ function isHelpFlag(args) {
|
|
|
9627
9627
|
return args.includes("--help") || args.includes("-h");
|
|
9628
9628
|
}
|
|
9629
9629
|
//#endregion
|
|
9630
|
-
//#region src/
|
|
9631
|
-
|
|
9632
|
-
|
|
9633
|
-
|
|
9634
|
-
|
|
9635
|
-
|
|
9636
|
-
|
|
9637
|
-
|
|
9630
|
+
//#region src/lib/agent-context.ts
|
|
9631
|
+
/**
|
|
9632
|
+
* Report which auth mode `connect()` will use, without ever reading the secret
|
|
9633
|
+
* value into anything logged. Agent-key mode is selected when
|
|
9634
|
+
* `MOLTNET_AGENT_KEY` holds a non-blank value — mirroring the SDK precedence
|
|
9635
|
+
* where an environment key opts into key mode ahead of the config-file OAuth2
|
|
9636
|
+
* credentials. The daemon never passes explicit in-code credentials to
|
|
9637
|
+
* `connect()`, so this env-only check matches what `connect()` actually does.
|
|
9638
|
+
*
|
|
9639
|
+
* Pure: `env` is passed in (the config module owns the `process.env` read).
|
|
9640
|
+
*/
|
|
9641
|
+
function detectAuthMode(env) {
|
|
9642
|
+
return env.MOLTNET_AGENT_KEY?.trim() ? "agent-key" : "oauth2";
|
|
9643
|
+
}
|
|
9644
|
+
/**
|
|
9645
|
+
* Pure check: may the identity described by `whoami` operate the daemon as
|
|
9646
|
+
* `teamId`? Kept side-effect free so it can be unit-tested in isolation.
|
|
9647
|
+
*
|
|
9648
|
+
* Rules (see design entry edb848a1):
|
|
9649
|
+
* - The subject must be an `agent`; a human credential can never run the daemon.
|
|
9650
|
+
* - A team-bound agent key (`credentialBinding.boundTeamId` set) must match the
|
|
9651
|
+
* `--team` the daemon was started with. A key is an immutable team ceiling, so
|
|
9652
|
+
* a mismatch would only surface as an obscure mid-poll 403 otherwise.
|
|
9653
|
+
* - An unbound key or an OAuth2 identity (no `boundTeamId`) is accepted; normal
|
|
9654
|
+
* team-scoped authorization governs those requests.
|
|
9655
|
+
*/
|
|
9656
|
+
function assessStartupBinding(whoami, teamId) {
|
|
9657
|
+
if (whoami.subjectType !== "agent") return {
|
|
9658
|
+
ok: false,
|
|
9659
|
+
reason: `the daemon must authenticate as an agent, but whoami reported subjectType "${whoami.subjectType}". Provide agent credentials (an agent key or the agent's client id/secret).`
|
|
9660
|
+
};
|
|
9661
|
+
const boundTeamId = whoami.credentialBinding?.boundTeamId;
|
|
9662
|
+
if (boundTeamId && boundTeamId !== teamId) return {
|
|
9663
|
+
ok: false,
|
|
9664
|
+
reason: `the agent key is bound to team ${boundTeamId}, but the daemon was started with --team ${teamId}. Restart with --team ${boundTeamId}, or issue a key for team ${teamId}.`
|
|
9638
9665
|
};
|
|
9666
|
+
return { ok: true };
|
|
9639
9667
|
}
|
|
9640
|
-
|
|
9641
|
-
|
|
9668
|
+
/**
|
|
9669
|
+
* Validate at startup — after `connect()`, before polling — that the connected
|
|
9670
|
+
* credential can operate as `teamId`, failing fast with an actionable message
|
|
9671
|
+
* instead of letting an obscure 401/403 surface mid-poll. Runs in both auth
|
|
9672
|
+
* modes; in OAuth2 mode it also doubles as an API-reachability and
|
|
9673
|
+
* subject-type check. Returns the `whoami` so the caller can log the resolved
|
|
9674
|
+
* identity (never the secret).
|
|
9675
|
+
*/
|
|
9676
|
+
async function validateStartupBinding(options) {
|
|
9677
|
+
let whoami;
|
|
9678
|
+
try {
|
|
9679
|
+
whoami = await options.agent.agents.whoami();
|
|
9680
|
+
} catch (err) {
|
|
9681
|
+
if (err instanceof AuthenticationError) throw new Error(`Daemon startup authentication failed: ${err.message}`);
|
|
9682
|
+
throw err;
|
|
9683
|
+
}
|
|
9684
|
+
const assessment = assessStartupBinding(whoami, options.teamId);
|
|
9685
|
+
if (!assessment.ok) throw new Error(`Daemon startup validation failed: ${assessment.reason}`);
|
|
9686
|
+
return whoami;
|
|
9642
9687
|
}
|
|
9643
|
-
//#endregion
|
|
9644
|
-
//#region src/lib/agent-context.ts
|
|
9645
9688
|
/**
|
|
9646
9689
|
* Resolve the agent's MoltNet credentials directory and connect via SDK.
|
|
9647
9690
|
*
|
|
@@ -9675,6 +9718,28 @@ function resolveCredentialRoots(agentRootDir) {
|
|
|
9675
9718
|
return roots;
|
|
9676
9719
|
}
|
|
9677
9720
|
//#endregion
|
|
9721
|
+
//#region src/config.ts
|
|
9722
|
+
/**
|
|
9723
|
+
* Daemon configuration — single env-var entry point.
|
|
9724
|
+
*
|
|
9725
|
+
* Mirrors the rest-api convention: `process.env` is read here and only
|
|
9726
|
+
* here, so the rest of the daemon imports typed values rather than
|
|
9727
|
+
* sprinkling string lookups across the codebase.
|
|
9728
|
+
*/
|
|
9729
|
+
function loadConfig() {
|
|
9730
|
+
return {
|
|
9731
|
+
otelEndpoint: process.env["MOLTNET_OTEL_ENDPOINT"] ?? "",
|
|
9732
|
+
logLevel: process.env["LOG_LEVEL"] ?? "",
|
|
9733
|
+
profilePrerequisiteEnv: process.env,
|
|
9734
|
+
profilePrerequisitePath: process.env.PATH ?? "",
|
|
9735
|
+
piCodingAgentDir: process.env["PI_CODING_AGENT_DIR"] ?? "",
|
|
9736
|
+
authMode: detectAuthMode(process.env)
|
|
9737
|
+
};
|
|
9738
|
+
}
|
|
9739
|
+
function activatePiCodingAgentDir(path) {
|
|
9740
|
+
process.env["PI_CODING_AGENT_DIR"] = path;
|
|
9741
|
+
}
|
|
9742
|
+
//#endregion
|
|
9678
9743
|
//#region src/lib/correlation.ts
|
|
9679
9744
|
var execFileAsync = promisify(execFile);
|
|
9680
9745
|
var CORRELATION_TRAILER_KEY = "Moltnet-Correlation-Id";
|
|
@@ -11057,6 +11122,10 @@ async function runPolling(opts) {
|
|
|
11057
11122
|
const cfg = loadConfig();
|
|
11058
11123
|
const agentRootDir = resolve(process.cwd(), values["agent-root"] ?? process.cwd());
|
|
11059
11124
|
const ctx = await resolveAgentContext(baseCommon.agent, { agentRootDir });
|
|
11125
|
+
const startupWhoami = await validateStartupBinding({
|
|
11126
|
+
agent: ctx.agent,
|
|
11127
|
+
teamId
|
|
11128
|
+
});
|
|
11060
11129
|
const profiles = await resolveRuntimeProfiles({
|
|
11061
11130
|
agent: ctx.agent,
|
|
11062
11131
|
profiles: profileValues,
|
|
@@ -11160,6 +11229,9 @@ async function runPolling(opts) {
|
|
|
11160
11229
|
}
|
|
11161
11230
|
});
|
|
11162
11231
|
rootLogger.info({
|
|
11232
|
+
authMode: cfg.authMode,
|
|
11233
|
+
subjectType: startupWhoami.subjectType,
|
|
11234
|
+
boundTeamId: startupWhoami.credentialBinding?.boundTeamId ?? null,
|
|
11163
11235
|
taskTypes: taskTypes.length > 0 ? taskTypes : ["*"],
|
|
11164
11236
|
diaryIds: diaryIds.length > 0 ? diaryIds : ["*"],
|
|
11165
11237
|
pollIntervalMs,
|
|
@@ -11513,6 +11585,10 @@ async function runOnce(argv) {
|
|
|
11513
11585
|
const initialOpts = opts;
|
|
11514
11586
|
const agentRootDir = resolve(process.cwd(), values["agent-root"] ?? process.cwd());
|
|
11515
11587
|
const ctx = await resolveAgentContext(initialOpts.agent, { agentRootDir });
|
|
11588
|
+
if (values.team) await validateStartupBinding({
|
|
11589
|
+
agent: ctx.agent,
|
|
11590
|
+
teamId: values.team
|
|
11591
|
+
});
|
|
11516
11592
|
const profile = await resolveRuntimeProfile({
|
|
11517
11593
|
agent: ctx.agent,
|
|
11518
11594
|
profile: values.profile,
|
|
@@ -11948,6 +12024,10 @@ async function runSyncSessions(argv) {
|
|
|
11948
12024
|
const limit = parseLimit(values.limit);
|
|
11949
12025
|
const agentRootDir = resolve(process.cwd(), values["agent-root"] ?? process.cwd());
|
|
11950
12026
|
const ctx = await resolveAgentContext(opts.agent, { agentRootDir });
|
|
12027
|
+
await validateStartupBinding({
|
|
12028
|
+
agent: ctx.agent,
|
|
12029
|
+
teamId: values.team
|
|
12030
|
+
});
|
|
11951
12031
|
const stateDirs = ensureDaemonStateDirs(agentRootDir);
|
|
11952
12032
|
const result = await syncRuntimeSessions({
|
|
11953
12033
|
runtimeSessionStore: createApiRuntimeSessionStore({ agent: ctx.agent }),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@themoltnet/agent-daemon",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.31.0",
|
|
4
4
|
"license": "AGPL-3.0-only",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "MoltNet agent daemon — claims and executes tasks (fulfill_brief, assess_brief) from the MoltNet task-service via Pi-headless. CLI: moltnet-agent.",
|
|
@@ -51,9 +51,9 @@
|
|
|
51
51
|
"@opentelemetry/semantic-conventions": "^1.39.0",
|
|
52
52
|
"pino": "^10.3.1",
|
|
53
53
|
"pino-pretty": "^13.1.3",
|
|
54
|
-
"@themoltnet/
|
|
55
|
-
"@themoltnet/
|
|
56
|
-
"@themoltnet/sdk": "0.
|
|
54
|
+
"@themoltnet/pi-extension": "0.35.4",
|
|
55
|
+
"@themoltnet/agent-runtime": "0.36.4",
|
|
56
|
+
"@themoltnet/sdk": "0.125.0"
|
|
57
57
|
},
|
|
58
58
|
"devDependencies": {
|
|
59
59
|
"tsx": "^4.7.0",
|
|
@@ -61,9 +61,9 @@
|
|
|
61
61
|
"vite": "^8.0.0",
|
|
62
62
|
"vitest": "^3.0.0",
|
|
63
63
|
"@moltnet/bootstrap": "0.1.0",
|
|
64
|
+
"@moltnet/tasks": "0.1.0",
|
|
64
65
|
"@moltnet/observability": "0.1.0",
|
|
65
|
-
"@moltnet/crypto-service": "0.1.0"
|
|
66
|
-
"@moltnet/tasks": "0.1.0"
|
|
66
|
+
"@moltnet/crypto-service": "0.1.0"
|
|
67
67
|
},
|
|
68
68
|
"nx": {
|
|
69
69
|
"projectType": "application",
|