@themoltnet/agent-daemon 0.63.0 → 0.64.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 +6 -2
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +1161 -354
- package/package.json +10 -10
package/dist/cli.js
CHANGED
|
@@ -9,9 +9,9 @@ import "multiformats/codecs/json";
|
|
|
9
9
|
import "multiformats/hashes/sha2";
|
|
10
10
|
import "typebox/value";
|
|
11
11
|
import { execFile, spawn } from "node:child_process";
|
|
12
|
-
import { basename, dirname, isAbsolute, join, resolve, sep } from "node:path";
|
|
12
|
+
import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
13
13
|
import { parseArgs, parseEnv, promisify } from "node:util";
|
|
14
|
-
import { CredentialPersistenceError, EnrollmentRecoveryError, FILE_SECRET_PROVIDER, FileSecretProvider, ProjectConfigError, ProvisioningNotStartedError, RegisterIdentityError, WORKSPACE_STRATEGIES, boundedIdentitySignal, canonicalDirectory, canonicalStoreRoot, connect, createNodeSecretProviderRegistry, defaultStoreRoot, enrollTeam, getProjectConfigPath, isDefaultStore, readProjectConfig, register, resolveProjectBinding, resolveStoreRoot, resolveStoreSelection } from "@themoltnet/sdk/node";
|
|
14
|
+
import { CredentialPersistenceError, EnrollmentRecoveryError, FILE_SECRET_PROVIDER, FileSecretProvider, ProjectConfigError, ProvisioningNotStartedError, RegisterIdentityError, WORKSPACE_STRATEGIES, boundedIdentitySignal, canonicalDirectory, canonicalStoreRoot, connect, createNodeSecretProviderRegistry, defaultStoreRoot, enrollTeam, getProjectConfigPath, isDefaultStore, normalizeProjectEndpoint, readProjectConfig, register, resolveProjectBinding, resolveStoreRoot, resolveStoreSelection, updateProjectConfig, validateProjectConfig } from "@themoltnet/sdk/node";
|
|
15
15
|
import { constants, copyFileSync, createReadStream, createWriteStream, existsSync, lstatSync, mkdirSync, mkdtempSync, openSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, statSync, symlinkSync, writeFileSync } from "node:fs";
|
|
16
16
|
import { AuthenticationError, IDENTITY_ALIAS_PATTERN, MoltNetError, assertIdentityAlias, assertTrustedConfigApiUrl, createExecutorAttestor, formatSecretReferenceString, getConfigDir, getIdentityDir, hasAgentKeyConfiguration, isCanonicalConfig, parseSecretReferenceString, readConfig, requireSecureCredentialApiUrl, resolveAgentKey, resolveEnvSecretReference, resolveIdentitySeed, selectAgentKeyReference, signBytes } from "@themoltnet/sdk";
|
|
17
17
|
import { AgentRuntime, ApiTaskReporter, ApiTaskSource, PollingApiTaskSource, RuntimeProfilePrerequisiteError, createLocalSeedSigner, resolveAgentIdentity, resolveRuntimeProfile, resolveRuntimeProfiles, validateRuntimeProfilePrerequisites } from "@themoltnet/agent-runtime";
|
|
@@ -866,10 +866,8 @@ var OPERATOR_OAUTH = Object.freeze({
|
|
|
866
866
|
provisioningAudience: "moltnet:provisioning",
|
|
867
867
|
localControlAudience: "moltnet:agent-server",
|
|
868
868
|
nativeClientId: "moltnet-native",
|
|
869
|
-
consoleClientId: "moltnet-console",
|
|
870
869
|
approvalTransportGraceSeconds: 30,
|
|
871
870
|
callbackPort: 17375,
|
|
872
|
-
consoleLifetimeSeconds: 900,
|
|
873
871
|
nativeLifetimeSeconds: 300,
|
|
874
872
|
serverPort: 17374
|
|
875
873
|
});
|
|
@@ -1548,7 +1546,9 @@ Type.Intersect([ConflictProblemDetailsSchema, Type.Object({ flagged: Type.Option
|
|
|
1548
1546
|
threats: Type.Array(Type.Ref("InjectionThreat"))
|
|
1549
1547
|
}, { additionalProperties: false }))) })], { $id: "InjectionConflictProblemDetails" });
|
|
1550
1548
|
Type.Intersect([ProblemDetailsSchema, Type.Object({ errors: Type.Array(Type.Ref("ValidationError")) })], { $id: "ValidationProblemDetails" });
|
|
1551
|
-
|
|
1549
|
+
//#endregion
|
|
1550
|
+
//#region ../../libs/models/src/projects.ts
|
|
1551
|
+
var ProjectResponseSchema = Type.Object({
|
|
1552
1552
|
id: Type.String({ format: "uuid" }),
|
|
1553
1553
|
teamId: Type.String({ format: "uuid" }),
|
|
1554
1554
|
creatorAgentId: Type.Union([Type.String({ format: "uuid" }), Type.Null()]),
|
|
@@ -3758,8 +3758,11 @@ async function resolveSelectionApiUrl(agentName, options) {
|
|
|
3758
3758
|
* sprinkling string lookups across the codebase.
|
|
3759
3759
|
*/
|
|
3760
3760
|
/** All daemon commands announce the same legacy alias policy as Go and Desktop. */
|
|
3761
|
+
function markLegacyStoreNoticeShown() {
|
|
3762
|
+
process.env["MOLTNET_LEGACY_STORE_NOTICE_SHOWN"] = "1";
|
|
3763
|
+
}
|
|
3761
3764
|
function legacyStoreNotice() {
|
|
3762
|
-
return process.env["MOLTNET_AGENT_SERVER_ROOT"] !== void 0 ? "MOLTNET_AGENT_SERVER_ROOT is deprecated; use MOLTNET_HOME. Both select the entire store; no data is migrated." : void 0;
|
|
3765
|
+
return process.env["MOLTNET_AGENT_SERVER_ROOT"] !== void 0 && process.env["MOLTNET_LEGACY_STORE_NOTICE_SHOWN"] !== "1" ? "MOLTNET_AGENT_SERVER_ROOT is deprecated; use MOLTNET_HOME. Both select the entire store; no data is migrated." : void 0;
|
|
3763
3766
|
}
|
|
3764
3767
|
function loadConfig() {
|
|
3765
3768
|
assertSingleCredentialForm("MOLTNET_AGENT_KEY", "MOLTNET_AGENT_KEY_REF");
|
|
@@ -3824,14 +3827,12 @@ function loadAgentServerEnvConfig(root) {
|
|
|
3824
3827
|
const issuer = process.env["MOLTNET_OPERATOR_OAUTH_ISSUER"];
|
|
3825
3828
|
const publicUrl = process.env["MOLTNET_OPERATOR_OAUTH_PUBLIC_URL"] ?? issuer;
|
|
3826
3829
|
const nativeClientId = process.env["MOLTNET_NATIVE_OAUTH_CLIENT_ID"];
|
|
3827
|
-
const consoleClientId = process.env["MOLTNET_CONSOLE_OAUTH_CLIENT_ID"];
|
|
3828
3830
|
const apiUrl = process.env["MOLTNET_OPERATOR_API_URL"];
|
|
3829
3831
|
return {
|
|
3830
3832
|
operatorOAuth: {
|
|
3831
3833
|
...issuer ? { issuer } : {},
|
|
3832
3834
|
...publicUrl ? { publicUrl } : {},
|
|
3833
3835
|
...nativeClientId ? { nativeClientId } : {},
|
|
3834
|
-
...consoleClientId ? { consoleClientId } : {},
|
|
3835
3836
|
...apiUrl ? { apiUrl } : {}
|
|
3836
3837
|
},
|
|
3837
3838
|
port: process.env["MOLTNET_AGENT_SERVER_PORT"] ?? "",
|
|
@@ -3859,7 +3860,8 @@ function loadUpdateEnvConfig(root) {
|
|
|
3859
3860
|
var execFileAsync$1 = promisify(execFile);
|
|
3860
3861
|
var GIT_TIMEOUT_MS = 1e4;
|
|
3861
3862
|
var GIT_MAX_OUTPUT_BYTES = 64 * 1024;
|
|
3862
|
-
|
|
3863
|
+
/** `signal` stops the `git` process; an abort is reported as an abort. */
|
|
3864
|
+
async function validateGitSource(source, signal) {
|
|
3863
3865
|
const inherited = processEnvSnapshot();
|
|
3864
3866
|
try {
|
|
3865
3867
|
const { stdout } = await execFileAsync$1("git", [
|
|
@@ -3877,15 +3879,37 @@ async function validateGitSource(source) {
|
|
|
3877
3879
|
},
|
|
3878
3880
|
timeout: GIT_TIMEOUT_MS,
|
|
3879
3881
|
killSignal: "SIGKILL",
|
|
3880
|
-
maxBuffer: GIT_MAX_OUTPUT_BYTES
|
|
3882
|
+
maxBuffer: GIT_MAX_OUTPUT_BYTES,
|
|
3883
|
+
...signal ? { signal } : {}
|
|
3881
3884
|
});
|
|
3882
3885
|
const top = stdout.trim().split("\n")[0];
|
|
3883
3886
|
if (await canonicalDirectory(top) === source) return;
|
|
3884
3887
|
} catch (cause) {
|
|
3888
|
+
if (signal?.aborted) throw cause;
|
|
3885
3889
|
throw new ProjectConfigError("selection", `git-worktree source ${source} requires a Git repository root with a committed revision: ${cause instanceof Error ? cause.message : String(cause)}`, { cause });
|
|
3886
3890
|
}
|
|
3887
3891
|
throw new ProjectConfigError("selection", `git-worktree source ${source} must be the Git repository root, not a subdirectory`);
|
|
3888
3892
|
}
|
|
3893
|
+
/** Hooks that would run commands; an empty `hooks: {}` prepares nothing. */
|
|
3894
|
+
function hasPreparationHooks(hooks) {
|
|
3895
|
+
return Boolean(hooks?.afterCreate || hooks?.beforeRun);
|
|
3896
|
+
}
|
|
3897
|
+
/**
|
|
3898
|
+
* The one rule for whether this runtime can prepare a location. Worker
|
|
3899
|
+
* selection, workspace policy and Desktop readiness all call it, so a location
|
|
3900
|
+
* Desktop shows as ready is one a worker can start.
|
|
3901
|
+
*/
|
|
3902
|
+
function preparationBlocker(strategy, hooks) {
|
|
3903
|
+
if (strategy === "isolated-directory") return {
|
|
3904
|
+
code: "unsupported_strategy",
|
|
3905
|
+
message: "this runtime does not support isolated-directory preparation"
|
|
3906
|
+
};
|
|
3907
|
+
if (hasPreparationHooks(hooks)) return {
|
|
3908
|
+
code: "hooks_unavailable",
|
|
3909
|
+
message: "this runtime does not support project setup hooks"
|
|
3910
|
+
};
|
|
3911
|
+
return null;
|
|
3912
|
+
}
|
|
3889
3913
|
function projectRunOptionDefs() {
|
|
3890
3914
|
return {
|
|
3891
3915
|
project: { type: "string" },
|
|
@@ -3897,13 +3921,16 @@ function projectRunOptionDefs() {
|
|
|
3897
3921
|
"workspace-strategy": { type: "string" }
|
|
3898
3922
|
};
|
|
3899
3923
|
}
|
|
3924
|
+
/** Serialize for a worker; the inverse of parsing with `projectRunOptionDefs`. */
|
|
3925
|
+
function projectRunArgs(flags) {
|
|
3926
|
+
return Object.entries(flags).flatMap(([name, value]) => value === void 0 || value === false ? [] : value === true ? [`--${name}`] : [`--${name}`, value]);
|
|
3927
|
+
}
|
|
3900
3928
|
function strategy(value) {
|
|
3901
3929
|
if (value === void 0) return void 0;
|
|
3902
3930
|
if (WORKSPACE_STRATEGIES.includes(value)) return value;
|
|
3903
3931
|
throw new ProjectConfigError("validation", `Unknown workspace strategy ${value}; choose ${WORKSPACE_STRATEGIES.join(", ")}`);
|
|
3904
3932
|
}
|
|
3905
|
-
|
|
3906
|
-
async function resolveRunProjectSelection(args) {
|
|
3933
|
+
async function resolveRunProjectSelection(args, options = {}) {
|
|
3907
3934
|
const env = processEnvSnapshot();
|
|
3908
3935
|
const inherited = env.MOLTNET_ACTIVE_IDENTITY === args.agent && !args.general;
|
|
3909
3936
|
const bindingName = args.binding ?? (!args["config-file"] && !args.project && inherited ? env.MOLTNET_PROJECT_BINDING : void 0);
|
|
@@ -3935,8 +3962,10 @@ async function resolveRunProjectSelection(args) {
|
|
|
3935
3962
|
if (workspaceStrategy === "none" && args.source !== void 0) throw new ProjectConfigError("selection", "No-workspace execution cannot specify a source");
|
|
3936
3963
|
let source = binding?.source;
|
|
3937
3964
|
if (workspaceStrategy !== "none" && !source) source = await canonicalDirectory(resolve(args.cwd, args.source ?? "."));
|
|
3938
|
-
|
|
3939
|
-
if (
|
|
3965
|
+
const blocker = preparationBlocker(workspaceStrategy, binding?.hooks);
|
|
3966
|
+
if (blocker) throw new ProjectConfigError("selection", `Binding ${binding?.name ?? "(run override)"} in ${configPath}: ${blocker.message}; choose a supported binding`);
|
|
3967
|
+
if (source && (binding || args.source !== void 0)) options.guardSource?.(source);
|
|
3968
|
+
if (workspaceStrategy === "git-worktree" && source) await validateGitSource(source, options.signal);
|
|
3940
3969
|
return {
|
|
3941
3970
|
configPath,
|
|
3942
3971
|
selectedBy: args.binding || args.project ? "explicit" : bindingName ? "activation" : binding ? "ancestor" : "general",
|
|
@@ -3953,8 +3982,8 @@ async function resolveRunProjectSelection(args) {
|
|
|
3953
3982
|
/** A selected location fixes this worker's strategy; saved profiles are never mutated. */
|
|
3954
3983
|
function applyProjectWorkspacePolicy(profile, selection) {
|
|
3955
3984
|
if (!selection.workspaceExplicit) return profile;
|
|
3956
|
-
|
|
3957
|
-
if (
|
|
3985
|
+
const blocker = preparationBlocker(selection.strategy, selection.binding?.hooks);
|
|
3986
|
+
if (blocker) throw new Error(`Workspace preparation: ${blocker.message}`);
|
|
3958
3987
|
const mode = selection.strategy === "existing" ? "shared_mount" : selection.strategy === "git-worktree" ? "dedicated_worktree" : "none";
|
|
3959
3988
|
if (profile.allowedWorkspaceModes.length && !profile.allowedWorkspaceModes.includes(mode)) throw new Error(`Workspace strategy ${selection.strategy} is not allowed by profile ${profile.name}`);
|
|
3960
3989
|
return {
|
|
@@ -4169,7 +4198,9 @@ Example:
|
|
|
4169
4198
|
--agent legreffier \\
|
|
4170
4199
|
--state idle`;
|
|
4171
4200
|
function isHelpFlag(args) {
|
|
4172
|
-
|
|
4201
|
+
const separator = args.indexOf("--");
|
|
4202
|
+
const flags = separator === -1 ? args : args.slice(0, separator);
|
|
4203
|
+
return flags.includes("--help") || flags.includes("-h");
|
|
4173
4204
|
}
|
|
4174
4205
|
var AGENT_SERVER_HELP = `\
|
|
4175
4206
|
agent-daemon server — local supervisor for managed runs.
|
|
@@ -5685,6 +5716,8 @@ function assertProviderEnvName(providerId, value) {
|
|
|
5685
5716
|
if (value !== expected) throw new AgentServerStoreError("invalid_state", `provider envName must be ${expected}`);
|
|
5686
5717
|
return value;
|
|
5687
5718
|
}
|
|
5719
|
+
/** The run follows its runtime profile's own workspace mode. */
|
|
5720
|
+
var PROFILE_DEFAULT_STRATEGY = "profile-default";
|
|
5688
5721
|
function readJson(path) {
|
|
5689
5722
|
let raw;
|
|
5690
5723
|
try {
|
|
@@ -9261,8 +9294,7 @@ var RELEASE_CONNECTION = {
|
|
|
9261
9294
|
apiUrl: "https://api.themolt.net",
|
|
9262
9295
|
issuer: "https://auth.themolt.net",
|
|
9263
9296
|
publicUrl: "https://auth.themolt.net",
|
|
9264
|
-
nativeClientId: "moltnet-native"
|
|
9265
|
-
consoleClientId: "moltnet-console"
|
|
9297
|
+
nativeClientId: "moltnet-native"
|
|
9266
9298
|
};
|
|
9267
9299
|
var KEYS = Object.keys(RELEASE_CONNECTION);
|
|
9268
9300
|
function validateConnectionOverrides(value) {
|
|
@@ -9559,7 +9591,7 @@ var OperatorOAuth = class {
|
|
|
9559
9591
|
const command = process.platform === "darwin" ? "open" : process.platform === "win32" ? "explorer.exe" : "xdg-open";
|
|
9560
9592
|
return new Promise((resolve, reject) => {
|
|
9561
9593
|
execFile(command, [url], (error) => {
|
|
9562
|
-
if (error) reject(/* @__PURE__ */ new Error("Could not open
|
|
9594
|
+
if (error) reject(/* @__PURE__ */ new Error("Could not open browser approval"));
|
|
9563
9595
|
else resolve();
|
|
9564
9596
|
});
|
|
9565
9597
|
});
|
|
@@ -9589,22 +9621,14 @@ var OperatorOAuth = class {
|
|
|
9589
9621
|
cancel() {
|
|
9590
9622
|
this.pending?.abort();
|
|
9591
9623
|
}
|
|
9624
|
+
operatorConfigured() {
|
|
9625
|
+
return this.operator !== null;
|
|
9626
|
+
}
|
|
9592
9627
|
removeOperator() {
|
|
9593
9628
|
this.cancel();
|
|
9594
9629
|
rmSync(join(this.root, "operator.json"), { force: true });
|
|
9595
9630
|
this.operator = null;
|
|
9596
9631
|
}
|
|
9597
|
-
metadata() {
|
|
9598
|
-
return {
|
|
9599
|
-
protocolVersion: OPERATOR_OAUTH.protocolVersion,
|
|
9600
|
-
instance: this.instance,
|
|
9601
|
-
issuer: this.config.issuer,
|
|
9602
|
-
authorizationUrl: this.config.authorizationUrl,
|
|
9603
|
-
tokenUrl: this.config.tokenUrl,
|
|
9604
|
-
clientId: this.config.consoleClientId,
|
|
9605
|
-
operatorConfigured: !!this.operator
|
|
9606
|
-
};
|
|
9607
|
-
}
|
|
9608
9632
|
async verify(token, scope, clientId) {
|
|
9609
9633
|
const { payload } = await jwtVerify(token, this.keys, {
|
|
9610
9634
|
algorithms: ["RS256"],
|
|
@@ -9615,7 +9639,7 @@ var OperatorOAuth = class {
|
|
|
9615
9639
|
"iat",
|
|
9616
9640
|
"sub"
|
|
9617
9641
|
],
|
|
9618
|
-
maxTokenAge:
|
|
9642
|
+
maxTokenAge: OPERATOR_OAUTH.nativeLifetimeSeconds
|
|
9619
9643
|
});
|
|
9620
9644
|
const claims = payload.ext;
|
|
9621
9645
|
const scopes = typeof payload.scope === "string" ? payload.scope.split(" ") : payload.scp;
|
|
@@ -9626,10 +9650,6 @@ var OperatorOAuth = class {
|
|
|
9626
9650
|
provisioning: claims["moltnet:provisioning"]
|
|
9627
9651
|
};
|
|
9628
9652
|
}
|
|
9629
|
-
async verifyBrowser(token) {
|
|
9630
|
-
const operator = await this.verify(token, LOCAL_SCOPE, this.config.consoleClientId);
|
|
9631
|
-
if (!this.operator || operator.issuer !== this.operator.issuer || operator.subject !== this.operator.subject) throw new InvalidOperatorGrantError("Native operator sign-in required");
|
|
9632
|
-
}
|
|
9633
9653
|
async authorize(grant, signal) {
|
|
9634
9654
|
if (this.active) throw new Error("An approval is already pending");
|
|
9635
9655
|
this.active = true;
|
|
@@ -9683,9 +9703,9 @@ var OperatorOAuth = class {
|
|
|
9683
9703
|
...grant ? { provisioning: JSON.stringify(grant) } : {}
|
|
9684
9704
|
})) url.searchParams.set(key, value);
|
|
9685
9705
|
try {
|
|
9686
|
-
Promise.resolve(this.openBrowser(url.href)).catch(() => reject(/* @__PURE__ */ new Error("Could not open
|
|
9706
|
+
Promise.resolve(this.openBrowser(url.href)).catch(() => reject(/* @__PURE__ */ new Error("Could not open browser approval")));
|
|
9687
9707
|
} catch {
|
|
9688
|
-
reject(/* @__PURE__ */ new Error("Could not open
|
|
9708
|
+
reject(/* @__PURE__ */ new Error("Could not open browser approval"));
|
|
9689
9709
|
}
|
|
9690
9710
|
});
|
|
9691
9711
|
});
|
|
@@ -10096,6 +10116,94 @@ function snapshot(login) {
|
|
|
10096
10116
|
};
|
|
10097
10117
|
}
|
|
10098
10118
|
//#endregion
|
|
10119
|
+
//#region src/lib/agent-server/catalogue-project-reader.ts
|
|
10120
|
+
var PAGE_SIZE = 100;
|
|
10121
|
+
var ProjectPaginationError = class extends Error {
|
|
10122
|
+
name = "ProjectPaginationError";
|
|
10123
|
+
};
|
|
10124
|
+
async function readCatalogueProjects(projects, teamId) {
|
|
10125
|
+
const items = [];
|
|
10126
|
+
let offset = 0;
|
|
10127
|
+
for (let page = 0; page < 10; page++) {
|
|
10128
|
+
const result = await projects.list({
|
|
10129
|
+
includeArchived: false,
|
|
10130
|
+
limit: PAGE_SIZE,
|
|
10131
|
+
offset
|
|
10132
|
+
}, { teamId });
|
|
10133
|
+
items.push(...result.items);
|
|
10134
|
+
if (result.nextOffset === null) return {
|
|
10135
|
+
items,
|
|
10136
|
+
truncated: false
|
|
10137
|
+
};
|
|
10138
|
+
if (!Number.isSafeInteger(result.nextOffset) || result.nextOffset <= offset) throw new ProjectPaginationError("Invalid project pagination offset");
|
|
10139
|
+
offset = result.nextOffset;
|
|
10140
|
+
}
|
|
10141
|
+
return {
|
|
10142
|
+
items,
|
|
10143
|
+
truncated: true
|
|
10144
|
+
};
|
|
10145
|
+
}
|
|
10146
|
+
/** One project by id; null when the credential cannot see it. */
|
|
10147
|
+
async function readCatalogueProject(projects, teamId, projectId) {
|
|
10148
|
+
try {
|
|
10149
|
+
return await projects.get(projectId, { teamId });
|
|
10150
|
+
} catch (error) {
|
|
10151
|
+
if (error instanceof MoltNetError && (error.statusCode === 403 || error.statusCode === 404)) return null;
|
|
10152
|
+
throw error;
|
|
10153
|
+
}
|
|
10154
|
+
}
|
|
10155
|
+
//#endregion
|
|
10156
|
+
//#region src/lib/agent-server/readiness.ts
|
|
10157
|
+
/**
|
|
10158
|
+
* Whether a runtime profile can execute on *this* machine.
|
|
10159
|
+
*
|
|
10160
|
+
* A profile is authored in Console against a team; whether it can run depends
|
|
10161
|
+
* on local facts only the server knows — which provider keys are configured
|
|
10162
|
+
* here and which runtime kinds this machine can produce.
|
|
10163
|
+
*
|
|
10164
|
+
* The prerequisite comparison itself is **not** reimplemented here. Run start
|
|
10165
|
+
* calls `validateRuntimeProfilePrerequisites`, and the catalogue calls the same
|
|
10166
|
+
* function, so the composer cannot promise a run that startup would reject for
|
|
10167
|
+
* a reason the two evaluated differently.
|
|
10168
|
+
*/
|
|
10169
|
+
function deriveProfileReadiness(profile, machine) {
|
|
10170
|
+
const blockers = [];
|
|
10171
|
+
const env = {};
|
|
10172
|
+
for (const [name, configured] of machine.providerEnv) if (configured) env[name] = "configured";
|
|
10173
|
+
try {
|
|
10174
|
+
validateRuntimeProfilePrerequisites(profile, env, {
|
|
10175
|
+
tools: machine.inventory?.tools ?? profile.requiredTools,
|
|
10176
|
+
executables: machine.inventory?.executables ?? profile.requiredExecutables
|
|
10177
|
+
});
|
|
10178
|
+
} catch (error) {
|
|
10179
|
+
if (!(error instanceof RuntimeProfilePrerequisiteError)) throw error;
|
|
10180
|
+
for (const name of error.missingEnv) blockers.push({
|
|
10181
|
+
code: "env_missing",
|
|
10182
|
+
message: `${name} is not configured on this machine.`,
|
|
10183
|
+
remedy: "Add the key under Providers, then reopen this run."
|
|
10184
|
+
});
|
|
10185
|
+
for (const name of error.missingTools) blockers.push({
|
|
10186
|
+
code: "tool_missing",
|
|
10187
|
+
message: `The runtime does not provide the tool ${name}.`,
|
|
10188
|
+
remedy: `Use a profile whose runtime provides ${name}, or change the profile in Console.`
|
|
10189
|
+
});
|
|
10190
|
+
for (const name of error.missingExecutables) blockers.push({
|
|
10191
|
+
code: "executable_missing",
|
|
10192
|
+
message: `The runtime does not provide the executable ${name}.`,
|
|
10193
|
+
remedy: `Use a runtime that ships ${name}, or drop the requirement in Console.`
|
|
10194
|
+
});
|
|
10195
|
+
}
|
|
10196
|
+
if (!machine.runtimeKinds.has(profile.runtimeKind)) blockers.push({
|
|
10197
|
+
code: "runtime_unregistered",
|
|
10198
|
+
message: `Runtime kind ${profile.runtimeKind} is not available on this machine.`,
|
|
10199
|
+
remedy: "Register the runtime under Runtimes, then reopen this run."
|
|
10200
|
+
});
|
|
10201
|
+
return {
|
|
10202
|
+
ready: blockers.length === 0,
|
|
10203
|
+
blockers
|
|
10204
|
+
};
|
|
10205
|
+
}
|
|
10206
|
+
//#endregion
|
|
10099
10207
|
//#region src/lib/agent-server/identity.ts
|
|
10100
10208
|
/**
|
|
10101
10209
|
* AgentServer identity activation (#2061/#1834 boundary).
|
|
@@ -10539,7 +10647,7 @@ async function verifyTeamActivation(store, alias, managed, external, connectImpl
|
|
|
10539
10647
|
if (missing.length) throw new TeamCredentialError({
|
|
10540
10648
|
code: "agent_key_scopes_insufficient",
|
|
10541
10649
|
message: `This credential lacks ${missing.join(", ")}.`,
|
|
10542
|
-
remedy: "Renew through
|
|
10650
|
+
remedy: "Renew through browser approval with the required desktop scopes."
|
|
10543
10651
|
});
|
|
10544
10652
|
activated.boundTeamId = teamId;
|
|
10545
10653
|
return captureTeamCredential(activated, {
|
|
@@ -10549,6 +10657,367 @@ async function verifyTeamActivation(store, alias, managed, external, connectImpl
|
|
|
10549
10657
|
});
|
|
10550
10658
|
}
|
|
10551
10659
|
//#endregion
|
|
10660
|
+
//#region src/lib/agent-server/catalogue.ts
|
|
10661
|
+
/**
|
|
10662
|
+
* The team/profile catalogue the desktop Run Center composes runs from.
|
|
10663
|
+
*
|
|
10664
|
+
* Scoped to the *selected agent identity*, never a human session: the daemon
|
|
10665
|
+
* already reads runtime profiles with agent credentials on every run, so this
|
|
10666
|
+
* reads the same data with the same authority, earlier. An agent-scoped list is
|
|
10667
|
+
* also authoritative about what can actually run, which a human-scoped list is
|
|
10668
|
+
* not — Console needs a "this agent cannot poll that team" error precisely
|
|
10669
|
+
* because it offers teams the agent cannot serve.
|
|
10670
|
+
*
|
|
10671
|
+
* Team and diary travel together here for the same reason `RunSpec` pairs
|
|
10672
|
+
* them: the CLI's context store refuses one without the other, and a diary
|
|
10673
|
+
* that drifts from its team means entries land in the wrong place.
|
|
10674
|
+
*/
|
|
10675
|
+
async function buildCatalogue(options) {
|
|
10676
|
+
const { agent, machine, identityDefault, logger } = options;
|
|
10677
|
+
const entries = await Promise.all(agent.teamIds.map(async (teamId) => {
|
|
10678
|
+
try {
|
|
10679
|
+
const result = await agent.readTeam(teamId);
|
|
10680
|
+
if (result.team.id !== teamId) throw new Error("Team response mismatch");
|
|
10681
|
+
const diaries = result.diaries.filter((diary) => diary.teamId === teamId).map(({ id, name }) => ({
|
|
10682
|
+
id,
|
|
10683
|
+
name
|
|
10684
|
+
}));
|
|
10685
|
+
const team = {
|
|
10686
|
+
teamId,
|
|
10687
|
+
teamName: result.team.name,
|
|
10688
|
+
available: true,
|
|
10689
|
+
blockers: [],
|
|
10690
|
+
credential: result.credential,
|
|
10691
|
+
diaries,
|
|
10692
|
+
defaultDiaryId: resolveDefaultDiary(teamId, diaries, identityDefault)
|
|
10693
|
+
};
|
|
10694
|
+
const profiles = result.profiles.filter((profile) => profile.teamId === teamId).map((profile) => ({
|
|
10695
|
+
...profile,
|
|
10696
|
+
...deriveProfileReadiness(profile, machine)
|
|
10697
|
+
}));
|
|
10698
|
+
try {
|
|
10699
|
+
const page = await agent.readProjects(teamId);
|
|
10700
|
+
return {
|
|
10701
|
+
team,
|
|
10702
|
+
profiles,
|
|
10703
|
+
projects: page.items.filter((project) => project.teamId === teamId && !project.archived),
|
|
10704
|
+
projectErrors: page.truncated ? [{
|
|
10705
|
+
teamId,
|
|
10706
|
+
code: "truncated",
|
|
10707
|
+
message: "Only the first projects are listed. Archive unused projects to see the rest."
|
|
10708
|
+
}] : []
|
|
10709
|
+
};
|
|
10710
|
+
} catch (error) {
|
|
10711
|
+
logger?.warn({
|
|
10712
|
+
...safeErrorContext(error),
|
|
10713
|
+
teamId,
|
|
10714
|
+
code: "agent_server_project_discovery_failed"
|
|
10715
|
+
}, "AgentServer project discovery failed");
|
|
10716
|
+
return {
|
|
10717
|
+
team,
|
|
10718
|
+
profiles,
|
|
10719
|
+
projects: [],
|
|
10720
|
+
projectErrors: [projectError(teamId, error)]
|
|
10721
|
+
};
|
|
10722
|
+
}
|
|
10723
|
+
} catch (error) {
|
|
10724
|
+
const blocker = credentialBlocker(error);
|
|
10725
|
+
logger?.warn({
|
|
10726
|
+
...safeErrorContext(error),
|
|
10727
|
+
teamId,
|
|
10728
|
+
blocker: blocker.code,
|
|
10729
|
+
code: "agent_server_team_unavailable"
|
|
10730
|
+
}, "AgentServer team credential unavailable");
|
|
10731
|
+
return {
|
|
10732
|
+
team: {
|
|
10733
|
+
teamId,
|
|
10734
|
+
teamName: teamId,
|
|
10735
|
+
available: false,
|
|
10736
|
+
blockers: [blocker],
|
|
10737
|
+
credential: agent.lastVerified(teamId),
|
|
10738
|
+
diaries: [],
|
|
10739
|
+
defaultDiaryId: null
|
|
10740
|
+
},
|
|
10741
|
+
profiles: [],
|
|
10742
|
+
projects: [],
|
|
10743
|
+
projectErrors: []
|
|
10744
|
+
};
|
|
10745
|
+
}
|
|
10746
|
+
}));
|
|
10747
|
+
const teams = entries.map(({ team }) => team);
|
|
10748
|
+
const available = teams.filter((team) => team.available);
|
|
10749
|
+
return {
|
|
10750
|
+
teams,
|
|
10751
|
+
defaultTeamId: available.find((team) => team.teamId === identityDefault.teamId)?.teamId ?? available[0]?.teamId ?? null,
|
|
10752
|
+
profiles: entries.flatMap((entry) => entry.profiles),
|
|
10753
|
+
projects: entries.flatMap((entry) => entry.projects),
|
|
10754
|
+
projectErrors: entries.flatMap((entry) => entry.projectErrors)
|
|
10755
|
+
};
|
|
10756
|
+
}
|
|
10757
|
+
function projectError(teamId, error) {
|
|
10758
|
+
if (error instanceof MoltNetError && (error.statusCode === 401 || error.statusCode === 403)) return {
|
|
10759
|
+
teamId,
|
|
10760
|
+
code: "forbidden",
|
|
10761
|
+
message: "This team credential cannot list projects. Renew it with project access."
|
|
10762
|
+
};
|
|
10763
|
+
if (error instanceof ProjectPaginationError) return {
|
|
10764
|
+
teamId,
|
|
10765
|
+
code: "invalid_response",
|
|
10766
|
+
message: "The server returned an unreadable project list."
|
|
10767
|
+
};
|
|
10768
|
+
return {
|
|
10769
|
+
teamId,
|
|
10770
|
+
code: "unreachable",
|
|
10771
|
+
message: "Projects could not be loaded. Retry project discovery."
|
|
10772
|
+
};
|
|
10773
|
+
}
|
|
10774
|
+
/** The one default-diary rule, shared with General run start. */
|
|
10775
|
+
function resolveDefaultDiary(teamId, diaries, identityDefault) {
|
|
10776
|
+
const bound = diaries.find((diary) => diary.id === identityDefault.diaryId);
|
|
10777
|
+
if (bound && identityDefault.teamId === teamId) return bound.id;
|
|
10778
|
+
return diaries.length === 1 ? diaries[0]?.id ?? null : null;
|
|
10779
|
+
}
|
|
10780
|
+
//#endregion
|
|
10781
|
+
//#region src/lib/agent-server/http-error.ts
|
|
10782
|
+
/** A route failure with a stable wire code; the server error handler maps it verbatim. */
|
|
10783
|
+
var AgentServerHttpError = class extends Error {
|
|
10784
|
+
name = "AgentServerHttpError";
|
|
10785
|
+
constructor(statusCode, code, message, options) {
|
|
10786
|
+
super(message, options);
|
|
10787
|
+
this.statusCode = statusCode;
|
|
10788
|
+
this.code = code;
|
|
10789
|
+
}
|
|
10790
|
+
};
|
|
10791
|
+
//#endregion
|
|
10792
|
+
//#region src/lib/agent-server/identity-binding.ts
|
|
10793
|
+
/**
|
|
10794
|
+
* The identity-wide team/diary binding, read from `<identityDir>/env`.
|
|
10795
|
+
*
|
|
10796
|
+
* This mirrors the Go CLI's `identityDefaultBinding` (`project_selection.go`),
|
|
10797
|
+
* which is the fallback the CLI uses when a working directory has no
|
|
10798
|
+
* registered project binding.
|
|
10799
|
+
*
|
|
10800
|
+
* The desktop cannot use the CLI's *location* bindings at all — its composer
|
|
10801
|
+
* has no working directory to key on — but it can honour this identity-wide
|
|
10802
|
+
* default, so an operator sees their familiar team preselected rather than an
|
|
10803
|
+
* arbitrary first entry.
|
|
10804
|
+
*
|
|
10805
|
+
* Like the CLI, a half-filled pair is treated as no binding: a team without a
|
|
10806
|
+
* diary is ignored.
|
|
10807
|
+
*/
|
|
10808
|
+
function readIdentityDefaultBinding(identityDir) {
|
|
10809
|
+
let contents;
|
|
10810
|
+
try {
|
|
10811
|
+
contents = readFileSync(join(identityDir, "env"), "utf8");
|
|
10812
|
+
} catch {
|
|
10813
|
+
return {};
|
|
10814
|
+
}
|
|
10815
|
+
const env = parseEnv(contents);
|
|
10816
|
+
const teamId = env["MOLTNET_TEAM_ID"]?.trim();
|
|
10817
|
+
const diaryId = env["MOLTNET_DIARY_ID"]?.trim();
|
|
10818
|
+
if (!teamId || !diaryId) return {};
|
|
10819
|
+
return {
|
|
10820
|
+
teamId,
|
|
10821
|
+
diaryId
|
|
10822
|
+
};
|
|
10823
|
+
}
|
|
10824
|
+
async function verifyProjectTarget(reader, target, options) {
|
|
10825
|
+
const { signal, logger } = options;
|
|
10826
|
+
let project = null;
|
|
10827
|
+
let diary = null;
|
|
10828
|
+
try {
|
|
10829
|
+
await untilAborted(signal, async () => {
|
|
10830
|
+
if (target.projectId) project = await reader.readProject(target.teamId, target.projectId, signal);
|
|
10831
|
+
if (target.diaryId) diary = await reader.readDiary(target.teamId, target.diaryId, signal);
|
|
10832
|
+
});
|
|
10833
|
+
} catch (error) {
|
|
10834
|
+
if (!signal.aborted) {
|
|
10835
|
+
if (error instanceof TeamCredentialError) throw error;
|
|
10836
|
+
if (notVisible(error)) throw projectUnavailable();
|
|
10837
|
+
}
|
|
10838
|
+
logger?.warn({
|
|
10839
|
+
...safeErrorContext(error),
|
|
10840
|
+
teamId: target.teamId,
|
|
10841
|
+
...target.projectId ? { projectId: target.projectId } : {},
|
|
10842
|
+
...target.diaryId ? { diaryId: target.diaryId } : {},
|
|
10843
|
+
code: signal.aborted ? "agent_server_project_check_aborted" : "agent_server_project_check_failed"
|
|
10844
|
+
}, "AgentServer project check failed");
|
|
10845
|
+
throw checkUnavailable(error, signal.aborted);
|
|
10846
|
+
}
|
|
10847
|
+
const found = project;
|
|
10848
|
+
if (target.projectId && (!found || found.id !== target.projectId || found.teamId !== target.teamId || found.archived)) throw projectUnavailable();
|
|
10849
|
+
const foundDiary = diary;
|
|
10850
|
+
if (target.diaryId && (!foundDiary || foundDiary.id !== target.diaryId || foundDiary.teamId !== target.teamId)) throw new AgentServerHttpError(400, "diary_unavailable", "Choose a diary belonging to this team");
|
|
10851
|
+
return { project: found };
|
|
10852
|
+
}
|
|
10853
|
+
/** Null when the credential cannot see the resource; other failures propagate. */
|
|
10854
|
+
async function visibleOrNull(read) {
|
|
10855
|
+
try {
|
|
10856
|
+
return await read();
|
|
10857
|
+
} catch (error) {
|
|
10858
|
+
if (notVisible(error)) return null;
|
|
10859
|
+
throw error;
|
|
10860
|
+
}
|
|
10861
|
+
}
|
|
10862
|
+
/**
|
|
10863
|
+
* Settles when `work` does or `signal` aborts. The SDK's team, project and
|
|
10864
|
+
* diary reads take no signal, so an in-flight GET may finish in the background;
|
|
10865
|
+
* readers check the signal between steps so nothing further starts.
|
|
10866
|
+
*/
|
|
10867
|
+
async function untilAborted(signal, work) {
|
|
10868
|
+
signal.throwIfAborted();
|
|
10869
|
+
const aborted = new Promise((_, reject) => {
|
|
10870
|
+
signal.addEventListener("abort", () => {
|
|
10871
|
+
const reason = signal.reason;
|
|
10872
|
+
reject(reason instanceof Error ? reason : new Error(String(reason)));
|
|
10873
|
+
}, { once: true });
|
|
10874
|
+
});
|
|
10875
|
+
return Promise.race([work(), aborted]);
|
|
10876
|
+
}
|
|
10877
|
+
/** True only when `cause` is the abort itself, not a failure seen after it fired. */
|
|
10878
|
+
function causedByAbort(cause, signal) {
|
|
10879
|
+
if (!signal.aborted) return false;
|
|
10880
|
+
if (cause === signal.reason) return true;
|
|
10881
|
+
const error = cause;
|
|
10882
|
+
return error?.cause === signal.reason || error?.name === "AbortError" || error?.name === "TimeoutError";
|
|
10883
|
+
}
|
|
10884
|
+
function checkUnavailable(cause, timedOut) {
|
|
10885
|
+
return new AgentServerHttpError(503, "project_check_unavailable", timedOut ? "The request could not be completed in time, so nothing was changed. Retry in a moment." : "The server could not confirm this project. Retry in a moment.", { cause });
|
|
10886
|
+
}
|
|
10887
|
+
function projectUnavailable() {
|
|
10888
|
+
return new AgentServerHttpError(400, "project_unavailable", "Verify team access and choose an available project");
|
|
10889
|
+
}
|
|
10890
|
+
function notVisible(error) {
|
|
10891
|
+
return error instanceof MoltNetError && [
|
|
10892
|
+
401,
|
|
10893
|
+
403,
|
|
10894
|
+
404
|
|
10895
|
+
].includes(error.statusCode ?? 0);
|
|
10896
|
+
}
|
|
10897
|
+
//#endregion
|
|
10898
|
+
//#region src/lib/agent-server/protected-roots.ts
|
|
10899
|
+
/**
|
|
10900
|
+
* A worker holds agent and provider keys, so neither a run nor a saved
|
|
10901
|
+
* location may use a folder inside, or containing, the MoltNet store or its
|
|
10902
|
+
* secrets. Both sides are compared canonically.
|
|
10903
|
+
*/
|
|
10904
|
+
function isProtectedFolder(source, roots) {
|
|
10905
|
+
const folder = canonicalOrSelf(source);
|
|
10906
|
+
return roots.some((root) => {
|
|
10907
|
+
const protectedRoot = canonicalOrSelf(root);
|
|
10908
|
+
return within(folder, protectedRoot) || within(protectedRoot, folder);
|
|
10909
|
+
});
|
|
10910
|
+
}
|
|
10911
|
+
var PROTECTED_FOLDER_MESSAGE = "Choose a folder outside the MoltNet configuration store";
|
|
10912
|
+
function canonicalOrSelf(path) {
|
|
10913
|
+
try {
|
|
10914
|
+
return realpathSync.native(path);
|
|
10915
|
+
} catch {
|
|
10916
|
+
return path;
|
|
10917
|
+
}
|
|
10918
|
+
}
|
|
10919
|
+
function within(child, parent) {
|
|
10920
|
+
const suffix = relative(parent, child);
|
|
10921
|
+
return suffix === "" || !isAbsolute(suffix) && suffix !== ".." && !suffix.startsWith(`..${sep}`);
|
|
10922
|
+
}
|
|
10923
|
+
//#endregion
|
|
10924
|
+
//#region src/lib/agent-server/managed-project-selection.ts
|
|
10925
|
+
/** True when the start request names any project selection field. */
|
|
10926
|
+
function requestsProjectSelection(spec) {
|
|
10927
|
+
return spec.projectId !== void 0 || spec.location !== void 0 || spec.source !== void 0 || spec.strategy !== void 0;
|
|
10928
|
+
}
|
|
10929
|
+
/**
|
|
10930
|
+
* Resolve against the base store once; the worker receives only this captured
|
|
10931
|
+
* config. The request in `spec` is never modified: resolved values are
|
|
10932
|
+
* returned in `workspace` and `effective`, so a run can be replayed from what
|
|
10933
|
+
* the caller asked for.
|
|
10934
|
+
*/
|
|
10935
|
+
async function resolveManagedProjectSelection(options) {
|
|
10936
|
+
const { spec, root, cwd, apiUrl, client, signal } = options;
|
|
10937
|
+
if (spec.source !== void 0) {
|
|
10938
|
+
if (!isAbsolute(spec.source)) throw new ProjectConfigError("selection", "Select an absolute source folder");
|
|
10939
|
+
let canonical;
|
|
10940
|
+
try {
|
|
10941
|
+
canonical = await canonicalDirectory(spec.source);
|
|
10942
|
+
} catch (cause) {
|
|
10943
|
+
throw new ProjectConfigError("selection", "The selected folder is unavailable. Choose an existing folder.", { cause });
|
|
10944
|
+
}
|
|
10945
|
+
assertOutsideProtected(canonical, options.protectedRoots);
|
|
10946
|
+
}
|
|
10947
|
+
if (spec.projectId === null && spec.location) throw new ProjectConfigError("selection", "General work cannot also name a location");
|
|
10948
|
+
const selection = await resolveRunProjectSelection({
|
|
10949
|
+
agent: spec.agent,
|
|
10950
|
+
cwd,
|
|
10951
|
+
team: spec.teamId,
|
|
10952
|
+
apiUrl,
|
|
10953
|
+
"config-file": getProjectConfigPath({ root }),
|
|
10954
|
+
general: !spec.projectId && !spec.location,
|
|
10955
|
+
project: spec.projectId ?? void 0,
|
|
10956
|
+
binding: spec.location,
|
|
10957
|
+
source: spec.source,
|
|
10958
|
+
"workspace-strategy": spec.strategy
|
|
10959
|
+
}, {
|
|
10960
|
+
signal,
|
|
10961
|
+
guardSource: (source) => assertOutsideProtected(source, options.protectedRoots)
|
|
10962
|
+
});
|
|
10963
|
+
const chosenSource = selection.workspaceExplicit ? selection.source : void 0;
|
|
10964
|
+
const reader = {
|
|
10965
|
+
readProject: (teamId, projectId, readSignal) => {
|
|
10966
|
+
readSignal.throwIfAborted();
|
|
10967
|
+
return visibleOrNull(() => client.projects.get(projectId, { teamId }));
|
|
10968
|
+
},
|
|
10969
|
+
readDiary: (teamId, diaryId, readSignal) => {
|
|
10970
|
+
readSignal.throwIfAborted();
|
|
10971
|
+
return visibleOrNull(() => client.diaries.get(diaryId, { teamId }));
|
|
10972
|
+
}
|
|
10973
|
+
};
|
|
10974
|
+
const check = {
|
|
10975
|
+
signal,
|
|
10976
|
+
...options.logger ? { logger: options.logger } : {}
|
|
10977
|
+
};
|
|
10978
|
+
const { project } = await verifyProjectTarget(reader, {
|
|
10979
|
+
teamId: spec.teamId,
|
|
10980
|
+
projectId: selection.projectId
|
|
10981
|
+
}, check);
|
|
10982
|
+
const diaryId = spec.diaryId ?? selection.binding?.diaryId ?? project?.defaultDiaryId ?? (selection.projectId === null ? await options.generalDefaultDiary?.() : void 0) ?? void 0;
|
|
10983
|
+
if (diaryId) await verifyProjectTarget(reader, {
|
|
10984
|
+
teamId: spec.teamId,
|
|
10985
|
+
projectId: null,
|
|
10986
|
+
diaryId
|
|
10987
|
+
}, check);
|
|
10988
|
+
const resolvedBinding = selection.binding ? {
|
|
10989
|
+
...selection.binding,
|
|
10990
|
+
...diaryId ? { diaryId } : {}
|
|
10991
|
+
} : void 0;
|
|
10992
|
+
const config = {
|
|
10993
|
+
version: 1,
|
|
10994
|
+
bindings: resolvedBinding ? [resolvedBinding] : []
|
|
10995
|
+
};
|
|
10996
|
+
const workspace = {
|
|
10997
|
+
projectId: selection.projectId,
|
|
10998
|
+
...resolvedBinding ? { location: resolvedBinding.name } : {},
|
|
10999
|
+
...diaryId ? { diaryId } : {},
|
|
11000
|
+
...chosenSource ? { source: chosenSource } : {},
|
|
11001
|
+
strategy: selection.workspaceExplicit ? selection.strategy : PROFILE_DEFAULT_STRATEGY
|
|
11002
|
+
};
|
|
11003
|
+
return {
|
|
11004
|
+
selection: {
|
|
11005
|
+
...selection,
|
|
11006
|
+
binding: resolvedBinding
|
|
11007
|
+
},
|
|
11008
|
+
workspace,
|
|
11009
|
+
config,
|
|
11010
|
+
effective: {
|
|
11011
|
+
...spec,
|
|
11012
|
+
projectId: selection.projectId,
|
|
11013
|
+
...diaryId ? { diaryId } : {}
|
|
11014
|
+
}
|
|
11015
|
+
};
|
|
11016
|
+
}
|
|
11017
|
+
function assertOutsideProtected(source, roots) {
|
|
11018
|
+
if (isProtectedFolder(source, roots)) throw new ProjectConfigError("selection", PROTECTED_FOLDER_MESSAGE);
|
|
11019
|
+
}
|
|
11020
|
+
//#endregion
|
|
10552
11021
|
//#region src/lib/agent-server/runs.ts
|
|
10553
11022
|
var STOP_GRACE_MS = 1e4;
|
|
10554
11023
|
var STOP_FORCE_MS = 2e3;
|
|
@@ -10637,7 +11106,7 @@ var RunManager = class {
|
|
|
10637
11106
|
};
|
|
10638
11107
|
}
|
|
10639
11108
|
/** Assemble child env + args for a run. Exposed for tests. */
|
|
10640
|
-
async prepare(spec, agent, piDir, providers = this.store.readProviders(), runtimeModule) {
|
|
11109
|
+
async prepare(spec, agent, piDir, providers = this.store.readProviders(), runtimeModule, workspace) {
|
|
10641
11110
|
const { activation, config } = agent;
|
|
10642
11111
|
const homeDir = join(dirname(piDir), "home");
|
|
10643
11112
|
const env = {
|
|
@@ -10682,7 +11151,16 @@ var RunManager = class {
|
|
|
10682
11151
|
String(runtimeSettings.heartbeatIntervalMs),
|
|
10683
11152
|
"--warm-retention-sec",
|
|
10684
11153
|
String(runtimeSettings.warmRetentionSec),
|
|
10685
|
-
...target.extraArgs
|
|
11154
|
+
...target.extraArgs,
|
|
11155
|
+
...workspace ? projectRunArgs({
|
|
11156
|
+
"config-file": workspace.configPath,
|
|
11157
|
+
...workspace.location ? { binding: workspace.location } : { general: true },
|
|
11158
|
+
...workspace.strategy === "profile-default" ? {} : {
|
|
11159
|
+
source: workspace.source,
|
|
11160
|
+
"workspace-strategy": workspace.strategy
|
|
11161
|
+
},
|
|
11162
|
+
"state-dir": this.runStateDir(spec, workspace)
|
|
11163
|
+
}) : []
|
|
10686
11164
|
];
|
|
10687
11165
|
env["MOLTNET_AGENT_KEY"] = requireCredentialSnapshot(agent).agentKey;
|
|
10688
11166
|
env["MOLTNET_API_URL"] = activation.apiUrl ?? (activation.source === "external" ? activation.configApiUrl : "");
|
|
@@ -10713,10 +11191,11 @@ var RunManager = class {
|
|
|
10713
11191
|
return {
|
|
10714
11192
|
args,
|
|
10715
11193
|
env,
|
|
10716
|
-
cwd: target.cwd
|
|
11194
|
+
cwd: workspace?.source ?? (workspace ? join(dirname(piDir), "workspace") : target.cwd)
|
|
10717
11195
|
};
|
|
10718
11196
|
}
|
|
10719
11197
|
async start(spec, signal) {
|
|
11198
|
+
spec = structuredClone(spec);
|
|
10720
11199
|
validateRunSpec(spec);
|
|
10721
11200
|
const releaseStart = this.reserveStart(spec.agent);
|
|
10722
11201
|
try {
|
|
@@ -10727,24 +11206,65 @@ var RunManager = class {
|
|
|
10727
11206
|
}
|
|
10728
11207
|
async startReserved(spec, signal) {
|
|
10729
11208
|
this.assertStartOpen(signal);
|
|
10730
|
-
const
|
|
11209
|
+
const deadline = AbortSignal.any([...signal ? [signal] : [], AbortSignal.timeout(this.options.startTimeoutMs ?? 1e4)]);
|
|
11210
|
+
const verify = this.options.verifyActivationImpl ?? verifyTeamActivation;
|
|
11211
|
+
const agent = await untilAborted(deadline, () => verify(this.store, spec.agent, this.options.secretProviders, this.options.externalSecretProviders, void 0, deadline, spec.teamId)).catch((cause) => {
|
|
11212
|
+
this.assertStartOpen(signal, deadline, cause);
|
|
10731
11213
|
if (cause instanceof TeamCredentialError || cause instanceof AgentServerStoreError) throw cause;
|
|
10732
11214
|
throw new AgentServerIdentityError("verification_failed", `Cannot start agent "${spec.agent}" for team "${spec.teamId}": credential verification failed. Check the selected team key and activation.`);
|
|
10733
11215
|
});
|
|
10734
|
-
this.assertStartOpen(signal);
|
|
11216
|
+
this.assertStartOpen(signal, deadline);
|
|
10735
11217
|
if (agent.boundTeamId && agent.boundTeamId !== spec.teamId) throw new AgentServerRunError("invalid_spec", `agent "${spec.agent}" has a key bound to team ${agent.boundTeamId}; start the run with that team, or create a new agent with an enrollment token from team ${spec.teamId}`);
|
|
10736
11218
|
const id = `${Date.now().toString(36)}-${randomBytes(4).toString("hex")}`;
|
|
10737
|
-
const runDir = this.store.runDir(id);
|
|
11219
|
+
const runDir = resolve(this.store.runDir(id));
|
|
10738
11220
|
const piDir = join(runDir, "pi");
|
|
10739
|
-
const providers = this.store.readProviders();
|
|
10740
|
-
const runtimeModule = this.options.resolveRuntimeModule ? await this.options.resolveRuntimeModule(spec, agent, dirname(piDir)) : this.options.runtimeRegistry ? await this.resolveRuntimeModule(spec, agent, dirname(piDir)) : void 0;
|
|
10741
|
-
const { args, env, cwd } = await this.prepare(spec, agent, piDir, providers, runtimeModule);
|
|
10742
|
-
this.assertStartOpen(signal);
|
|
10743
11221
|
let child;
|
|
10744
11222
|
let logStream;
|
|
10745
11223
|
let logLimiter;
|
|
11224
|
+
let workspace;
|
|
11225
|
+
let effective = spec;
|
|
11226
|
+
let selection;
|
|
10746
11227
|
try {
|
|
10747
11228
|
const { logPath } = this.store.createRunDir(id);
|
|
11229
|
+
const executionDir = join(runDir, "workspace");
|
|
11230
|
+
if (requestsProjectSelection(spec)) {
|
|
11231
|
+
mkdirSync(executionDir, {
|
|
11232
|
+
recursive: true,
|
|
11233
|
+
mode: 448
|
|
11234
|
+
});
|
|
11235
|
+
const projectRoot = this.options.projectRoot ?? this.store.root;
|
|
11236
|
+
const resolved = await resolveManagedProjectSelection({
|
|
11237
|
+
spec,
|
|
11238
|
+
root: projectRoot,
|
|
11239
|
+
cwd: executionDir,
|
|
11240
|
+
apiUrl: agent.activation.apiUrl ?? agent.config.endpoints.api,
|
|
11241
|
+
client: requireCredentialSnapshot(agent).client,
|
|
11242
|
+
protectedRoots: [
|
|
11243
|
+
projectRoot,
|
|
11244
|
+
this.store.root,
|
|
11245
|
+
this.store.secretsDir
|
|
11246
|
+
],
|
|
11247
|
+
signal: deadline,
|
|
11248
|
+
logger: { warn: (context, message) => this.log("warn", message, context) },
|
|
11249
|
+
generalDefaultDiary: () => this.generalDefaultDiary(spec, agent, deadline)
|
|
11250
|
+
});
|
|
11251
|
+
workspace = {
|
|
11252
|
+
...resolved.workspace,
|
|
11253
|
+
configPath: join(runDir, "projects.json")
|
|
11254
|
+
};
|
|
11255
|
+
effective = resolved.effective;
|
|
11256
|
+
selection = resolved.selection;
|
|
11257
|
+
writeRunSnapshot(workspace.configPath, resolved.config);
|
|
11258
|
+
mkdirSync(this.runStateDir(spec, workspace), {
|
|
11259
|
+
recursive: true,
|
|
11260
|
+
mode: 448
|
|
11261
|
+
});
|
|
11262
|
+
}
|
|
11263
|
+
const providers = this.store.readProviders();
|
|
11264
|
+
const executionCwd = workspace ? workspace.source ?? executionDir : dirname(piDir);
|
|
11265
|
+
const runtimeModule = await untilAborted(deadline, async () => this.options.resolveRuntimeModule ? this.options.resolveRuntimeModule(effective, agent, executionCwd) : this.options.runtimeRegistry ? this.resolveRuntimeModule(effective, agent, executionCwd, selection) : void 0);
|
|
11266
|
+
const { args, env, cwd } = await this.prepare(effective, agent, piDir, providers, runtimeModule, workspace);
|
|
11267
|
+
this.assertStartOpen(signal, deadline);
|
|
10748
11268
|
for (const dir of [
|
|
10749
11269
|
env.HOME,
|
|
10750
11270
|
env.XDG_CACHE_HOME,
|
|
@@ -10775,7 +11295,7 @@ var RunManager = class {
|
|
|
10775
11295
|
child?.kill("SIGKILL");
|
|
10776
11296
|
});
|
|
10777
11297
|
const spawnImpl = this.options.spawnImpl ?? spawn;
|
|
10778
|
-
this.assertStartOpen(signal);
|
|
11298
|
+
this.assertStartOpen(signal, deadline);
|
|
10779
11299
|
child = spawnImpl(entry.execPath, [
|
|
10780
11300
|
...entry.execArgv,
|
|
10781
11301
|
entry.scriptPath,
|
|
@@ -10803,6 +11323,7 @@ var RunManager = class {
|
|
|
10803
11323
|
child.stderr?.pipe(logLimiter, { end: false });
|
|
10804
11324
|
const record = {
|
|
10805
11325
|
...spec,
|
|
11326
|
+
...workspace ? { workspace } : {},
|
|
10806
11327
|
id,
|
|
10807
11328
|
status: "running",
|
|
10808
11329
|
pid: child.pid,
|
|
@@ -10845,6 +11366,7 @@ var RunManager = class {
|
|
|
10845
11366
|
this.store.writeRun(record);
|
|
10846
11367
|
this.log("info", "agent server run started", {
|
|
10847
11368
|
...runContext(id, spec.agent, child),
|
|
11369
|
+
...selectionContext(spec, workspace),
|
|
10848
11370
|
transition: "running"
|
|
10849
11371
|
});
|
|
10850
11372
|
return record;
|
|
@@ -10859,20 +11381,29 @@ var RunManager = class {
|
|
|
10859
11381
|
});
|
|
10860
11382
|
this.log("error", "agent server run failed to start", {
|
|
10861
11383
|
...runContext(id, spec.agent, child),
|
|
11384
|
+
...selectionContext(spec, workspace),
|
|
10862
11385
|
transition: "start_failed",
|
|
10863
11386
|
...safeRunError(cause)
|
|
10864
11387
|
});
|
|
11388
|
+
if (!signal?.aborted && !this.closing && !(cause instanceof AgentServerHttpError) && causedByAbort(cause, deadline)) throw startTimedOut(cause);
|
|
10865
11389
|
throw cause;
|
|
10866
11390
|
}
|
|
10867
11391
|
}
|
|
10868
|
-
async resolveRuntimeModule(spec, activated, cwd) {
|
|
10869
|
-
const
|
|
11392
|
+
async resolveRuntimeModule(spec, activated, cwd, selection) {
|
|
11393
|
+
const effectiveProfiles = (await resolveRuntimeProfiles({
|
|
10870
11394
|
agent: await this.connectAgent(activated, spec.teamId),
|
|
10871
11395
|
profiles: spec.profiles,
|
|
10872
11396
|
teamId: spec.teamId,
|
|
10873
11397
|
cwd
|
|
11398
|
+
})).map((profile) => {
|
|
11399
|
+
if (!selection) return profile;
|
|
11400
|
+
try {
|
|
11401
|
+
return applyProjectWorkspacePolicy(profile, selection);
|
|
11402
|
+
} catch (error) {
|
|
11403
|
+
throw new AgentServerRunError("invalid_spec", error instanceof Error ? error.message : "This profile does not support the selected workspace strategy");
|
|
11404
|
+
}
|
|
10874
11405
|
});
|
|
10875
|
-
const kinds = [...new Set(
|
|
11406
|
+
const kinds = [...new Set(effectiveProfiles.map((profile) => profile.runtimeKind))];
|
|
10876
11407
|
if (kinds.length !== 1) throw new AgentServerRunError("invalid_spec", "All profiles in a server-managed run must use the same runtime kind.");
|
|
10877
11408
|
const kind = kinds[0];
|
|
10878
11409
|
let registration;
|
|
@@ -11017,8 +11548,10 @@ var RunManager = class {
|
|
|
11017
11548
|
else this.startingByAgent.delete(agent);
|
|
11018
11549
|
};
|
|
11019
11550
|
}
|
|
11020
|
-
|
|
11551
|
+
/** Shutdown or disconnect wins; otherwise an expired budget is a retryable 503. */
|
|
11552
|
+
assertStartOpen(signal, deadline, cause) {
|
|
11021
11553
|
if (this.closing || signal?.aborted) throw new AgentServerRunError("invalid_spec", "agent server is shutting down");
|
|
11554
|
+
if (deadline?.aborted && (cause === void 0 || causedByAbort(cause, deadline))) throw startTimedOut(cause);
|
|
11022
11555
|
}
|
|
11023
11556
|
pruneCompletedRuns() {
|
|
11024
11557
|
const removed = this.store.pruneCompletedRuns({
|
|
@@ -11050,10 +11583,40 @@ var RunManager = class {
|
|
|
11050
11583
|
});
|
|
11051
11584
|
}
|
|
11052
11585
|
}
|
|
11053
|
-
|
|
11054
|
-
|
|
11055
|
-
|
|
11056
|
-
|
|
11586
|
+
/**
|
|
11587
|
+
* Same rule as the catalogue. Best effort: without it the worker resolves its
|
|
11588
|
+
* own diary, as it did before project selection existed.
|
|
11589
|
+
*/
|
|
11590
|
+
async generalDefaultDiary(spec, agent, deadline) {
|
|
11591
|
+
try {
|
|
11592
|
+
const { items } = await untilAborted(deadline, () => requireCredentialSnapshot(agent).client.diaries.list());
|
|
11593
|
+
return resolveDefaultDiary(spec.teamId, items.filter((diary) => diary.teamId === spec.teamId), readIdentityDefaultBinding(this.store.identityDir(spec.agent))) ?? void 0;
|
|
11594
|
+
} catch (error) {
|
|
11595
|
+
this.log("warn", "agent server default diary lookup failed", {
|
|
11596
|
+
...safeRunError(error),
|
|
11597
|
+
agent: spec.agent,
|
|
11598
|
+
teamId: spec.teamId
|
|
11599
|
+
});
|
|
11600
|
+
return;
|
|
11601
|
+
}
|
|
11602
|
+
}
|
|
11603
|
+
/**
|
|
11604
|
+
* Stable across runs, so retries and continuations find their execution-plan
|
|
11605
|
+
* cache and task workspaces. Keyed by agent and location, under the store,
|
|
11606
|
+
* never inside the user's folder.
|
|
11607
|
+
*/
|
|
11608
|
+
runStateDir(spec, workspace) {
|
|
11609
|
+
const scope = createHash("sha256").update(JSON.stringify([
|
|
11610
|
+
spec.teamId,
|
|
11611
|
+
workspace.projectId,
|
|
11612
|
+
workspace.source ?? null
|
|
11613
|
+
])).digest("hex").slice(0, 12);
|
|
11614
|
+
return join(this.store.root, "run-state", spec.agent, `${workspace.location ? locationSegment(workspace.location) : "general"}-${scope}`);
|
|
11615
|
+
}
|
|
11616
|
+
log(level, message, context) {
|
|
11617
|
+
this.options.logger?.[level](context, message);
|
|
11618
|
+
}
|
|
11619
|
+
};
|
|
11057
11620
|
function createByteLimitTransform(limit, onTruncated) {
|
|
11058
11621
|
const byteLimit = Math.max(0, Math.floor(limit));
|
|
11059
11622
|
const contentLimit = Math.max(0, byteLimit - LOG_TRUNCATION_MARKER.length);
|
|
@@ -11077,6 +11640,33 @@ function createByteLimitTransform(limit, onTruncated) {
|
|
|
11077
11640
|
callback();
|
|
11078
11641
|
} });
|
|
11079
11642
|
}
|
|
11643
|
+
/** Non-secret ids that tie a run's logs to its project selection. */
|
|
11644
|
+
function selectionContext(spec, workspace) {
|
|
11645
|
+
return {
|
|
11646
|
+
...spec.projectId !== void 0 ? { requestedProjectId: spec.projectId } : {},
|
|
11647
|
+
...spec.location ? { requestedLocation: spec.location } : {},
|
|
11648
|
+
...workspace ? {
|
|
11649
|
+
projectId: workspace.projectId,
|
|
11650
|
+
...workspace.location ? { location: workspace.location } : {},
|
|
11651
|
+
...workspace.diaryId ? { diaryId: workspace.diaryId } : {},
|
|
11652
|
+
strategy: workspace.strategy
|
|
11653
|
+
} : {}
|
|
11654
|
+
};
|
|
11655
|
+
}
|
|
11656
|
+
/** Readable when it is a safe path segment; hashed otherwise. */
|
|
11657
|
+
function locationSegment(name) {
|
|
11658
|
+
return /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/u.test(name) ? `location-${name}` : `location-${createHash("sha256").update(name).digest("hex").slice(0, 16)}`;
|
|
11659
|
+
}
|
|
11660
|
+
function startTimedOut(cause) {
|
|
11661
|
+
return new AgentServerHttpError(503, "start_timeout", "The run could not be prepared in time, so it was not started. Retry in a moment.", { cause });
|
|
11662
|
+
}
|
|
11663
|
+
/** Created exclusively and owner-only: a run never reuses or widens a snapshot. */
|
|
11664
|
+
function writeRunSnapshot(path, config) {
|
|
11665
|
+
writeFileSync(path, JSON.stringify(config), {
|
|
11666
|
+
mode: 384,
|
|
11667
|
+
flag: "wx"
|
|
11668
|
+
});
|
|
11669
|
+
}
|
|
11080
11670
|
function runContext(runId, agent, child) {
|
|
11081
11671
|
return {
|
|
11082
11672
|
runId,
|
|
@@ -11304,112 +11894,6 @@ function createAgentServerSecretProviders(settings, store) {
|
|
|
11304
11894
|
};
|
|
11305
11895
|
}
|
|
11306
11896
|
//#endregion
|
|
11307
|
-
//#region src/lib/agent-server/readiness.ts
|
|
11308
|
-
/**
|
|
11309
|
-
* Whether a runtime profile can execute on *this* machine.
|
|
11310
|
-
*
|
|
11311
|
-
* A profile is authored in Console against a team; whether it can run depends
|
|
11312
|
-
* on local facts only the server knows — which provider keys are configured
|
|
11313
|
-
* here and which runtime kinds this machine can produce.
|
|
11314
|
-
*
|
|
11315
|
-
* The prerequisite comparison itself is **not** reimplemented here. Run start
|
|
11316
|
-
* calls `validateRuntimeProfilePrerequisites`, and the catalogue calls the same
|
|
11317
|
-
* function, so the composer cannot promise a run that startup would reject for
|
|
11318
|
-
* a reason the two evaluated differently.
|
|
11319
|
-
*/
|
|
11320
|
-
function deriveProfileReadiness(profile, machine) {
|
|
11321
|
-
const blockers = [];
|
|
11322
|
-
const env = {};
|
|
11323
|
-
for (const [name, configured] of machine.providerEnv) if (configured) env[name] = "configured";
|
|
11324
|
-
try {
|
|
11325
|
-
validateRuntimeProfilePrerequisites(profile, env, {
|
|
11326
|
-
tools: machine.inventory?.tools ?? profile.requiredTools,
|
|
11327
|
-
executables: machine.inventory?.executables ?? profile.requiredExecutables
|
|
11328
|
-
});
|
|
11329
|
-
} catch (error) {
|
|
11330
|
-
if (!(error instanceof RuntimeProfilePrerequisiteError)) throw error;
|
|
11331
|
-
for (const name of error.missingEnv) blockers.push({
|
|
11332
|
-
code: "env_missing",
|
|
11333
|
-
message: `${name} is not configured on this machine.`,
|
|
11334
|
-
remedy: "Add the key under Providers, then reopen this run."
|
|
11335
|
-
});
|
|
11336
|
-
for (const name of error.missingTools) blockers.push({
|
|
11337
|
-
code: "tool_missing",
|
|
11338
|
-
message: `The runtime does not provide the tool ${name}.`,
|
|
11339
|
-
remedy: `Use a profile whose runtime provides ${name}, or change the profile in Console.`
|
|
11340
|
-
});
|
|
11341
|
-
for (const name of error.missingExecutables) blockers.push({
|
|
11342
|
-
code: "executable_missing",
|
|
11343
|
-
message: `The runtime does not provide the executable ${name}.`,
|
|
11344
|
-
remedy: `Use a runtime that ships ${name}, or drop the requirement in Console.`
|
|
11345
|
-
});
|
|
11346
|
-
}
|
|
11347
|
-
if (!machine.runtimeKinds.has(profile.runtimeKind)) blockers.push({
|
|
11348
|
-
code: "runtime_unregistered",
|
|
11349
|
-
message: `Runtime kind ${profile.runtimeKind} is not available on this machine.`,
|
|
11350
|
-
remedy: "Register the runtime under Runtimes, then reopen this run."
|
|
11351
|
-
});
|
|
11352
|
-
return {
|
|
11353
|
-
ready: blockers.length === 0,
|
|
11354
|
-
blockers
|
|
11355
|
-
};
|
|
11356
|
-
}
|
|
11357
|
-
//#endregion
|
|
11358
|
-
//#region src/lib/agent-server/catalogue.ts
|
|
11359
|
-
async function buildCatalogue(options) {
|
|
11360
|
-
const { agent, machine, identityDefault } = options;
|
|
11361
|
-
const entries = await Promise.all(agent.teamIds.map(async (teamId) => {
|
|
11362
|
-
try {
|
|
11363
|
-
const result = await agent.readTeam(teamId);
|
|
11364
|
-
if (result.team.id !== teamId) throw new Error("Team response mismatch");
|
|
11365
|
-
const diaries = result.diaries.filter((diary) => diary.teamId === teamId).map(({ id, name }) => ({
|
|
11366
|
-
id,
|
|
11367
|
-
name
|
|
11368
|
-
}));
|
|
11369
|
-
return {
|
|
11370
|
-
team: {
|
|
11371
|
-
teamId,
|
|
11372
|
-
teamName: result.team.name,
|
|
11373
|
-
available: true,
|
|
11374
|
-
blockers: [],
|
|
11375
|
-
credential: result.credential,
|
|
11376
|
-
diaries,
|
|
11377
|
-
defaultDiaryId: resolveDefaultDiary(teamId, diaries, identityDefault)
|
|
11378
|
-
},
|
|
11379
|
-
profiles: result.profiles.filter((profile) => profile.teamId === teamId).map((profile) => ({
|
|
11380
|
-
...profile,
|
|
11381
|
-
...deriveProfileReadiness(profile, machine)
|
|
11382
|
-
}))
|
|
11383
|
-
};
|
|
11384
|
-
} catch (error) {
|
|
11385
|
-
return {
|
|
11386
|
-
team: {
|
|
11387
|
-
teamId,
|
|
11388
|
-
teamName: teamId,
|
|
11389
|
-
available: false,
|
|
11390
|
-
blockers: [credentialBlocker(error)],
|
|
11391
|
-
credential: agent.lastVerified(teamId),
|
|
11392
|
-
diaries: [],
|
|
11393
|
-
defaultDiaryId: null
|
|
11394
|
-
},
|
|
11395
|
-
profiles: []
|
|
11396
|
-
};
|
|
11397
|
-
}
|
|
11398
|
-
}));
|
|
11399
|
-
const teams = entries.map(({ team }) => team);
|
|
11400
|
-
const available = teams.filter((team) => team.available);
|
|
11401
|
-
return {
|
|
11402
|
-
teams,
|
|
11403
|
-
defaultTeamId: available.find((team) => team.teamId === identityDefault.teamId)?.teamId ?? available[0]?.teamId ?? null,
|
|
11404
|
-
profiles: entries.flatMap((entry) => entry.profiles)
|
|
11405
|
-
};
|
|
11406
|
-
}
|
|
11407
|
-
function resolveDefaultDiary(teamId, diaries, identityDefault) {
|
|
11408
|
-
const bound = diaries.find((diary) => diary.id === identityDefault.diaryId);
|
|
11409
|
-
if (bound && identityDefault.teamId === teamId) return bound.id;
|
|
11410
|
-
return diaries.length === 1 ? diaries[0]?.id ?? null : null;
|
|
11411
|
-
}
|
|
11412
|
-
//#endregion
|
|
11413
11897
|
//#region src/lib/agent-server/enrollment.ts
|
|
11414
11898
|
/** Native callers receive metadata only; approval and storage stay local. */
|
|
11415
11899
|
async function enrollIdentityTeam(options) {
|
|
@@ -11494,37 +11978,139 @@ async function enrollIdentityTeam(options) {
|
|
|
11494
11978
|
}
|
|
11495
11979
|
}
|
|
11496
11980
|
//#endregion
|
|
11497
|
-
//#region src/lib/agent-server/
|
|
11981
|
+
//#region src/lib/agent-server/project-bindings.ts
|
|
11982
|
+
/** Each check may spawn `git`; bound how many run at once. */
|
|
11983
|
+
var READINESS_CONCURRENCY = 4;
|
|
11984
|
+
var PREPARATION_COPY = {
|
|
11985
|
+
unsupported_strategy: "Isolated directory preparation is unavailable. Choose Work here or a Git worktree.",
|
|
11986
|
+
hooks_unavailable: "Preparation hooks are unavailable. Remove the hooks or choose another location."
|
|
11987
|
+
};
|
|
11988
|
+
/** Normalize with the Go-compatible grammar, reporting a rejection as a coded error. */
|
|
11989
|
+
function locationEndpoint(value) {
|
|
11990
|
+
try {
|
|
11991
|
+
return normalizeProjectEndpoint(value);
|
|
11992
|
+
} catch {
|
|
11993
|
+
throw new AgentServerHttpError(400, "endpoint_unsupported", "Local project locations need an HTTPS server endpoint; HTTP is allowed only on loopback");
|
|
11994
|
+
}
|
|
11995
|
+
}
|
|
11996
|
+
/** Machine registrations live at the base store, outside connection directories. */
|
|
11997
|
+
var LocalProjectBindings = class {
|
|
11998
|
+
root;
|
|
11999
|
+
path;
|
|
12000
|
+
apiUrl;
|
|
12001
|
+
/** `protectedRoots`: store and secrets directories no location may use. */
|
|
12002
|
+
constructor(root, apiUrl, protectedRoots = []) {
|
|
12003
|
+
this.protectedRoots = protectedRoots;
|
|
12004
|
+
this.root = resolveStoreRoot({ root });
|
|
12005
|
+
this.path = getProjectConfigPath({ root: this.root });
|
|
12006
|
+
this.apiUrl = locationEndpoint(apiUrl);
|
|
12007
|
+
}
|
|
12008
|
+
async list() {
|
|
12009
|
+
const bindings = (await stored(() => readProjectConfig(this.path))).bindings.filter((binding) => normalizeProjectEndpoint(binding.apiUrl) === this.apiUrl);
|
|
12010
|
+
const locations = [];
|
|
12011
|
+
for (let i = 0; i < bindings.length; i += READINESS_CONCURRENCY) locations.push(...await Promise.all(bindings.slice(i, i + READINESS_CONCURRENCY).map((binding) => this.describe(binding))));
|
|
12012
|
+
return locations;
|
|
12013
|
+
}
|
|
12014
|
+
/** Once `signal` aborts, nothing is written, not even after the lock is taken. */
|
|
12015
|
+
async save(value, options = {}) {
|
|
12016
|
+
const input = structuredClone(value);
|
|
12017
|
+
validateProjectConfig({
|
|
12018
|
+
version: 1,
|
|
12019
|
+
bindings: [input]
|
|
12020
|
+
});
|
|
12021
|
+
if (locationEndpoint(input.apiUrl) !== this.apiUrl) throw new AgentServerHttpError(400, "endpoint_mismatch", "Location belongs to another endpoint");
|
|
12022
|
+
if (input.source && !isAbsolute(input.source)) throw new ProjectConfigError("validation", "Select an absolute source folder");
|
|
12023
|
+
const location = await this.describe(input, options.signal);
|
|
12024
|
+
if (!location.readiness.ready) throw new AgentServerHttpError(400, location.readiness.code ?? "location_unavailable", location.readiness.message ?? "Location is unavailable");
|
|
12025
|
+
const binding = {
|
|
12026
|
+
...input,
|
|
12027
|
+
apiUrl: this.apiUrl,
|
|
12028
|
+
...location.effectiveSource ? { source: location.effectiveSource } : {}
|
|
12029
|
+
};
|
|
12030
|
+
options.signal?.throwIfAborted();
|
|
12031
|
+
await this.update((config) => {
|
|
12032
|
+
options.signal?.throwIfAborted();
|
|
12033
|
+
const index = config.bindings.findIndex((entry) => entry.name === binding.name);
|
|
12034
|
+
const previous = config.bindings[index];
|
|
12035
|
+
if (previous && normalizeProjectEndpoint(previous.apiUrl) !== this.apiUrl) throw conflict("This name belongs to another endpoint");
|
|
12036
|
+
if (previous && hasPreparationHooks(previous.hooks)) throw conflict("Remove preparation hooks from the configuration before editing this location");
|
|
12037
|
+
if (previous && (previous.teamId !== binding.teamId || previous.projectId !== binding.projectId)) throw conflict("This name belongs to another project");
|
|
12038
|
+
if (binding.default) {
|
|
12039
|
+
for (const entry of config.bindings) if (normalizeProjectEndpoint(entry.apiUrl) === this.apiUrl && entry.teamId === binding.teamId && entry.projectId === binding.projectId) entry.default = false;
|
|
12040
|
+
}
|
|
12041
|
+
if (index < 0) config.bindings.push(binding);
|
|
12042
|
+
else config.bindings[index] = binding;
|
|
12043
|
+
});
|
|
12044
|
+
return this.describe(binding);
|
|
12045
|
+
}
|
|
12046
|
+
async remove(name) {
|
|
12047
|
+
await this.update((config) => {
|
|
12048
|
+
const index = config.bindings.findIndex((entry) => entry.name === name && normalizeProjectEndpoint(entry.apiUrl) === this.apiUrl);
|
|
12049
|
+
if (index < 0) throw new AgentServerHttpError(404, "location_not_found", "Location not found");
|
|
12050
|
+
config.bindings.splice(index, 1);
|
|
12051
|
+
});
|
|
12052
|
+
}
|
|
12053
|
+
update(mutate) {
|
|
12054
|
+
return stored(() => updateProjectConfig(this.path, mutate));
|
|
12055
|
+
}
|
|
12056
|
+
async describe(binding, signal) {
|
|
12057
|
+
const { effectiveSource, readiness } = await this.readiness(binding, signal);
|
|
12058
|
+
return {
|
|
12059
|
+
...binding,
|
|
12060
|
+
effectiveSource,
|
|
12061
|
+
readiness
|
|
12062
|
+
};
|
|
12063
|
+
}
|
|
12064
|
+
async readiness(binding, signal) {
|
|
12065
|
+
const declared = binding.source ? resolve(this.root, binding.source) : null;
|
|
12066
|
+
const unavailable = (code, message, effectiveSource = declared) => ({
|
|
12067
|
+
effectiveSource,
|
|
12068
|
+
readiness: {
|
|
12069
|
+
ready: false,
|
|
12070
|
+
code,
|
|
12071
|
+
message
|
|
12072
|
+
}
|
|
12073
|
+
});
|
|
12074
|
+
const blocker = preparationBlocker(binding.strategy, binding.hooks);
|
|
12075
|
+
if (blocker) return unavailable(blocker.code, PREPARATION_COPY[blocker.code]);
|
|
12076
|
+
if (binding.strategy === "none") return {
|
|
12077
|
+
effectiveSource: declared,
|
|
12078
|
+
readiness: { ready: true }
|
|
12079
|
+
};
|
|
12080
|
+
if (!declared) return unavailable("folder_missing", "Choose an existing source folder.");
|
|
12081
|
+
let source;
|
|
12082
|
+
try {
|
|
12083
|
+
source = await canonicalDirectory(declared);
|
|
12084
|
+
} catch {
|
|
12085
|
+
return unavailable("folder_missing", "The source folder is unavailable. Choose an existing folder.");
|
|
12086
|
+
}
|
|
12087
|
+
if (isProtectedFolder(source, this.protectedRoots)) return unavailable("folder_protected", `${PROTECTED_FOLDER_MESSAGE}.`, source);
|
|
12088
|
+
if (binding.strategy === "git-worktree") try {
|
|
12089
|
+
await validateGitSource(source, signal);
|
|
12090
|
+
} catch (cause) {
|
|
12091
|
+
if (signal?.aborted) throw cause;
|
|
12092
|
+
return unavailable("git_unavailable", "Choose a Git repository root with a committed revision, or choose Work here.", source);
|
|
12093
|
+
}
|
|
12094
|
+
return {
|
|
12095
|
+
effectiveSource: source,
|
|
12096
|
+
readiness: { ready: true }
|
|
12097
|
+
};
|
|
12098
|
+
}
|
|
12099
|
+
};
|
|
11498
12100
|
/**
|
|
11499
|
-
*
|
|
11500
|
-
*
|
|
11501
|
-
* This mirrors the Go CLI's `identityDefaultBinding` (`project_selection.go`),
|
|
11502
|
-
* which is the fallback the CLI uses when a working directory has no
|
|
11503
|
-
* registered project binding.
|
|
11504
|
-
*
|
|
11505
|
-
* The desktop cannot use the CLI's *location* bindings at all — its composer
|
|
11506
|
-
* has no working directory to key on — but it can honour this identity-wide
|
|
11507
|
-
* default, so an operator sees their familiar team preselected rather than an
|
|
11508
|
-
* arbitrary first entry.
|
|
11509
|
-
*
|
|
11510
|
-
* Like the CLI, a half-filled pair is treated as no binding: a team without a
|
|
11511
|
-
* diary is ignored.
|
|
12101
|
+
* A stored file that fails validation is server-side state, not a bad request.
|
|
12102
|
+
* The detail names a local path, so it goes to the logs via `cause`.
|
|
11512
12103
|
*/
|
|
11513
|
-
function
|
|
11514
|
-
let contents;
|
|
12104
|
+
async function stored(work) {
|
|
11515
12105
|
try {
|
|
11516
|
-
|
|
11517
|
-
} catch {
|
|
11518
|
-
|
|
12106
|
+
return await work();
|
|
12107
|
+
} catch (error) {
|
|
12108
|
+
if (error instanceof ProjectConfigError && error.kind === "validation") throw new AgentServerHttpError(500, "config_invalid", "The project locations file is invalid. Repair or remove it, then retry.", { cause: error });
|
|
12109
|
+
throw error;
|
|
11519
12110
|
}
|
|
11520
|
-
|
|
11521
|
-
|
|
11522
|
-
|
|
11523
|
-
if (!teamId || !diaryId) return {};
|
|
11524
|
-
return {
|
|
11525
|
-
teamId,
|
|
11526
|
-
diaryId
|
|
11527
|
-
};
|
|
12111
|
+
}
|
|
12112
|
+
function conflict(message) {
|
|
12113
|
+
return new AgentServerHttpError(409, "location_conflict", message);
|
|
11528
12114
|
}
|
|
11529
12115
|
//#endregion
|
|
11530
12116
|
//#region src/lib/agent-server/protocol.ts
|
|
@@ -11626,10 +12212,83 @@ var AgentServerCatalogueProfileSchema = Type.Intersect([Type.Pick(RuntimeProfile
|
|
|
11626
12212
|
var AgentServerCatalogueSchema = Type.Object({
|
|
11627
12213
|
teams: Type.Array(schemaRef(AgentServerCatalogueTeamSchema)),
|
|
11628
12214
|
defaultTeamId: Type.Union([Type.String(), Type.Null()]),
|
|
11629
|
-
profiles: Type.Array(schemaRef(AgentServerCatalogueProfileSchema))
|
|
12215
|
+
profiles: Type.Array(schemaRef(AgentServerCatalogueProfileSchema)),
|
|
12216
|
+
projects: Type.Array(Type.Pick(ProjectResponseSchema, [
|
|
12217
|
+
"id",
|
|
12218
|
+
"teamId",
|
|
12219
|
+
"name",
|
|
12220
|
+
"description",
|
|
12221
|
+
"defaultDiaryId",
|
|
12222
|
+
"archived"
|
|
12223
|
+
])),
|
|
12224
|
+
projectErrors: Type.Array(Type.Object({
|
|
12225
|
+
teamId: Type.String(),
|
|
12226
|
+
code: Type.Union([
|
|
12227
|
+
"forbidden",
|
|
12228
|
+
"unreachable",
|
|
12229
|
+
"invalid_response",
|
|
12230
|
+
"truncated"
|
|
12231
|
+
].map((value) => Type.Literal(value))),
|
|
12232
|
+
message: Type.String()
|
|
12233
|
+
}))
|
|
11630
12234
|
}, { $id: "AgentServerCatalogue" });
|
|
11631
12235
|
var CatalogueQuerySchema = Type.Object({ identity: Type.String({ minLength: 1 }) });
|
|
12236
|
+
var AgentServerProjectLocationSchema = Type.Object({
|
|
12237
|
+
name: Type.String({ minLength: 1 }),
|
|
12238
|
+
apiUrl: Type.String(),
|
|
12239
|
+
teamId: Type.String({ minLength: 1 }),
|
|
12240
|
+
projectId: Type.String({ minLength: 1 }),
|
|
12241
|
+
diaryId: Type.Optional(Type.String({ minLength: 1 })),
|
|
12242
|
+
source: Type.Optional(Type.String({ minLength: 1 })),
|
|
12243
|
+
strategy: Type.Union([
|
|
12244
|
+
"none",
|
|
12245
|
+
"existing",
|
|
12246
|
+
"git-worktree",
|
|
12247
|
+
"isolated-directory"
|
|
12248
|
+
].map((value) => Type.Literal(value))),
|
|
12249
|
+
default: Type.Optional(Type.Boolean()),
|
|
12250
|
+
effectiveSource: Type.Union([Type.String(), Type.Null()]),
|
|
12251
|
+
readiness: Type.Object({
|
|
12252
|
+
ready: Type.Boolean(),
|
|
12253
|
+
code: Type.Optional(Type.String()),
|
|
12254
|
+
message: Type.Optional(Type.String())
|
|
12255
|
+
})
|
|
12256
|
+
}, { $id: "AgentServerProjectLocation" });
|
|
12257
|
+
/** Closed: hooks and other stored fields are never writable over this route. */
|
|
12258
|
+
var SaveProjectLocationSchema = Type.Object({
|
|
12259
|
+
identity: Type.String({ minLength: 1 }),
|
|
12260
|
+
teamId: Type.String({ minLength: 1 }),
|
|
12261
|
+
projectId: Type.String({ minLength: 1 }),
|
|
12262
|
+
diaryId: Type.Optional(Type.String({ minLength: 1 })),
|
|
12263
|
+
source: Type.Optional(Type.String({ minLength: 1 })),
|
|
12264
|
+
strategy: Type.Union([
|
|
12265
|
+
"none",
|
|
12266
|
+
"existing",
|
|
12267
|
+
"git-worktree"
|
|
12268
|
+
].map((value) => Type.Literal(value))),
|
|
12269
|
+
default: Type.Optional(Type.Boolean())
|
|
12270
|
+
}, { additionalProperties: false });
|
|
12271
|
+
var ProjectLocationParamsSchema = Type.Object({ name: Type.String({ minLength: 1 }) });
|
|
12272
|
+
/** Local project selection on a run request; only the native client may set these. */
|
|
12273
|
+
var RunProjectFields = {
|
|
12274
|
+
projectId: Type.Optional(Type.Union([Type.String({ minLength: 1 }), Type.Null()])),
|
|
12275
|
+
location: Type.Optional(Type.String({ minLength: 1 })),
|
|
12276
|
+
source: Type.Optional(Type.String({ minLength: 1 })),
|
|
12277
|
+
strategy: Type.Optional(AgentServerProjectLocationSchema.properties.strategy)
|
|
12278
|
+
};
|
|
12279
|
+
/** Names gated to the native client; derived so a new field is gated automatically. */
|
|
12280
|
+
var NATIVE_RUN_FIELDS = Object.keys(RunProjectFields);
|
|
12281
|
+
/** What a run resolved to; the record's top-level fields stay as requested. */
|
|
12282
|
+
var RunWorkspaceSchema = Type.Object({
|
|
12283
|
+
projectId: Type.Union([Type.String(), Type.Null()]),
|
|
12284
|
+
location: Type.Optional(Type.String()),
|
|
12285
|
+
diaryId: Type.Optional(Type.String()),
|
|
12286
|
+
source: Type.Optional(Type.String()),
|
|
12287
|
+
strategy: Type.Union([AgentServerProjectLocationSchema.properties.strategy, Type.Literal(PROFILE_DEFAULT_STRATEGY)])
|
|
12288
|
+
});
|
|
11632
12289
|
var AgentServerRunRecordSchema = Type.Object({
|
|
12290
|
+
...RunProjectFields,
|
|
12291
|
+
workspace: Type.Optional(RunWorkspaceSchema),
|
|
11633
12292
|
id: Type.String(),
|
|
11634
12293
|
agent: Type.String(),
|
|
11635
12294
|
teamId: Type.String(),
|
|
@@ -11709,6 +12368,7 @@ var PutProviderSchema = Type.Object({
|
|
|
11709
12368
|
});
|
|
11710
12369
|
var DiscoverModelsSchema = Type.Object({ models: ProviderModelList }, { $id: "DiscoveredModels" });
|
|
11711
12370
|
var StartRunSchema = Type.Object({
|
|
12371
|
+
...RunProjectFields,
|
|
11712
12372
|
agent: Type.String(),
|
|
11713
12373
|
teamId: Type.String(),
|
|
11714
12374
|
diaryId: Type.Optional(Type.String()),
|
|
@@ -11725,6 +12385,7 @@ var LogStreamSchema = Type.String({
|
|
|
11725
12385
|
contentMediaType: "text/event-stream"
|
|
11726
12386
|
});
|
|
11727
12387
|
var AGENT_SERVER_SCHEMAS = [
|
|
12388
|
+
AgentServerProjectLocationSchema,
|
|
11728
12389
|
AgentServerHealthSchema,
|
|
11729
12390
|
AgentServerProblemSchema,
|
|
11730
12391
|
AgentServerAgentSchema,
|
|
@@ -11747,6 +12408,39 @@ var AGENT_SERVER_SCHEMAS = [
|
|
|
11747
12408
|
var localControlSecurity = [{ agentServerToken: [] }];
|
|
11748
12409
|
var problemResponse = { default: schemaRef(AgentServerProblemSchema) };
|
|
11749
12410
|
var AgentServerRouteSchemas = {
|
|
12411
|
+
listProjectLocations: {
|
|
12412
|
+
operationId: "listNativeProjectLocations",
|
|
12413
|
+
tags: ["native-projects"],
|
|
12414
|
+
security: localControlSecurity,
|
|
12415
|
+
description: "Requires the Desktop native grant; browser authorization is insufficient.",
|
|
12416
|
+
response: {
|
|
12417
|
+
200: Type.Object({ locations: Type.Array(schemaRef(AgentServerProjectLocationSchema)) }),
|
|
12418
|
+
...problemResponse
|
|
12419
|
+
}
|
|
12420
|
+
},
|
|
12421
|
+
saveProjectLocation: {
|
|
12422
|
+
operationId: "saveNativeProjectLocation",
|
|
12423
|
+
tags: ["native-projects"],
|
|
12424
|
+
security: localControlSecurity,
|
|
12425
|
+
description: "Creates or replaces the named location. Requires the Desktop native grant.",
|
|
12426
|
+
params: ProjectLocationParamsSchema,
|
|
12427
|
+
body: SaveProjectLocationSchema,
|
|
12428
|
+
response: {
|
|
12429
|
+
200: schemaRef(AgentServerProjectLocationSchema),
|
|
12430
|
+
...problemResponse
|
|
12431
|
+
}
|
|
12432
|
+
},
|
|
12433
|
+
removeProjectLocation: {
|
|
12434
|
+
operationId: "removeNativeProjectLocation",
|
|
12435
|
+
tags: ["native-projects"],
|
|
12436
|
+
security: localControlSecurity,
|
|
12437
|
+
description: "Removes the registration only; the folder is left untouched. Requires the Desktop native grant.",
|
|
12438
|
+
params: ProjectLocationParamsSchema,
|
|
12439
|
+
response: {
|
|
12440
|
+
200: Type.Object({ removed: Type.Boolean() }),
|
|
12441
|
+
...problemResponse
|
|
12442
|
+
}
|
|
12443
|
+
},
|
|
11750
12444
|
health: {
|
|
11751
12445
|
operationId: "getAgentServerHealth",
|
|
11752
12446
|
tags: ["system"],
|
|
@@ -11920,6 +12614,7 @@ var AgentServerRouteSchemas = {
|
|
|
11920
12614
|
operationId: "startAgentServerRun",
|
|
11921
12615
|
tags: ["runs"],
|
|
11922
12616
|
security: localControlSecurity,
|
|
12617
|
+
description: "projectId (other than null), location, source and strategy are native-only: other origins receive 403 native_required. A request naming none of them runs without project workspace wiring. The record keeps these fields as requested; resolved values are in `workspace`.",
|
|
11923
12618
|
body: StartRunSchema,
|
|
11924
12619
|
response: {
|
|
11925
12620
|
201: schemaRef(AgentServerRunSchema),
|
|
@@ -11954,8 +12649,8 @@ var AgentServerRouteSchemas = {
|
|
|
11954
12649
|
* loopback-companion security profile (#2066): loopback Host enforcement,
|
|
11955
12650
|
* exact-origin CORS, Fetch-Metadata guards, strict JSON parsing.
|
|
11956
12651
|
*
|
|
11957
|
-
* Control routes require
|
|
11958
|
-
*
|
|
12652
|
+
* Control routes require the process-scoped native Desktop grant. OAuth is
|
|
12653
|
+
* used only for native operator sign-in and credential provisioning.
|
|
11959
12654
|
*/
|
|
11960
12655
|
var AGENT_SERVER_TOKEN_HEADER = "x-moltnet-agent-server-token";
|
|
11961
12656
|
var BODY_LIMIT = 64 * 1024;
|
|
@@ -12014,14 +12709,6 @@ async function readAgentServerLogDelta(handle, state, limit = LOG_READ_LIMIT_BYT
|
|
|
12014
12709
|
omitted
|
|
12015
12710
|
};
|
|
12016
12711
|
}
|
|
12017
|
-
var AgentServerHttpError = class extends Error {
|
|
12018
|
-
name = "AgentServerHttpError";
|
|
12019
|
-
constructor(statusCode, code, message) {
|
|
12020
|
-
super(message);
|
|
12021
|
-
this.statusCode = statusCode;
|
|
12022
|
-
this.code = code;
|
|
12023
|
-
}
|
|
12024
|
-
};
|
|
12025
12712
|
function requireBody(request) {
|
|
12026
12713
|
const body = request.body;
|
|
12027
12714
|
if (typeof body !== "object" || body === null || Array.isArray(body)) throw new AgentServerHttpError(400, "invalid_body", "JSON object body required");
|
|
@@ -12107,22 +12794,6 @@ function buildAgentServer(input) {
|
|
|
12107
12794
|
isOriginAllowed: (origin) => origin === "moltnet-agent-desktop://native" || origin === options.selfOrigin || browserOrigins.has(origin)
|
|
12108
12795
|
});
|
|
12109
12796
|
}
|
|
12110
|
-
let verificationWindow = Date.now();
|
|
12111
|
-
let verifications = 0;
|
|
12112
|
-
const browserVerification = /* @__PURE__ */ new WeakMap();
|
|
12113
|
-
function verifyBrowser(request, token) {
|
|
12114
|
-
const previous = browserVerification.get(request);
|
|
12115
|
-
if (previous) return previous;
|
|
12116
|
-
if (Date.now() - verificationWindow >= RATE_LIMIT_WINDOW_MS) {
|
|
12117
|
-
verificationWindow = Date.now();
|
|
12118
|
-
verifications = 0;
|
|
12119
|
-
}
|
|
12120
|
-
if (++verifications > RATE_LIMIT_MAX) throw new AgentServerHttpError(429, "rate_limited", "Too many authorization attempts");
|
|
12121
|
-
if (!oauth) throw new AgentServerHttpError(503, "oauth_unavailable", "Local OAuth is not configured");
|
|
12122
|
-
const pending = oauth.verifyBrowser(token);
|
|
12123
|
-
browserVerification.set(request, pending);
|
|
12124
|
-
return pending;
|
|
12125
|
-
}
|
|
12126
12797
|
function hasValidNativeGrant(origin, token) {
|
|
12127
12798
|
if (origin !== "moltnet-agent-desktop://native" || typeof token !== "string" || token.length === 0) return false;
|
|
12128
12799
|
try {
|
|
@@ -12153,56 +12824,20 @@ function buildAgentServer(input) {
|
|
|
12153
12824
|
max: options.rateLimitMax ?? RATE_LIMIT_MAX,
|
|
12154
12825
|
timeWindow: RATE_LIMIT_WINDOW_MS,
|
|
12155
12826
|
errorResponseBuilder: () => new AgentServerHttpError(429, "rate_limited", "Too many requests"),
|
|
12156
|
-
keyGenerator:
|
|
12827
|
+
keyGenerator: (request) => {
|
|
12157
12828
|
const origin = request.headers.origin;
|
|
12158
12829
|
if (!isConfiguredOrigin(origin, options)) return `ip:${request.ip}`;
|
|
12159
12830
|
const presented = request.headers[AGENT_SERVER_TOKEN_HEADER];
|
|
12160
|
-
|
|
12161
|
-
if (typeof presented === "string" && presented.length > 0) try {
|
|
12162
|
-
if (origin === "moltnet-agent-desktop://native") authenticated = hasValidNativeGrant(origin, presented);
|
|
12163
|
-
else {
|
|
12164
|
-
if (!oauth) return `unauth:${origin}:${request.ip}`;
|
|
12165
|
-
await verifyBrowser(request, presented);
|
|
12166
|
-
authenticated = true;
|
|
12167
|
-
}
|
|
12168
|
-
} catch (error) {
|
|
12169
|
-
if (error instanceof AgentServerHttpError && error.statusCode === 429) throw error;
|
|
12170
|
-
if (origin === "moltnet-agent-desktop://native") throw error;
|
|
12171
|
-
}
|
|
12172
|
-
return authenticated ? `origin:${origin}` : `unauth:${origin}:${request.ip}`;
|
|
12831
|
+
return typeof presented === "string" && presented.length > 0 && hasValidNativeGrant(origin, presented) ? `origin:${origin}` : `unauth:${origin}:${request.ip}`;
|
|
12173
12832
|
}
|
|
12174
12833
|
});
|
|
12175
12834
|
const requireAuthorizedOrigin = async (request) => {
|
|
12176
12835
|
if (restartRequired) throw new AgentServerHttpError(409, "restart_required", "Restart the Agent Server to apply connection settings");
|
|
12177
12836
|
const origin = requireOriginHeader(request.headers);
|
|
12178
12837
|
const token = request.headers[AGENT_SERVER_TOKEN_HEADER];
|
|
12179
|
-
if (typeof token !== "string" || token.length === 0) throw new AgentServerHttpError(401, "authorization_required", "
|
|
12180
|
-
if (origin
|
|
12181
|
-
|
|
12182
|
-
if (!oauth) throw new AgentServerHttpError(503, "oauth_unavailable", "Local OAuth is not configured");
|
|
12183
|
-
const admission = browserVerification.get(request);
|
|
12184
|
-
browserVerification.delete(request);
|
|
12185
|
-
await (admission ?? oauth.verifyBrowser(token));
|
|
12186
|
-
} catch (error) {
|
|
12187
|
-
if (error instanceof AgentServerHttpError) throw error;
|
|
12188
|
-
const code = error && typeof error === "object" && "code" in error ? error.code : void 0;
|
|
12189
|
-
const rejected = error instanceof InvalidOperatorGrantError || typeof code === "string" && [
|
|
12190
|
-
"ERR_JWT_EXPIRED",
|
|
12191
|
-
"ERR_JWT_CLAIM_VALIDATION_FAILED",
|
|
12192
|
-
"ERR_JWS_SIGNATURE_VERIFICATION_FAILED",
|
|
12193
|
-
"ERR_JWS_INVALID",
|
|
12194
|
-
"ERR_JWT_INVALID",
|
|
12195
|
-
"ERR_JOSE_ALG_NOT_ALLOWED",
|
|
12196
|
-
"ERR_JWKS_NO_MATCHING_KEY"
|
|
12197
|
-
].includes(code);
|
|
12198
|
-
request.log.warn({
|
|
12199
|
-
stage: "local-control-authorization",
|
|
12200
|
-
outcome: rejected ? "rejected" : "unavailable",
|
|
12201
|
-
code: typeof code === "string" ? code : void 0
|
|
12202
|
-
}, "Local control authorization failed");
|
|
12203
|
-
if (!rejected) throw new AgentServerHttpError(503, "authorization_unavailable", "Local authorization is unavailable. Check Server settings or retry shortly.");
|
|
12204
|
-
throw new AgentServerHttpError(401, "authorization_required", "Sign in to authorize local control");
|
|
12205
|
-
}
|
|
12838
|
+
if (typeof token !== "string" || token.length === 0) throw new AgentServerHttpError(401, "authorization_required", "Native authorization is required");
|
|
12839
|
+
if (origin !== "moltnet-agent-desktop://native") throw new AgentServerHttpError(403, "native_required", "Native Desktop authorization required");
|
|
12840
|
+
requireNativeGrant(request);
|
|
12206
12841
|
return origin;
|
|
12207
12842
|
};
|
|
12208
12843
|
app.after(() => {
|
|
@@ -12211,13 +12846,12 @@ function buildAgentServer(input) {
|
|
|
12211
12846
|
});
|
|
12212
12847
|
app.get("/health", { schema: AgentServerRouteSchemas.health }, async () => ({ status: "ok" }));
|
|
12213
12848
|
app.get("/v1/native/connection-settings", { schema: { hide: true } }, async (request) => {
|
|
12214
|
-
|
|
12215
|
-
return options.connectionSettings.view();
|
|
12849
|
+
return (await requireNativeOrigin(requireAuthorizedOrigin, request, options.connectionSettings)).view();
|
|
12216
12850
|
});
|
|
12217
12851
|
app.post("/v1/native/connection-settings", { schema: { hide: true } }, async (request) => {
|
|
12218
|
-
|
|
12852
|
+
const store = await requireNativeOrigin(requireAuthorizedOrigin, request, options.connectionSettings);
|
|
12219
12853
|
try {
|
|
12220
|
-
const settings = options.runs.prepareServerRestart(() =>
|
|
12854
|
+
const settings = options.runs.prepareServerRestart(() => store.save(request.body));
|
|
12221
12855
|
oauth?.cancel();
|
|
12222
12856
|
restartRequired = true;
|
|
12223
12857
|
return settings;
|
|
@@ -12225,40 +12859,6 @@ function buildAgentServer(input) {
|
|
|
12225
12859
|
throw new AgentServerHttpError(400, "invalid_connection_settings", error instanceof Error ? error.message : "Invalid connection settings");
|
|
12226
12860
|
}
|
|
12227
12861
|
});
|
|
12228
|
-
app.get("/oauth/metadata", { schema: {
|
|
12229
|
-
operationId: "getAgentServerOAuthMetadata",
|
|
12230
|
-
tags: ["operator"],
|
|
12231
|
-
response: { 200: {
|
|
12232
|
-
type: "object",
|
|
12233
|
-
required: [
|
|
12234
|
-
"protocolVersion",
|
|
12235
|
-
"instance",
|
|
12236
|
-
"issuer",
|
|
12237
|
-
"authorizationUrl",
|
|
12238
|
-
"tokenUrl",
|
|
12239
|
-
"clientId",
|
|
12240
|
-
"operatorConfigured"
|
|
12241
|
-
],
|
|
12242
|
-
properties: {
|
|
12243
|
-
protocolVersion: {
|
|
12244
|
-
type: "integer",
|
|
12245
|
-
const: OPERATOR_OAUTH.protocolVersion
|
|
12246
|
-
},
|
|
12247
|
-
instance: {
|
|
12248
|
-
type: "string",
|
|
12249
|
-
format: "uuid"
|
|
12250
|
-
},
|
|
12251
|
-
issuer: { type: "string" },
|
|
12252
|
-
authorizationUrl: { type: "string" },
|
|
12253
|
-
tokenUrl: { type: "string" },
|
|
12254
|
-
clientId: { type: "string" },
|
|
12255
|
-
operatorConfigured: { type: "boolean" }
|
|
12256
|
-
}
|
|
12257
|
-
} }
|
|
12258
|
-
} }, async () => {
|
|
12259
|
-
if (!oauth) throw new AgentServerHttpError(503, "oauth_unavailable", "Local OAuth is not configured");
|
|
12260
|
-
return oauth.metadata();
|
|
12261
|
-
});
|
|
12262
12862
|
app.post("/v1/operator/sign-in", { schema: {
|
|
12263
12863
|
operationId: "signInAgentServerOperator",
|
|
12264
12864
|
response: { 200: {
|
|
@@ -12267,10 +12867,12 @@ function buildAgentServer(input) {
|
|
|
12267
12867
|
required: ["state"]
|
|
12268
12868
|
} }
|
|
12269
12869
|
} }, async (request) => {
|
|
12270
|
-
|
|
12271
|
-
await oauth.authorize(void 0, requestOperationSignal(request, options.shutdownSignal));
|
|
12870
|
+
await (await requireNativeOrigin(requireAuthorizedOrigin, request, oauth)).authorize(void 0, requestOperationSignal(request, options.shutdownSignal));
|
|
12272
12871
|
return { state: "authorized" };
|
|
12273
12872
|
});
|
|
12873
|
+
app.get("/v1/native/operator", { schema: { hide: true } }, async (request) => {
|
|
12874
|
+
return { operatorConfigured: (await requireNativeOrigin(requireAuthorizedOrigin, request, oauth)).operatorConfigured() };
|
|
12875
|
+
});
|
|
12274
12876
|
app.post("/v1/operator/cancel", { schema: {
|
|
12275
12877
|
operationId: "cancelAgentServerOperatorApproval",
|
|
12276
12878
|
tags: ["operator"],
|
|
@@ -12284,8 +12886,7 @@ function buildAgentServer(input) {
|
|
|
12284
12886
|
required: ["state"]
|
|
12285
12887
|
} }
|
|
12286
12888
|
} }, async (request) => {
|
|
12287
|
-
|
|
12288
|
-
oauth.cancel();
|
|
12889
|
+
(await requireNativeOrigin(requireAuthorizedOrigin, request, oauth)).cancel();
|
|
12289
12890
|
return { state: "cancelled" };
|
|
12290
12891
|
});
|
|
12291
12892
|
app.delete("/v1/operator", { schema: {
|
|
@@ -12301,8 +12902,7 @@ function buildAgentServer(input) {
|
|
|
12301
12902
|
required: ["state"]
|
|
12302
12903
|
} }
|
|
12303
12904
|
} }, async (request) => {
|
|
12304
|
-
|
|
12305
|
-
oauth.removeOperator();
|
|
12905
|
+
(await requireNativeOrigin(requireAuthorizedOrigin, request, oauth)).removeOperator();
|
|
12306
12906
|
return { state: "removed" };
|
|
12307
12907
|
});
|
|
12308
12908
|
registerStatusRoute(app, options, requireAuthorizedOrigin);
|
|
@@ -12311,6 +12911,7 @@ function buildAgentServer(input) {
|
|
|
12311
12911
|
registerSubscriptionRoutes(app, options, requireAuthorizedOrigin);
|
|
12312
12912
|
registerRunRoutes(app, options, requireAuthorizedOrigin);
|
|
12313
12913
|
registerCatalogueRoute(app, options, requireAuthorizedOrigin);
|
|
12914
|
+
registerProjectLocationRoutes(app, options, requireAuthorizedOrigin);
|
|
12314
12915
|
});
|
|
12315
12916
|
app.addHook("preClose", async () => {
|
|
12316
12917
|
options.operatorOAuth?.cancel();
|
|
@@ -12337,6 +12938,103 @@ function buildAgentServer(input) {
|
|
|
12337
12938
|
});
|
|
12338
12939
|
return app;
|
|
12339
12940
|
}
|
|
12941
|
+
async function requireNativeOrigin(authorize, request, ...resource) {
|
|
12942
|
+
if (await authorize(request) !== "moltnet-agent-desktop://native" || resource.length > 0 && resource[0] === void 0) throw new AgentServerHttpError(403, "native_required", "Native administration required");
|
|
12943
|
+
return resource[0];
|
|
12944
|
+
}
|
|
12945
|
+
function registerProjectLocationRoutes(app, options, authorize) {
|
|
12946
|
+
let locations;
|
|
12947
|
+
const getLocations = () => {
|
|
12948
|
+
const root = options.connectionSettings?.root;
|
|
12949
|
+
if (!root) throw new AgentServerHttpError(503, "locations_unavailable", "Project locations need the Desktop connection store");
|
|
12950
|
+
locations ??= new LocalProjectBindings(root, options.defaultApiUrl, [
|
|
12951
|
+
root,
|
|
12952
|
+
options.store.root,
|
|
12953
|
+
options.store.secretsDir
|
|
12954
|
+
]);
|
|
12955
|
+
return locations;
|
|
12956
|
+
};
|
|
12957
|
+
const requireNativeRequest = async (request) => {
|
|
12958
|
+
await requireNativeOrigin(authorize, request);
|
|
12959
|
+
if (request.validationError) throw new AgentServerHttpError(400, "invalid_location", "Check the project location fields");
|
|
12960
|
+
};
|
|
12961
|
+
app.get("/v1/native/project-locations", { schema: AgentServerRouteSchemas.listProjectLocations }, async (request) => {
|
|
12962
|
+
await requireNativeRequest(request);
|
|
12963
|
+
return { locations: await getLocations().list() };
|
|
12964
|
+
});
|
|
12965
|
+
const saveFields = new Set(Object.keys(AgentServerRouteSchemas.saveProjectLocation.body.properties));
|
|
12966
|
+
app.put("/v1/native/project-locations/:name", {
|
|
12967
|
+
schema: AgentServerRouteSchemas.saveProjectLocation,
|
|
12968
|
+
attachValidation: true,
|
|
12969
|
+
preValidation: async (request) => {
|
|
12970
|
+
await requireNativeOrigin(authorize, request);
|
|
12971
|
+
const body = request.body;
|
|
12972
|
+
if (body && typeof body === "object" && Object.keys(body).some((key) => !saveFields.has(key))) throw new AgentServerHttpError(400, "invalid_location", "Check the project location fields");
|
|
12973
|
+
}
|
|
12974
|
+
}, async (request) => {
|
|
12975
|
+
await requireNativeRequest(request);
|
|
12976
|
+
const bindings = getLocations();
|
|
12977
|
+
const { name } = request.params;
|
|
12978
|
+
const { identity, ...location } = requireBody(request);
|
|
12979
|
+
const alias = identity.trim();
|
|
12980
|
+
requireActivation(options.store, alias);
|
|
12981
|
+
const { config } = await loadAgentActivation(options.store, alias);
|
|
12982
|
+
if (locationEndpoint(config.endpoints.api) !== bindings.apiUrl) throw new AgentServerHttpError(400, "endpoint_mismatch", "Choose an identity for the current server endpoint");
|
|
12983
|
+
const signal = AbortSignal.any([requestOperationSignal(request, options.shutdownSignal), AbortSignal.timeout(options.projectSaveTimeoutMs ?? 1e4)]);
|
|
12984
|
+
await verifyLocationTarget(options, alias, location, request.log, signal);
|
|
12985
|
+
try {
|
|
12986
|
+
return await bindings.save({
|
|
12987
|
+
...location,
|
|
12988
|
+
name,
|
|
12989
|
+
apiUrl: bindings.apiUrl
|
|
12990
|
+
}, { signal });
|
|
12991
|
+
} catch (error) {
|
|
12992
|
+
if (signal.aborted) throw checkUnavailable(error, true);
|
|
12993
|
+
throw error;
|
|
12994
|
+
}
|
|
12995
|
+
});
|
|
12996
|
+
app.delete("/v1/native/project-locations/:name", {
|
|
12997
|
+
schema: AgentServerRouteSchemas.removeProjectLocation,
|
|
12998
|
+
attachValidation: true
|
|
12999
|
+
}, async (request) => {
|
|
13000
|
+
await requireNativeRequest(request);
|
|
13001
|
+
await getLocations().remove(request.params.name);
|
|
13002
|
+
return { removed: true };
|
|
13003
|
+
});
|
|
13004
|
+
}
|
|
13005
|
+
/**
|
|
13006
|
+
* A location save checks only its target team: `readTeam` verifies the team
|
|
13007
|
+
* credential and returns its diaries, then the shared check reads the project.
|
|
13008
|
+
*/
|
|
13009
|
+
async function verifyLocationTarget(options, alias, target, logger, signal) {
|
|
13010
|
+
const agent = await catalogueAgent(options, alias);
|
|
13011
|
+
if (!agent.teamIds.includes(target.teamId)) throw projectUnavailable();
|
|
13012
|
+
let diaries = [];
|
|
13013
|
+
await verifyProjectTarget({
|
|
13014
|
+
readProject: async (teamId, projectId, readSignal) => {
|
|
13015
|
+
const team = await agent.readTeam(teamId, readSignal);
|
|
13016
|
+
if (team.team.id !== teamId) throw new Error("Team response mismatch");
|
|
13017
|
+
diaries = team.diaries;
|
|
13018
|
+
return agent.readProject(teamId, projectId, readSignal);
|
|
13019
|
+
},
|
|
13020
|
+
readDiary: async (teamId, diaryId) => diaries.find((diary) => diary.id === diaryId && diary.teamId === teamId) ?? null
|
|
13021
|
+
}, target, {
|
|
13022
|
+
signal,
|
|
13023
|
+
logger
|
|
13024
|
+
});
|
|
13025
|
+
}
|
|
13026
|
+
async function catalogueAgent(options, alias) {
|
|
13027
|
+
return options.catalogueAgentFor ? options.catalogueAgentFor(alias) : defaultCatalogueAgent(options, alias);
|
|
13028
|
+
}
|
|
13029
|
+
async function readIdentityCatalogue(options, alias, logger) {
|
|
13030
|
+
requireActivation(options.store, alias);
|
|
13031
|
+
return buildCatalogue({
|
|
13032
|
+
agent: await catalogueAgent(options, alias),
|
|
13033
|
+
machine: machineCapabilities(options),
|
|
13034
|
+
identityDefault: readIdentityDefaultBinding(options.store.identityDir(alias)),
|
|
13035
|
+
logger
|
|
13036
|
+
});
|
|
13037
|
+
}
|
|
12340
13038
|
function registerCatalogueRoute(app, options, requireAuthorizedOrigin) {
|
|
12341
13039
|
app.get("/v1/catalogue", {
|
|
12342
13040
|
schema: AgentServerRouteSchemas.catalogue,
|
|
@@ -12345,34 +13043,42 @@ function registerCatalogueRoute(app, options, requireAuthorizedOrigin) {
|
|
|
12345
13043
|
await requireAuthorizedOrigin(request);
|
|
12346
13044
|
const { identity } = request.query ?? {};
|
|
12347
13045
|
if (!identity || identity.trim().length === 0) throw new AgentServerHttpError(400, "invalid_query", "\"identity\" is required");
|
|
12348
|
-
|
|
12349
|
-
requireActivation(options.store, alias);
|
|
12350
|
-
return buildCatalogue({
|
|
12351
|
-
agent: await (options.catalogueAgentFor ? options.catalogueAgentFor(alias) : defaultCatalogueAgent(options, alias)),
|
|
12352
|
-
machine: machineCapabilities(options),
|
|
12353
|
-
identityDefault: readIdentityDefaultBinding(options.store.identityDir(alias))
|
|
12354
|
-
});
|
|
13046
|
+
return readIdentityCatalogue(options, identity.trim(), request.log);
|
|
12355
13047
|
});
|
|
12356
13048
|
}
|
|
12357
13049
|
/** Resolve and verify each indexed team independently with its exact key. */
|
|
12358
13050
|
async function defaultCatalogueAgent(options, alias) {
|
|
12359
13051
|
const { config } = await loadAgentActivation(options.store, alias);
|
|
13052
|
+
const clients = /* @__PURE__ */ new Map();
|
|
13053
|
+
const verifiedClient = (teamId) => {
|
|
13054
|
+
const client = clients.get(teamId);
|
|
13055
|
+
if (!client) throw new Error("Team must be verified before project discovery");
|
|
13056
|
+
return client;
|
|
13057
|
+
};
|
|
12360
13058
|
return {
|
|
12361
13059
|
teamIds: Object.keys(config.agent_key_refs ?? {}),
|
|
12362
13060
|
lastVerified: (teamId) => requireActivation(options.store, alias).credentialHealth?.[teamId],
|
|
12363
|
-
readTeam: async (teamId) => {
|
|
12364
|
-
const
|
|
13061
|
+
readTeam: async (teamId, signal) => {
|
|
13062
|
+
const activated = await verifyTeamActivation(options.store, alias, options.secretProviders, options.externalSecretProviders, void 0, signal && options.shutdownSignal ? AbortSignal.any([signal, options.shutdownSignal]) : signal ?? options.shutdownSignal, teamId);
|
|
13063
|
+
signal?.throwIfAborted();
|
|
13064
|
+
const { client, metadata } = requireCredentialSnapshot(activated);
|
|
12365
13065
|
const [team, diaries, profiles] = await Promise.all([
|
|
12366
13066
|
client.teams.get(teamId),
|
|
12367
13067
|
client.diaries.list(),
|
|
12368
13068
|
client.runtimeProfiles.list({ teamId })
|
|
12369
13069
|
]);
|
|
13070
|
+
clients.set(teamId, client);
|
|
12370
13071
|
return {
|
|
12371
13072
|
team,
|
|
12372
13073
|
diaries: diaries.items,
|
|
12373
13074
|
profiles: profiles.items,
|
|
12374
13075
|
credential: metadata
|
|
12375
13076
|
};
|
|
13077
|
+
},
|
|
13078
|
+
readProjects: async (teamId) => readCatalogueProjects(verifiedClient(teamId).projects, teamId),
|
|
13079
|
+
readProject: async (teamId, projectId, signal) => {
|
|
13080
|
+
signal?.throwIfAborted();
|
|
13081
|
+
return readCatalogueProject(verifiedClient(teamId).projects, teamId, projectId);
|
|
12376
13082
|
}
|
|
12377
13083
|
};
|
|
12378
13084
|
}
|
|
@@ -12395,7 +13101,7 @@ function machineCapabilities(options) {
|
|
|
12395
13101
|
function registerStatusRoute(app, options, requireAuthorizedOrigin) {
|
|
12396
13102
|
const { store, runs } = options;
|
|
12397
13103
|
app.get("/v1/status", { schema: AgentServerRouteSchemas.status }, async (request) => {
|
|
12398
|
-
await requireAuthorizedOrigin(request);
|
|
13104
|
+
const origin = await requireAuthorizedOrigin(request);
|
|
12399
13105
|
const selected = selectedIdentity(store, options.activeIdentity);
|
|
12400
13106
|
return {
|
|
12401
13107
|
version: options.version,
|
|
@@ -12405,7 +13111,7 @@ function registerStatusRoute(app, options, requireAuthorizedOrigin) {
|
|
|
12405
13111
|
identities: identityViews(store),
|
|
12406
13112
|
...selected ? { selectedIdentity: selected } : {},
|
|
12407
13113
|
providers: options.providers.list(),
|
|
12408
|
-
runs: await runViews(runs),
|
|
13114
|
+
runs: await runViews(runs, origin),
|
|
12409
13115
|
runtimeSettings: options.runtimeSettings ?? DEFAULT_LOCAL_OPERATIONAL_SETTINGS
|
|
12410
13116
|
};
|
|
12411
13117
|
});
|
|
@@ -12530,11 +13236,30 @@ function registerProviderRoutes(app, options, requireAuthorizedOrigin) {
|
|
|
12530
13236
|
return reply.code(204).send(null);
|
|
12531
13237
|
});
|
|
12532
13238
|
}
|
|
12533
|
-
async function runViews(runs) {
|
|
12534
|
-
return (await runs.listAsync(RUN_HISTORY_LIMIT)).map((record) => (
|
|
12535
|
-
|
|
12536
|
-
|
|
12537
|
-
|
|
13239
|
+
async function runViews(runs, origin) {
|
|
13240
|
+
return (await runs.listAsync(RUN_HISTORY_LIMIT)).map((record) => runView(record, runs.isActive(record.id), origin));
|
|
13241
|
+
}
|
|
13242
|
+
/**
|
|
13243
|
+
* Local folders are native-only, as project locations are: other origins see
|
|
13244
|
+
* the location name and project, never the path. The snapshot path is
|
|
13245
|
+
* daemon-internal for every origin.
|
|
13246
|
+
*/
|
|
13247
|
+
function runView(record, active, origin) {
|
|
13248
|
+
const native = origin === NATIVE_CLIENT_ORIGIN;
|
|
13249
|
+
const { source, workspace, ...rest } = record;
|
|
13250
|
+
const view = {
|
|
13251
|
+
...rest,
|
|
13252
|
+
...native && source !== void 0 ? { source } : {},
|
|
13253
|
+
active
|
|
13254
|
+
};
|
|
13255
|
+
if (workspace) {
|
|
13256
|
+
const { configPath: _configPath, source: resolvedSource, ...shared } = workspace;
|
|
13257
|
+
view.workspace = {
|
|
13258
|
+
...shared,
|
|
13259
|
+
...native && resolvedSource !== void 0 ? { source: resolvedSource } : {}
|
|
13260
|
+
};
|
|
13261
|
+
}
|
|
13262
|
+
return view;
|
|
12538
13263
|
}
|
|
12539
13264
|
function registerSubscriptionRoutes(app, options, requireAuthorizedOrigin) {
|
|
12540
13265
|
app.get("/v1/subscriptions", { schema: AgentServerRouteSchemas.listSubscriptions }, async (request) => {
|
|
@@ -12561,17 +13286,22 @@ function registerSubscriptionRoutes(app, options, requireAuthorizedOrigin) {
|
|
|
12561
13286
|
function registerRunRoutes(app, options, requireAuthorizedOrigin) {
|
|
12562
13287
|
const { runs } = options;
|
|
12563
13288
|
app.get("/v1/runs", { schema: AgentServerRouteSchemas.listRuns }, async (request) => {
|
|
12564
|
-
await requireAuthorizedOrigin(request);
|
|
12565
|
-
return runViews(runs);
|
|
13289
|
+
return runViews(runs, await requireAuthorizedOrigin(request));
|
|
12566
13290
|
});
|
|
12567
13291
|
app.post("/v1/runs", {
|
|
12568
13292
|
schema: AgentServerRouteSchemas.startRun,
|
|
12569
13293
|
attachValidation: true
|
|
12570
13294
|
}, async (request, reply) => {
|
|
12571
|
-
await requireAuthorizedOrigin(request);
|
|
13295
|
+
const origin = await requireAuthorizedOrigin(request);
|
|
12572
13296
|
const body = requireBody(request);
|
|
13297
|
+
if (NATIVE_RUN_FIELDS.some((key) => body[key] !== void 0 && body[key] !== null)) await requireNativeOrigin(async () => origin, request);
|
|
13298
|
+
if (request.validationError) throw new AgentServerHttpError(400, "invalid_spec", `Check the run fields: ${request.validationError.message}`);
|
|
12573
13299
|
const diaryId = optionalString(body, "diaryId");
|
|
12574
13300
|
const record = await runs.start({
|
|
13301
|
+
...body.projectId === null ? { projectId: null } : body.projectId !== void 0 ? { projectId: requireString(body, "projectId") } : {},
|
|
13302
|
+
...body.location !== void 0 ? { location: requireString(body, "location") } : {},
|
|
13303
|
+
...body.source !== void 0 ? { source: requireString(body, "source") } : {},
|
|
13304
|
+
...body.strategy !== void 0 ? { strategy: requireString(body, "strategy") } : {},
|
|
12575
13305
|
agent: requireString(body, "agent"),
|
|
12576
13306
|
teamId: requireString(body, "teamId"),
|
|
12577
13307
|
...diaryId ? { diaryId } : {},
|
|
@@ -12579,15 +13309,13 @@ function registerRunRoutes(app, options, requireAuthorizedOrigin) {
|
|
|
12579
13309
|
taskTypes: stringArray(body, "taskTypes"),
|
|
12580
13310
|
mode: requireString(body, "mode")
|
|
12581
13311
|
}, requestOperationSignal(request, options.shutdownSignal));
|
|
12582
|
-
return reply.code(201).send(
|
|
12583
|
-
...record,
|
|
12584
|
-
active: runs.isActive(record.id)
|
|
12585
|
-
});
|
|
13312
|
+
return reply.code(201).send(runView(record, runs.isActive(record.id), origin));
|
|
12586
13313
|
});
|
|
12587
13314
|
app.delete("/v1/runs/:runId", { schema: AgentServerRouteSchemas.stopRun }, async (request) => {
|
|
12588
|
-
await requireAuthorizedOrigin(request);
|
|
13315
|
+
const origin = await requireAuthorizedOrigin(request);
|
|
12589
13316
|
const { runId } = request.params;
|
|
12590
|
-
|
|
13317
|
+
const record = runs.stop(runId);
|
|
13318
|
+
return runView(record, runs.isActive(record.id), origin);
|
|
12591
13319
|
});
|
|
12592
13320
|
registerRunLogRoute(app, options, requireAuthorizedOrigin);
|
|
12593
13321
|
}
|
|
@@ -12614,9 +13342,10 @@ function registerRunLogRoute(app, options, requireAuthorizedOrigin) {
|
|
|
12614
13342
|
} }
|
|
12615
13343
|
} }
|
|
12616
13344
|
} }, async (request) => {
|
|
12617
|
-
await requireAuthorizedOrigin(request);
|
|
13345
|
+
const origin = await requireAuthorizedOrigin(request);
|
|
12618
13346
|
const { runId } = request.params;
|
|
12619
13347
|
const record = runs.status(runId);
|
|
13348
|
+
const redact = localPathRedactor(record, origin, options);
|
|
12620
13349
|
const handle = await open(store.resolveRunLogPath(record.id), constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
12621
13350
|
try {
|
|
12622
13351
|
const state = {
|
|
@@ -12624,20 +13353,22 @@ function registerRunLogRoute(app, options, requireAuthorizedOrigin) {
|
|
|
12624
13353
|
fragment: ""
|
|
12625
13354
|
};
|
|
12626
13355
|
const { lines, omitted } = await readAgentServerLogDelta(handle, state);
|
|
13356
|
+
const native = origin === NATIVE_CLIENT_ORIGIN;
|
|
12627
13357
|
return { lines: [
|
|
12628
13358
|
...omitted ? ["[older log output omitted]"] : [],
|
|
12629
|
-
...lines,
|
|
12630
|
-
...state.fragment ? [state.fragment] : []
|
|
12631
|
-
] };
|
|
13359
|
+
...omitted && !native ? lines.slice(1) : lines,
|
|
13360
|
+
...state.fragment && (native || !runs.isActive(record.id)) ? [state.fragment] : []
|
|
13361
|
+
].map(redact) };
|
|
12632
13362
|
} finally {
|
|
12633
13363
|
await handle.close();
|
|
12634
13364
|
}
|
|
12635
13365
|
});
|
|
12636
13366
|
let openStreams = 0;
|
|
12637
13367
|
app.get("/v1/runs/:runId/logs", { schema: AgentServerRouteSchemas.streamRunLogs }, async (request, reply) => {
|
|
12638
|
-
await requireAuthorizedOrigin(request);
|
|
13368
|
+
const origin = await requireAuthorizedOrigin(request);
|
|
12639
13369
|
const { runId } = request.params;
|
|
12640
13370
|
const record = runs.status(runId);
|
|
13371
|
+
const redact = localPathRedactor(record, origin, options);
|
|
12641
13372
|
store.resolveRunLogPath(record.id);
|
|
12642
13373
|
if (openStreams >= MAX_LOG_STREAMS) throw new AgentServerHttpError(429, "rate_limited", "Too many concurrent log streams");
|
|
12643
13374
|
openStreams += 1;
|
|
@@ -12678,12 +13409,12 @@ function registerRunLogRoute(app, options, requireAuthorizedOrigin) {
|
|
|
12678
13409
|
});
|
|
12679
13410
|
};
|
|
12680
13411
|
const push = async () => {
|
|
12681
|
-
if (request.headers.origin !== "moltnet-agent-desktop://native") await requireAuthorizedOrigin(request);
|
|
12682
13412
|
const handle = await open(store.resolveRunLogPath(record.id), constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
12683
13413
|
try {
|
|
12684
13414
|
const { lines, omitted } = await readAgentServerLogDelta(handle, readState);
|
|
12685
13415
|
if (omitted) await writeData("[older log output omitted]");
|
|
12686
|
-
|
|
13416
|
+
const complete = omitted && origin !== "moltnet-agent-desktop://native" ? lines.slice(1) : lines;
|
|
13417
|
+
for (const line of complete) await writeData(redact(line));
|
|
12687
13418
|
} finally {
|
|
12688
13419
|
await handle.close();
|
|
12689
13420
|
}
|
|
@@ -12718,6 +13449,43 @@ function registerRunLogRoute(app, options, requireAuthorizedOrigin) {
|
|
|
12718
13449
|
return reply;
|
|
12719
13450
|
});
|
|
12720
13451
|
}
|
|
13452
|
+
/**
|
|
13453
|
+
* Worker logs name local folders (the chosen source, state and HOME under the
|
|
13454
|
+
* store). Non-native origins get them replaced, as `runView` does for records.
|
|
13455
|
+
* The user's home directory is included so any other path under it cannot
|
|
13456
|
+
* reveal the OS account name.
|
|
13457
|
+
*/
|
|
13458
|
+
/**
|
|
13459
|
+
* A service account's home can be `/` or `/root`; replacing it would rewrite
|
|
13460
|
+
* every slash in the log, so only a home with two or more segments counts.
|
|
13461
|
+
*/
|
|
13462
|
+
function redactableHome() {
|
|
13463
|
+
const home = homedir();
|
|
13464
|
+
return home.split(/[\\/]/u).filter(Boolean).length >= 2 ? home : void 0;
|
|
13465
|
+
}
|
|
13466
|
+
function localPathRedactor(record, origin, options) {
|
|
13467
|
+
if (origin === "moltnet-agent-desktop://native") return (line) => line;
|
|
13468
|
+
const paths = /* @__PURE__ */ new Set();
|
|
13469
|
+
for (const path of [
|
|
13470
|
+
record.source,
|
|
13471
|
+
record.workspace?.source,
|
|
13472
|
+
options.store.root,
|
|
13473
|
+
options.connectionSettings?.root,
|
|
13474
|
+
redactableHome()
|
|
13475
|
+
]) {
|
|
13476
|
+
if (!path) continue;
|
|
13477
|
+
const forms = [path];
|
|
13478
|
+
try {
|
|
13479
|
+
forms.push(realpathSync.native(path));
|
|
13480
|
+
} catch {}
|
|
13481
|
+
for (const form of forms) {
|
|
13482
|
+
paths.add(form);
|
|
13483
|
+
paths.add(JSON.stringify(form).slice(1, -1));
|
|
13484
|
+
}
|
|
13485
|
+
}
|
|
13486
|
+
const ordered = [...paths].sort((a, b) => b.length - a.length);
|
|
13487
|
+
return (line) => ordered.reduce((text, path) => text.split(path).join("<local path>"), line);
|
|
13488
|
+
}
|
|
12721
13489
|
function corsHeadersFor(request, options) {
|
|
12722
13490
|
const origin = request.headers.origin;
|
|
12723
13491
|
if (isConfiguredOrigin(origin, options)) return {
|
|
@@ -12727,9 +13495,26 @@ function corsHeadersFor(request, options) {
|
|
|
12727
13495
|
return {};
|
|
12728
13496
|
}
|
|
12729
13497
|
function isConfiguredOrigin(origin, options) {
|
|
12730
|
-
return typeof origin === "string" && (origin === "moltnet-agent-desktop://native" || options.allowedOrigins.includes(origin) || origin === options.selfOrigin);
|
|
13498
|
+
return typeof origin === "string" && (origin === "moltnet-agent-desktop://native" || !options.nativeOnly && (options.allowedOrigins.includes(origin) || origin === options.selfOrigin));
|
|
12731
13499
|
}
|
|
12732
13500
|
function normalizeAgentServerError(error) {
|
|
13501
|
+
if (error instanceof ProjectConfigError) {
|
|
13502
|
+
if (error.kind === "version") return {
|
|
13503
|
+
statusCode: 409,
|
|
13504
|
+
code: "config_version",
|
|
13505
|
+
message: "The project locations file was written by a newer MoltNet. Update this app."
|
|
13506
|
+
};
|
|
13507
|
+
if (error.kind === "io") return {
|
|
13508
|
+
statusCode: 500,
|
|
13509
|
+
code: "config_unavailable",
|
|
13510
|
+
message: "The project locations file could not be read or written. Check its ownership and permissions."
|
|
13511
|
+
};
|
|
13512
|
+
return {
|
|
13513
|
+
statusCode: 400,
|
|
13514
|
+
code: "invalid_location",
|
|
13515
|
+
message: error.message
|
|
13516
|
+
};
|
|
13517
|
+
}
|
|
12733
13518
|
if (error instanceof TeamCredentialError) return {
|
|
12734
13519
|
statusCode: 400,
|
|
12735
13520
|
code: error.blocker.code,
|
|
@@ -12814,7 +13599,7 @@ function nativeSocketValidationOptions(input) {
|
|
|
12814
13599
|
...input.cliAllowedOrigins ? { allowedOrigins: input.cliAllowedOrigins } : {}
|
|
12815
13600
|
};
|
|
12816
13601
|
}
|
|
12817
|
-
async function runAgentServer(argv) {
|
|
13602
|
+
async function runAgentServer(argv, ports = {}) {
|
|
12818
13603
|
if (isHelpFlag(argv)) {
|
|
12819
13604
|
console.log(AGENT_SERVER_HELP);
|
|
12820
13605
|
return 0;
|
|
@@ -12866,6 +13651,7 @@ async function runAgentServer(argv) {
|
|
|
12866
13651
|
name: "agent-daemon.server",
|
|
12867
13652
|
level: envConfig.logLevel || "info"
|
|
12868
13653
|
});
|
|
13654
|
+
const shutdownDisposers = [];
|
|
12869
13655
|
try {
|
|
12870
13656
|
try {
|
|
12871
13657
|
return await withAgentServerLock(settingsRoot, async () => {
|
|
@@ -12891,9 +13677,12 @@ async function runAgentServer(argv) {
|
|
|
12891
13677
|
logger
|
|
12892
13678
|
});
|
|
12893
13679
|
const runtimeRegistry = new RuntimeRegistry(store.root);
|
|
13680
|
+
const supplied = ports.configure?.(store);
|
|
12894
13681
|
const runs = new RunManager({
|
|
13682
|
+
...supplied?.runOptions,
|
|
12895
13683
|
store,
|
|
12896
13684
|
storeRoot: settingsRoot,
|
|
13685
|
+
projectRoot: settingsRoot,
|
|
12897
13686
|
secretProviders,
|
|
12898
13687
|
externalSecretProviders,
|
|
12899
13688
|
baseEnv: processEnvSnapshot(),
|
|
@@ -12903,16 +13692,17 @@ async function runAgentServer(argv) {
|
|
|
12903
13692
|
});
|
|
12904
13693
|
if (nativeSocket) await validateNativeSocket(nativeSocket);
|
|
12905
13694
|
const selfOrigin = nativeSocket ? void 0 : `http://127.0.0.1:${port}`;
|
|
13695
|
+
const operatorOAuth = new OperatorOAuth({
|
|
13696
|
+
issuer: connection.issuer,
|
|
13697
|
+
authorizationUrl: new URL("/oauth2/auth", connection.publicUrl).href,
|
|
13698
|
+
tokenUrl: new URL("/oauth2/token", connection.publicUrl).href,
|
|
13699
|
+
jwksUrl: new URL("/.well-known/jwks.json", connection.publicUrl).href,
|
|
13700
|
+
nativeClientId: connection.nativeClientId,
|
|
13701
|
+
callbackPort: OPERATOR_OAUTH.callbackPort
|
|
13702
|
+
}, root);
|
|
12906
13703
|
const app = buildAgentServer({
|
|
12907
|
-
|
|
12908
|
-
|
|
12909
|
-
authorizationUrl: new URL("/oauth2/auth", connection.publicUrl).href,
|
|
12910
|
-
tokenUrl: new URL("/oauth2/token", connection.publicUrl).href,
|
|
12911
|
-
jwksUrl: new URL("/.well-known/jwks.json", connection.publicUrl).href,
|
|
12912
|
-
nativeClientId: connection.nativeClientId,
|
|
12913
|
-
consoleClientId: connection.consoleClientId,
|
|
12914
|
-
callbackPort: OPERATOR_OAUTH.callbackPort
|
|
12915
|
-
}, root),
|
|
13704
|
+
...supplied?.catalogueAgentFor ? { catalogueAgentFor: supplied.catalogueAgentFor } : {},
|
|
13705
|
+
operatorOAuth,
|
|
12916
13706
|
nativeOnly: Boolean(nativeSocket),
|
|
12917
13707
|
connectionSettings,
|
|
12918
13708
|
operatorApiUrl: connection.apiUrl,
|
|
@@ -12952,7 +13742,7 @@ async function runAgentServer(argv) {
|
|
|
12952
13742
|
else console.error(`allowed origins: ${allowedOrigins.join(", ")}`);
|
|
12953
13743
|
if (nativeClient) console.error("native desktop client: authorized");
|
|
12954
13744
|
if (!nativeSocket) console.error("Connect from an allowed local-control client after operator authorization.");
|
|
12955
|
-
return await waitForAgentServerShutdown(runs, app, shutdownController, Boolean(values.supervised));
|
|
13745
|
+
return await waitForAgentServerShutdown(runs, app, shutdownController, Boolean(values.supervised), (dispose) => shutdownDisposers.push(dispose));
|
|
12956
13746
|
} catch (cause) {
|
|
12957
13747
|
await app.close().catch(() => void 0);
|
|
12958
13748
|
throw cause;
|
|
@@ -12975,10 +13765,14 @@ async function runAgentServer(argv) {
|
|
|
12975
13765
|
throw cause;
|
|
12976
13766
|
}
|
|
12977
13767
|
} finally {
|
|
12978
|
-
|
|
13768
|
+
try {
|
|
13769
|
+
await shutdownLogger();
|
|
13770
|
+
} finally {
|
|
13771
|
+
for (const dispose of shutdownDisposers) dispose();
|
|
13772
|
+
}
|
|
12979
13773
|
}
|
|
12980
13774
|
}
|
|
12981
|
-
function waitForAgentServerShutdown(runs, app, shutdownController, supervised) {
|
|
13775
|
+
function waitForAgentServerShutdown(runs, app, shutdownController, supervised, registerDisposer) {
|
|
12982
13776
|
return new Promise((resolvePromise) => {
|
|
12983
13777
|
let shuttingDown = false;
|
|
12984
13778
|
const shutdown = (source) => {
|
|
@@ -13009,8 +13803,6 @@ function waitForAgentServerShutdown(runs, app, shutdownController, supervised) {
|
|
|
13009
13803
|
if (forcedExitTimer) clearTimeout(forcedExitTimer);
|
|
13010
13804
|
const failures = results.filter((result) => result.status === "rejected");
|
|
13011
13805
|
for (const failure of failures) console.error(`shutdown cleanup failed: ${failure.reason.message}`);
|
|
13012
|
-
handlers.dispose();
|
|
13013
|
-
stdinGuard.dispose();
|
|
13014
13806
|
const exitCode = typeof process.exitCode === "number" ? process.exitCode : 0;
|
|
13015
13807
|
resolvePromise(failures.length > 0 ? 1 : exitCode);
|
|
13016
13808
|
})();
|
|
@@ -13023,6 +13815,10 @@ function waitForAgentServerShutdown(runs, app, shutdownController, supervised) {
|
|
|
13023
13815
|
enabled: supervised,
|
|
13024
13816
|
shutdown: () => shutdown("stdin")
|
|
13025
13817
|
});
|
|
13818
|
+
registerDisposer(() => {
|
|
13819
|
+
handlers.dispose();
|
|
13820
|
+
stdinGuard.dispose();
|
|
13821
|
+
});
|
|
13026
13822
|
});
|
|
13027
13823
|
}
|
|
13028
13824
|
/** Makes the supervising process's stdin pipe part of the server lifecycle. */
|
|
@@ -13391,13 +14187,24 @@ async function writeCache(cache) {
|
|
|
13391
14187
|
}
|
|
13392
14188
|
//#endregion
|
|
13393
14189
|
//#region src/version.ts
|
|
13394
|
-
var DAEMON_VERSION = "0.
|
|
14190
|
+
var DAEMON_VERSION = "0.64.0";
|
|
13395
14191
|
//#endregion
|
|
13396
14192
|
//#region src/cli.ts
|
|
13397
14193
|
async function runAgentDaemonCli(options) {
|
|
13398
|
-
const notice = legacyStoreNotice();
|
|
13399
|
-
if (notice) console.error(notice);
|
|
13400
14194
|
const [subcommand, ...rest] = options.argv ?? process.argv.slice(2);
|
|
14195
|
+
const help = subcommand === "--help" || subcommand === "-h" || [
|
|
14196
|
+
"poll",
|
|
14197
|
+
"drain",
|
|
14198
|
+
"once",
|
|
14199
|
+
"server",
|
|
14200
|
+
"providers",
|
|
14201
|
+
"sync-sessions"
|
|
14202
|
+
].includes(subcommand ?? "") && isHelpFlag(rest);
|
|
14203
|
+
const notice = legacyStoreNotice();
|
|
14204
|
+
if (notice && !help) {
|
|
14205
|
+
console.error(notice);
|
|
14206
|
+
markLegacyStoreNoticeShown();
|
|
14207
|
+
}
|
|
13401
14208
|
if (["poll", "drain"].includes(subcommand ?? "") && !isWorkspaceInvocation()) checkDaemonUpdate({ currentVersion: DAEMON_VERSION }).then((update) => {
|
|
13402
14209
|
if (update.updateAvailable) pino({ name: "moltnet.agent-daemon" }).info({
|
|
13403
14210
|
event: "agent-daemon.update_available",
|