@themoltnet/agent-daemon 0.63.1 → 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.js +1129 -345
- 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()]),
|
|
@@ -3827,14 +3827,12 @@ function loadAgentServerEnvConfig(root) {
|
|
|
3827
3827
|
const issuer = process.env["MOLTNET_OPERATOR_OAUTH_ISSUER"];
|
|
3828
3828
|
const publicUrl = process.env["MOLTNET_OPERATOR_OAUTH_PUBLIC_URL"] ?? issuer;
|
|
3829
3829
|
const nativeClientId = process.env["MOLTNET_NATIVE_OAUTH_CLIENT_ID"];
|
|
3830
|
-
const consoleClientId = process.env["MOLTNET_CONSOLE_OAUTH_CLIENT_ID"];
|
|
3831
3830
|
const apiUrl = process.env["MOLTNET_OPERATOR_API_URL"];
|
|
3832
3831
|
return {
|
|
3833
3832
|
operatorOAuth: {
|
|
3834
3833
|
...issuer ? { issuer } : {},
|
|
3835
3834
|
...publicUrl ? { publicUrl } : {},
|
|
3836
3835
|
...nativeClientId ? { nativeClientId } : {},
|
|
3837
|
-
...consoleClientId ? { consoleClientId } : {},
|
|
3838
3836
|
...apiUrl ? { apiUrl } : {}
|
|
3839
3837
|
},
|
|
3840
3838
|
port: process.env["MOLTNET_AGENT_SERVER_PORT"] ?? "",
|
|
@@ -3862,7 +3860,8 @@ function loadUpdateEnvConfig(root) {
|
|
|
3862
3860
|
var execFileAsync$1 = promisify(execFile);
|
|
3863
3861
|
var GIT_TIMEOUT_MS = 1e4;
|
|
3864
3862
|
var GIT_MAX_OUTPUT_BYTES = 64 * 1024;
|
|
3865
|
-
|
|
3863
|
+
/** `signal` stops the `git` process; an abort is reported as an abort. */
|
|
3864
|
+
async function validateGitSource(source, signal) {
|
|
3866
3865
|
const inherited = processEnvSnapshot();
|
|
3867
3866
|
try {
|
|
3868
3867
|
const { stdout } = await execFileAsync$1("git", [
|
|
@@ -3880,15 +3879,37 @@ async function validateGitSource(source) {
|
|
|
3880
3879
|
},
|
|
3881
3880
|
timeout: GIT_TIMEOUT_MS,
|
|
3882
3881
|
killSignal: "SIGKILL",
|
|
3883
|
-
maxBuffer: GIT_MAX_OUTPUT_BYTES
|
|
3882
|
+
maxBuffer: GIT_MAX_OUTPUT_BYTES,
|
|
3883
|
+
...signal ? { signal } : {}
|
|
3884
3884
|
});
|
|
3885
3885
|
const top = stdout.trim().split("\n")[0];
|
|
3886
3886
|
if (await canonicalDirectory(top) === source) return;
|
|
3887
3887
|
} catch (cause) {
|
|
3888
|
+
if (signal?.aborted) throw cause;
|
|
3888
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 });
|
|
3889
3890
|
}
|
|
3890
3891
|
throw new ProjectConfigError("selection", `git-worktree source ${source} must be the Git repository root, not a subdirectory`);
|
|
3891
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
|
+
}
|
|
3892
3913
|
function projectRunOptionDefs() {
|
|
3893
3914
|
return {
|
|
3894
3915
|
project: { type: "string" },
|
|
@@ -3900,13 +3921,16 @@ function projectRunOptionDefs() {
|
|
|
3900
3921
|
"workspace-strategy": { type: "string" }
|
|
3901
3922
|
};
|
|
3902
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
|
+
}
|
|
3903
3928
|
function strategy(value) {
|
|
3904
3929
|
if (value === void 0) return void 0;
|
|
3905
3930
|
if (WORKSPACE_STRATEGIES.includes(value)) return value;
|
|
3906
3931
|
throw new ProjectConfigError("validation", `Unknown workspace strategy ${value}; choose ${WORKSPACE_STRATEGIES.join(", ")}`);
|
|
3907
3932
|
}
|
|
3908
|
-
|
|
3909
|
-
async function resolveRunProjectSelection(args) {
|
|
3933
|
+
async function resolveRunProjectSelection(args, options = {}) {
|
|
3910
3934
|
const env = processEnvSnapshot();
|
|
3911
3935
|
const inherited = env.MOLTNET_ACTIVE_IDENTITY === args.agent && !args.general;
|
|
3912
3936
|
const bindingName = args.binding ?? (!args["config-file"] && !args.project && inherited ? env.MOLTNET_PROJECT_BINDING : void 0);
|
|
@@ -3938,8 +3962,10 @@ async function resolveRunProjectSelection(args) {
|
|
|
3938
3962
|
if (workspaceStrategy === "none" && args.source !== void 0) throw new ProjectConfigError("selection", "No-workspace execution cannot specify a source");
|
|
3939
3963
|
let source = binding?.source;
|
|
3940
3964
|
if (workspaceStrategy !== "none" && !source) source = await canonicalDirectory(resolve(args.cwd, args.source ?? "."));
|
|
3941
|
-
|
|
3942
|
-
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);
|
|
3943
3969
|
return {
|
|
3944
3970
|
configPath,
|
|
3945
3971
|
selectedBy: args.binding || args.project ? "explicit" : bindingName ? "activation" : binding ? "ancestor" : "general",
|
|
@@ -3956,8 +3982,8 @@ async function resolveRunProjectSelection(args) {
|
|
|
3956
3982
|
/** A selected location fixes this worker's strategy; saved profiles are never mutated. */
|
|
3957
3983
|
function applyProjectWorkspacePolicy(profile, selection) {
|
|
3958
3984
|
if (!selection.workspaceExplicit) return profile;
|
|
3959
|
-
|
|
3960
|
-
if (
|
|
3985
|
+
const blocker = preparationBlocker(selection.strategy, selection.binding?.hooks);
|
|
3986
|
+
if (blocker) throw new Error(`Workspace preparation: ${blocker.message}`);
|
|
3961
3987
|
const mode = selection.strategy === "existing" ? "shared_mount" : selection.strategy === "git-worktree" ? "dedicated_worktree" : "none";
|
|
3962
3988
|
if (profile.allowedWorkspaceModes.length && !profile.allowedWorkspaceModes.includes(mode)) throw new Error(`Workspace strategy ${selection.strategy} is not allowed by profile ${profile.name}`);
|
|
3963
3989
|
return {
|
|
@@ -5690,6 +5716,8 @@ function assertProviderEnvName(providerId, value) {
|
|
|
5690
5716
|
if (value !== expected) throw new AgentServerStoreError("invalid_state", `provider envName must be ${expected}`);
|
|
5691
5717
|
return value;
|
|
5692
5718
|
}
|
|
5719
|
+
/** The run follows its runtime profile's own workspace mode. */
|
|
5720
|
+
var PROFILE_DEFAULT_STRATEGY = "profile-default";
|
|
5693
5721
|
function readJson(path) {
|
|
5694
5722
|
let raw;
|
|
5695
5723
|
try {
|
|
@@ -9266,8 +9294,7 @@ var RELEASE_CONNECTION = {
|
|
|
9266
9294
|
apiUrl: "https://api.themolt.net",
|
|
9267
9295
|
issuer: "https://auth.themolt.net",
|
|
9268
9296
|
publicUrl: "https://auth.themolt.net",
|
|
9269
|
-
nativeClientId: "moltnet-native"
|
|
9270
|
-
consoleClientId: "moltnet-console"
|
|
9297
|
+
nativeClientId: "moltnet-native"
|
|
9271
9298
|
};
|
|
9272
9299
|
var KEYS = Object.keys(RELEASE_CONNECTION);
|
|
9273
9300
|
function validateConnectionOverrides(value) {
|
|
@@ -9564,7 +9591,7 @@ var OperatorOAuth = class {
|
|
|
9564
9591
|
const command = process.platform === "darwin" ? "open" : process.platform === "win32" ? "explorer.exe" : "xdg-open";
|
|
9565
9592
|
return new Promise((resolve, reject) => {
|
|
9566
9593
|
execFile(command, [url], (error) => {
|
|
9567
|
-
if (error) reject(/* @__PURE__ */ new Error("Could not open
|
|
9594
|
+
if (error) reject(/* @__PURE__ */ new Error("Could not open browser approval"));
|
|
9568
9595
|
else resolve();
|
|
9569
9596
|
});
|
|
9570
9597
|
});
|
|
@@ -9594,22 +9621,14 @@ var OperatorOAuth = class {
|
|
|
9594
9621
|
cancel() {
|
|
9595
9622
|
this.pending?.abort();
|
|
9596
9623
|
}
|
|
9624
|
+
operatorConfigured() {
|
|
9625
|
+
return this.operator !== null;
|
|
9626
|
+
}
|
|
9597
9627
|
removeOperator() {
|
|
9598
9628
|
this.cancel();
|
|
9599
9629
|
rmSync(join(this.root, "operator.json"), { force: true });
|
|
9600
9630
|
this.operator = null;
|
|
9601
9631
|
}
|
|
9602
|
-
metadata() {
|
|
9603
|
-
return {
|
|
9604
|
-
protocolVersion: OPERATOR_OAUTH.protocolVersion,
|
|
9605
|
-
instance: this.instance,
|
|
9606
|
-
issuer: this.config.issuer,
|
|
9607
|
-
authorizationUrl: this.config.authorizationUrl,
|
|
9608
|
-
tokenUrl: this.config.tokenUrl,
|
|
9609
|
-
clientId: this.config.consoleClientId,
|
|
9610
|
-
operatorConfigured: !!this.operator
|
|
9611
|
-
};
|
|
9612
|
-
}
|
|
9613
9632
|
async verify(token, scope, clientId) {
|
|
9614
9633
|
const { payload } = await jwtVerify(token, this.keys, {
|
|
9615
9634
|
algorithms: ["RS256"],
|
|
@@ -9620,7 +9639,7 @@ var OperatorOAuth = class {
|
|
|
9620
9639
|
"iat",
|
|
9621
9640
|
"sub"
|
|
9622
9641
|
],
|
|
9623
|
-
maxTokenAge:
|
|
9642
|
+
maxTokenAge: OPERATOR_OAUTH.nativeLifetimeSeconds
|
|
9624
9643
|
});
|
|
9625
9644
|
const claims = payload.ext;
|
|
9626
9645
|
const scopes = typeof payload.scope === "string" ? payload.scope.split(" ") : payload.scp;
|
|
@@ -9631,10 +9650,6 @@ var OperatorOAuth = class {
|
|
|
9631
9650
|
provisioning: claims["moltnet:provisioning"]
|
|
9632
9651
|
};
|
|
9633
9652
|
}
|
|
9634
|
-
async verifyBrowser(token) {
|
|
9635
|
-
const operator = await this.verify(token, LOCAL_SCOPE, this.config.consoleClientId);
|
|
9636
|
-
if (!this.operator || operator.issuer !== this.operator.issuer || operator.subject !== this.operator.subject) throw new InvalidOperatorGrantError("Native operator sign-in required");
|
|
9637
|
-
}
|
|
9638
9653
|
async authorize(grant, signal) {
|
|
9639
9654
|
if (this.active) throw new Error("An approval is already pending");
|
|
9640
9655
|
this.active = true;
|
|
@@ -9688,9 +9703,9 @@ var OperatorOAuth = class {
|
|
|
9688
9703
|
...grant ? { provisioning: JSON.stringify(grant) } : {}
|
|
9689
9704
|
})) url.searchParams.set(key, value);
|
|
9690
9705
|
try {
|
|
9691
|
-
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")));
|
|
9692
9707
|
} catch {
|
|
9693
|
-
reject(/* @__PURE__ */ new Error("Could not open
|
|
9708
|
+
reject(/* @__PURE__ */ new Error("Could not open browser approval"));
|
|
9694
9709
|
}
|
|
9695
9710
|
});
|
|
9696
9711
|
});
|
|
@@ -10101,6 +10116,94 @@ function snapshot(login) {
|
|
|
10101
10116
|
};
|
|
10102
10117
|
}
|
|
10103
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
|
|
10104
10207
|
//#region src/lib/agent-server/identity.ts
|
|
10105
10208
|
/**
|
|
10106
10209
|
* AgentServer identity activation (#2061/#1834 boundary).
|
|
@@ -10544,7 +10647,7 @@ async function verifyTeamActivation(store, alias, managed, external, connectImpl
|
|
|
10544
10647
|
if (missing.length) throw new TeamCredentialError({
|
|
10545
10648
|
code: "agent_key_scopes_insufficient",
|
|
10546
10649
|
message: `This credential lacks ${missing.join(", ")}.`,
|
|
10547
|
-
remedy: "Renew through
|
|
10650
|
+
remedy: "Renew through browser approval with the required desktop scopes."
|
|
10548
10651
|
});
|
|
10549
10652
|
activated.boundTeamId = teamId;
|
|
10550
10653
|
return captureTeamCredential(activated, {
|
|
@@ -10554,6 +10657,367 @@ async function verifyTeamActivation(store, alias, managed, external, connectImpl
|
|
|
10554
10657
|
});
|
|
10555
10658
|
}
|
|
10556
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
|
|
10557
11021
|
//#region src/lib/agent-server/runs.ts
|
|
10558
11022
|
var STOP_GRACE_MS = 1e4;
|
|
10559
11023
|
var STOP_FORCE_MS = 2e3;
|
|
@@ -10642,7 +11106,7 @@ var RunManager = class {
|
|
|
10642
11106
|
};
|
|
10643
11107
|
}
|
|
10644
11108
|
/** Assemble child env + args for a run. Exposed for tests. */
|
|
10645
|
-
async prepare(spec, agent, piDir, providers = this.store.readProviders(), runtimeModule) {
|
|
11109
|
+
async prepare(spec, agent, piDir, providers = this.store.readProviders(), runtimeModule, workspace) {
|
|
10646
11110
|
const { activation, config } = agent;
|
|
10647
11111
|
const homeDir = join(dirname(piDir), "home");
|
|
10648
11112
|
const env = {
|
|
@@ -10687,7 +11151,16 @@ var RunManager = class {
|
|
|
10687
11151
|
String(runtimeSettings.heartbeatIntervalMs),
|
|
10688
11152
|
"--warm-retention-sec",
|
|
10689
11153
|
String(runtimeSettings.warmRetentionSec),
|
|
10690
|
-
...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
|
+
}) : []
|
|
10691
11164
|
];
|
|
10692
11165
|
env["MOLTNET_AGENT_KEY"] = requireCredentialSnapshot(agent).agentKey;
|
|
10693
11166
|
env["MOLTNET_API_URL"] = activation.apiUrl ?? (activation.source === "external" ? activation.configApiUrl : "");
|
|
@@ -10718,10 +11191,11 @@ var RunManager = class {
|
|
|
10718
11191
|
return {
|
|
10719
11192
|
args,
|
|
10720
11193
|
env,
|
|
10721
|
-
cwd: target.cwd
|
|
11194
|
+
cwd: workspace?.source ?? (workspace ? join(dirname(piDir), "workspace") : target.cwd)
|
|
10722
11195
|
};
|
|
10723
11196
|
}
|
|
10724
11197
|
async start(spec, signal) {
|
|
11198
|
+
spec = structuredClone(spec);
|
|
10725
11199
|
validateRunSpec(spec);
|
|
10726
11200
|
const releaseStart = this.reserveStart(spec.agent);
|
|
10727
11201
|
try {
|
|
@@ -10732,24 +11206,65 @@ var RunManager = class {
|
|
|
10732
11206
|
}
|
|
10733
11207
|
async startReserved(spec, signal) {
|
|
10734
11208
|
this.assertStartOpen(signal);
|
|
10735
|
-
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);
|
|
10736
11213
|
if (cause instanceof TeamCredentialError || cause instanceof AgentServerStoreError) throw cause;
|
|
10737
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.`);
|
|
10738
11215
|
});
|
|
10739
|
-
this.assertStartOpen(signal);
|
|
11216
|
+
this.assertStartOpen(signal, deadline);
|
|
10740
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}`);
|
|
10741
11218
|
const id = `${Date.now().toString(36)}-${randomBytes(4).toString("hex")}`;
|
|
10742
|
-
const runDir = this.store.runDir(id);
|
|
11219
|
+
const runDir = resolve(this.store.runDir(id));
|
|
10743
11220
|
const piDir = join(runDir, "pi");
|
|
10744
|
-
const providers = this.store.readProviders();
|
|
10745
|
-
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;
|
|
10746
|
-
const { args, env, cwd } = await this.prepare(spec, agent, piDir, providers, runtimeModule);
|
|
10747
|
-
this.assertStartOpen(signal);
|
|
10748
11221
|
let child;
|
|
10749
11222
|
let logStream;
|
|
10750
11223
|
let logLimiter;
|
|
11224
|
+
let workspace;
|
|
11225
|
+
let effective = spec;
|
|
11226
|
+
let selection;
|
|
10751
11227
|
try {
|
|
10752
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);
|
|
10753
11268
|
for (const dir of [
|
|
10754
11269
|
env.HOME,
|
|
10755
11270
|
env.XDG_CACHE_HOME,
|
|
@@ -10780,7 +11295,7 @@ var RunManager = class {
|
|
|
10780
11295
|
child?.kill("SIGKILL");
|
|
10781
11296
|
});
|
|
10782
11297
|
const spawnImpl = this.options.spawnImpl ?? spawn;
|
|
10783
|
-
this.assertStartOpen(signal);
|
|
11298
|
+
this.assertStartOpen(signal, deadline);
|
|
10784
11299
|
child = spawnImpl(entry.execPath, [
|
|
10785
11300
|
...entry.execArgv,
|
|
10786
11301
|
entry.scriptPath,
|
|
@@ -10808,6 +11323,7 @@ var RunManager = class {
|
|
|
10808
11323
|
child.stderr?.pipe(logLimiter, { end: false });
|
|
10809
11324
|
const record = {
|
|
10810
11325
|
...spec,
|
|
11326
|
+
...workspace ? { workspace } : {},
|
|
10811
11327
|
id,
|
|
10812
11328
|
status: "running",
|
|
10813
11329
|
pid: child.pid,
|
|
@@ -10850,6 +11366,7 @@ var RunManager = class {
|
|
|
10850
11366
|
this.store.writeRun(record);
|
|
10851
11367
|
this.log("info", "agent server run started", {
|
|
10852
11368
|
...runContext(id, spec.agent, child),
|
|
11369
|
+
...selectionContext(spec, workspace),
|
|
10853
11370
|
transition: "running"
|
|
10854
11371
|
});
|
|
10855
11372
|
return record;
|
|
@@ -10864,20 +11381,29 @@ var RunManager = class {
|
|
|
10864
11381
|
});
|
|
10865
11382
|
this.log("error", "agent server run failed to start", {
|
|
10866
11383
|
...runContext(id, spec.agent, child),
|
|
11384
|
+
...selectionContext(spec, workspace),
|
|
10867
11385
|
transition: "start_failed",
|
|
10868
11386
|
...safeRunError(cause)
|
|
10869
11387
|
});
|
|
11388
|
+
if (!signal?.aborted && !this.closing && !(cause instanceof AgentServerHttpError) && causedByAbort(cause, deadline)) throw startTimedOut(cause);
|
|
10870
11389
|
throw cause;
|
|
10871
11390
|
}
|
|
10872
11391
|
}
|
|
10873
|
-
async resolveRuntimeModule(spec, activated, cwd) {
|
|
10874
|
-
const
|
|
11392
|
+
async resolveRuntimeModule(spec, activated, cwd, selection) {
|
|
11393
|
+
const effectiveProfiles = (await resolveRuntimeProfiles({
|
|
10875
11394
|
agent: await this.connectAgent(activated, spec.teamId),
|
|
10876
11395
|
profiles: spec.profiles,
|
|
10877
11396
|
teamId: spec.teamId,
|
|
10878
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
|
+
}
|
|
10879
11405
|
});
|
|
10880
|
-
const kinds = [...new Set(
|
|
11406
|
+
const kinds = [...new Set(effectiveProfiles.map((profile) => profile.runtimeKind))];
|
|
10881
11407
|
if (kinds.length !== 1) throw new AgentServerRunError("invalid_spec", "All profiles in a server-managed run must use the same runtime kind.");
|
|
10882
11408
|
const kind = kinds[0];
|
|
10883
11409
|
let registration;
|
|
@@ -11022,8 +11548,10 @@ var RunManager = class {
|
|
|
11022
11548
|
else this.startingByAgent.delete(agent);
|
|
11023
11549
|
};
|
|
11024
11550
|
}
|
|
11025
|
-
|
|
11551
|
+
/** Shutdown or disconnect wins; otherwise an expired budget is a retryable 503. */
|
|
11552
|
+
assertStartOpen(signal, deadline, cause) {
|
|
11026
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);
|
|
11027
11555
|
}
|
|
11028
11556
|
pruneCompletedRuns() {
|
|
11029
11557
|
const removed = this.store.pruneCompletedRuns({
|
|
@@ -11055,11 +11583,41 @@ var RunManager = class {
|
|
|
11055
11583
|
});
|
|
11056
11584
|
}
|
|
11057
11585
|
}
|
|
11058
|
-
|
|
11059
|
-
|
|
11060
|
-
|
|
11061
|
-
|
|
11062
|
-
|
|
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
|
+
};
|
|
11620
|
+
function createByteLimitTransform(limit, onTruncated) {
|
|
11063
11621
|
const byteLimit = Math.max(0, Math.floor(limit));
|
|
11064
11622
|
const contentLimit = Math.max(0, byteLimit - LOG_TRUNCATION_MARKER.length);
|
|
11065
11623
|
let emitted = 0;
|
|
@@ -11082,6 +11640,33 @@ function createByteLimitTransform(limit, onTruncated) {
|
|
|
11082
11640
|
callback();
|
|
11083
11641
|
} });
|
|
11084
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
|
+
}
|
|
11085
11670
|
function runContext(runId, agent, child) {
|
|
11086
11671
|
return {
|
|
11087
11672
|
runId,
|
|
@@ -11309,112 +11894,6 @@ function createAgentServerSecretProviders(settings, store) {
|
|
|
11309
11894
|
};
|
|
11310
11895
|
}
|
|
11311
11896
|
//#endregion
|
|
11312
|
-
//#region src/lib/agent-server/readiness.ts
|
|
11313
|
-
/**
|
|
11314
|
-
* Whether a runtime profile can execute on *this* machine.
|
|
11315
|
-
*
|
|
11316
|
-
* A profile is authored in Console against a team; whether it can run depends
|
|
11317
|
-
* on local facts only the server knows — which provider keys are configured
|
|
11318
|
-
* here and which runtime kinds this machine can produce.
|
|
11319
|
-
*
|
|
11320
|
-
* The prerequisite comparison itself is **not** reimplemented here. Run start
|
|
11321
|
-
* calls `validateRuntimeProfilePrerequisites`, and the catalogue calls the same
|
|
11322
|
-
* function, so the composer cannot promise a run that startup would reject for
|
|
11323
|
-
* a reason the two evaluated differently.
|
|
11324
|
-
*/
|
|
11325
|
-
function deriveProfileReadiness(profile, machine) {
|
|
11326
|
-
const blockers = [];
|
|
11327
|
-
const env = {};
|
|
11328
|
-
for (const [name, configured] of machine.providerEnv) if (configured) env[name] = "configured";
|
|
11329
|
-
try {
|
|
11330
|
-
validateRuntimeProfilePrerequisites(profile, env, {
|
|
11331
|
-
tools: machine.inventory?.tools ?? profile.requiredTools,
|
|
11332
|
-
executables: machine.inventory?.executables ?? profile.requiredExecutables
|
|
11333
|
-
});
|
|
11334
|
-
} catch (error) {
|
|
11335
|
-
if (!(error instanceof RuntimeProfilePrerequisiteError)) throw error;
|
|
11336
|
-
for (const name of error.missingEnv) blockers.push({
|
|
11337
|
-
code: "env_missing",
|
|
11338
|
-
message: `${name} is not configured on this machine.`,
|
|
11339
|
-
remedy: "Add the key under Providers, then reopen this run."
|
|
11340
|
-
});
|
|
11341
|
-
for (const name of error.missingTools) blockers.push({
|
|
11342
|
-
code: "tool_missing",
|
|
11343
|
-
message: `The runtime does not provide the tool ${name}.`,
|
|
11344
|
-
remedy: `Use a profile whose runtime provides ${name}, or change the profile in Console.`
|
|
11345
|
-
});
|
|
11346
|
-
for (const name of error.missingExecutables) blockers.push({
|
|
11347
|
-
code: "executable_missing",
|
|
11348
|
-
message: `The runtime does not provide the executable ${name}.`,
|
|
11349
|
-
remedy: `Use a runtime that ships ${name}, or drop the requirement in Console.`
|
|
11350
|
-
});
|
|
11351
|
-
}
|
|
11352
|
-
if (!machine.runtimeKinds.has(profile.runtimeKind)) blockers.push({
|
|
11353
|
-
code: "runtime_unregistered",
|
|
11354
|
-
message: `Runtime kind ${profile.runtimeKind} is not available on this machine.`,
|
|
11355
|
-
remedy: "Register the runtime under Runtimes, then reopen this run."
|
|
11356
|
-
});
|
|
11357
|
-
return {
|
|
11358
|
-
ready: blockers.length === 0,
|
|
11359
|
-
blockers
|
|
11360
|
-
};
|
|
11361
|
-
}
|
|
11362
|
-
//#endregion
|
|
11363
|
-
//#region src/lib/agent-server/catalogue.ts
|
|
11364
|
-
async function buildCatalogue(options) {
|
|
11365
|
-
const { agent, machine, identityDefault } = options;
|
|
11366
|
-
const entries = await Promise.all(agent.teamIds.map(async (teamId) => {
|
|
11367
|
-
try {
|
|
11368
|
-
const result = await agent.readTeam(teamId);
|
|
11369
|
-
if (result.team.id !== teamId) throw new Error("Team response mismatch");
|
|
11370
|
-
const diaries = result.diaries.filter((diary) => diary.teamId === teamId).map(({ id, name }) => ({
|
|
11371
|
-
id,
|
|
11372
|
-
name
|
|
11373
|
-
}));
|
|
11374
|
-
return {
|
|
11375
|
-
team: {
|
|
11376
|
-
teamId,
|
|
11377
|
-
teamName: result.team.name,
|
|
11378
|
-
available: true,
|
|
11379
|
-
blockers: [],
|
|
11380
|
-
credential: result.credential,
|
|
11381
|
-
diaries,
|
|
11382
|
-
defaultDiaryId: resolveDefaultDiary(teamId, diaries, identityDefault)
|
|
11383
|
-
},
|
|
11384
|
-
profiles: result.profiles.filter((profile) => profile.teamId === teamId).map((profile) => ({
|
|
11385
|
-
...profile,
|
|
11386
|
-
...deriveProfileReadiness(profile, machine)
|
|
11387
|
-
}))
|
|
11388
|
-
};
|
|
11389
|
-
} catch (error) {
|
|
11390
|
-
return {
|
|
11391
|
-
team: {
|
|
11392
|
-
teamId,
|
|
11393
|
-
teamName: teamId,
|
|
11394
|
-
available: false,
|
|
11395
|
-
blockers: [credentialBlocker(error)],
|
|
11396
|
-
credential: agent.lastVerified(teamId),
|
|
11397
|
-
diaries: [],
|
|
11398
|
-
defaultDiaryId: null
|
|
11399
|
-
},
|
|
11400
|
-
profiles: []
|
|
11401
|
-
};
|
|
11402
|
-
}
|
|
11403
|
-
}));
|
|
11404
|
-
const teams = entries.map(({ team }) => team);
|
|
11405
|
-
const available = teams.filter((team) => team.available);
|
|
11406
|
-
return {
|
|
11407
|
-
teams,
|
|
11408
|
-
defaultTeamId: available.find((team) => team.teamId === identityDefault.teamId)?.teamId ?? available[0]?.teamId ?? null,
|
|
11409
|
-
profiles: entries.flatMap((entry) => entry.profiles)
|
|
11410
|
-
};
|
|
11411
|
-
}
|
|
11412
|
-
function resolveDefaultDiary(teamId, diaries, identityDefault) {
|
|
11413
|
-
const bound = diaries.find((diary) => diary.id === identityDefault.diaryId);
|
|
11414
|
-
if (bound && identityDefault.teamId === teamId) return bound.id;
|
|
11415
|
-
return diaries.length === 1 ? diaries[0]?.id ?? null : null;
|
|
11416
|
-
}
|
|
11417
|
-
//#endregion
|
|
11418
11897
|
//#region src/lib/agent-server/enrollment.ts
|
|
11419
11898
|
/** Native callers receive metadata only; approval and storage stay local. */
|
|
11420
11899
|
async function enrollIdentityTeam(options) {
|
|
@@ -11499,37 +11978,139 @@ async function enrollIdentityTeam(options) {
|
|
|
11499
11978
|
}
|
|
11500
11979
|
}
|
|
11501
11980
|
//#endregion
|
|
11502
|
-
//#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
|
+
};
|
|
11503
12100
|
/**
|
|
11504
|
-
*
|
|
11505
|
-
*
|
|
11506
|
-
* This mirrors the Go CLI's `identityDefaultBinding` (`project_selection.go`),
|
|
11507
|
-
* which is the fallback the CLI uses when a working directory has no
|
|
11508
|
-
* registered project binding.
|
|
11509
|
-
*
|
|
11510
|
-
* The desktop cannot use the CLI's *location* bindings at all — its composer
|
|
11511
|
-
* has no working directory to key on — but it can honour this identity-wide
|
|
11512
|
-
* default, so an operator sees their familiar team preselected rather than an
|
|
11513
|
-
* arbitrary first entry.
|
|
11514
|
-
*
|
|
11515
|
-
* Like the CLI, a half-filled pair is treated as no binding: a team without a
|
|
11516
|
-
* 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`.
|
|
11517
12103
|
*/
|
|
11518
|
-
function
|
|
11519
|
-
let contents;
|
|
12104
|
+
async function stored(work) {
|
|
11520
12105
|
try {
|
|
11521
|
-
|
|
11522
|
-
} catch {
|
|
11523
|
-
|
|
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;
|
|
11524
12110
|
}
|
|
11525
|
-
|
|
11526
|
-
|
|
11527
|
-
|
|
11528
|
-
if (!teamId || !diaryId) return {};
|
|
11529
|
-
return {
|
|
11530
|
-
teamId,
|
|
11531
|
-
diaryId
|
|
11532
|
-
};
|
|
12111
|
+
}
|
|
12112
|
+
function conflict(message) {
|
|
12113
|
+
return new AgentServerHttpError(409, "location_conflict", message);
|
|
11533
12114
|
}
|
|
11534
12115
|
//#endregion
|
|
11535
12116
|
//#region src/lib/agent-server/protocol.ts
|
|
@@ -11631,10 +12212,83 @@ var AgentServerCatalogueProfileSchema = Type.Intersect([Type.Pick(RuntimeProfile
|
|
|
11631
12212
|
var AgentServerCatalogueSchema = Type.Object({
|
|
11632
12213
|
teams: Type.Array(schemaRef(AgentServerCatalogueTeamSchema)),
|
|
11633
12214
|
defaultTeamId: Type.Union([Type.String(), Type.Null()]),
|
|
11634
|
-
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
|
+
}))
|
|
11635
12234
|
}, { $id: "AgentServerCatalogue" });
|
|
11636
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
|
+
});
|
|
11637
12289
|
var AgentServerRunRecordSchema = Type.Object({
|
|
12290
|
+
...RunProjectFields,
|
|
12291
|
+
workspace: Type.Optional(RunWorkspaceSchema),
|
|
11638
12292
|
id: Type.String(),
|
|
11639
12293
|
agent: Type.String(),
|
|
11640
12294
|
teamId: Type.String(),
|
|
@@ -11714,6 +12368,7 @@ var PutProviderSchema = Type.Object({
|
|
|
11714
12368
|
});
|
|
11715
12369
|
var DiscoverModelsSchema = Type.Object({ models: ProviderModelList }, { $id: "DiscoveredModels" });
|
|
11716
12370
|
var StartRunSchema = Type.Object({
|
|
12371
|
+
...RunProjectFields,
|
|
11717
12372
|
agent: Type.String(),
|
|
11718
12373
|
teamId: Type.String(),
|
|
11719
12374
|
diaryId: Type.Optional(Type.String()),
|
|
@@ -11730,6 +12385,7 @@ var LogStreamSchema = Type.String({
|
|
|
11730
12385
|
contentMediaType: "text/event-stream"
|
|
11731
12386
|
});
|
|
11732
12387
|
var AGENT_SERVER_SCHEMAS = [
|
|
12388
|
+
AgentServerProjectLocationSchema,
|
|
11733
12389
|
AgentServerHealthSchema,
|
|
11734
12390
|
AgentServerProblemSchema,
|
|
11735
12391
|
AgentServerAgentSchema,
|
|
@@ -11752,6 +12408,39 @@ var AGENT_SERVER_SCHEMAS = [
|
|
|
11752
12408
|
var localControlSecurity = [{ agentServerToken: [] }];
|
|
11753
12409
|
var problemResponse = { default: schemaRef(AgentServerProblemSchema) };
|
|
11754
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
|
+
},
|
|
11755
12444
|
health: {
|
|
11756
12445
|
operationId: "getAgentServerHealth",
|
|
11757
12446
|
tags: ["system"],
|
|
@@ -11925,6 +12614,7 @@ var AgentServerRouteSchemas = {
|
|
|
11925
12614
|
operationId: "startAgentServerRun",
|
|
11926
12615
|
tags: ["runs"],
|
|
11927
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`.",
|
|
11928
12618
|
body: StartRunSchema,
|
|
11929
12619
|
response: {
|
|
11930
12620
|
201: schemaRef(AgentServerRunSchema),
|
|
@@ -11959,8 +12649,8 @@ var AgentServerRouteSchemas = {
|
|
|
11959
12649
|
* loopback-companion security profile (#2066): loopback Host enforcement,
|
|
11960
12650
|
* exact-origin CORS, Fetch-Metadata guards, strict JSON parsing.
|
|
11961
12651
|
*
|
|
11962
|
-
* Control routes require
|
|
11963
|
-
*
|
|
12652
|
+
* Control routes require the process-scoped native Desktop grant. OAuth is
|
|
12653
|
+
* used only for native operator sign-in and credential provisioning.
|
|
11964
12654
|
*/
|
|
11965
12655
|
var AGENT_SERVER_TOKEN_HEADER = "x-moltnet-agent-server-token";
|
|
11966
12656
|
var BODY_LIMIT = 64 * 1024;
|
|
@@ -12019,14 +12709,6 @@ async function readAgentServerLogDelta(handle, state, limit = LOG_READ_LIMIT_BYT
|
|
|
12019
12709
|
omitted
|
|
12020
12710
|
};
|
|
12021
12711
|
}
|
|
12022
|
-
var AgentServerHttpError = class extends Error {
|
|
12023
|
-
name = "AgentServerHttpError";
|
|
12024
|
-
constructor(statusCode, code, message) {
|
|
12025
|
-
super(message);
|
|
12026
|
-
this.statusCode = statusCode;
|
|
12027
|
-
this.code = code;
|
|
12028
|
-
}
|
|
12029
|
-
};
|
|
12030
12712
|
function requireBody(request) {
|
|
12031
12713
|
const body = request.body;
|
|
12032
12714
|
if (typeof body !== "object" || body === null || Array.isArray(body)) throw new AgentServerHttpError(400, "invalid_body", "JSON object body required");
|
|
@@ -12112,22 +12794,6 @@ function buildAgentServer(input) {
|
|
|
12112
12794
|
isOriginAllowed: (origin) => origin === "moltnet-agent-desktop://native" || origin === options.selfOrigin || browserOrigins.has(origin)
|
|
12113
12795
|
});
|
|
12114
12796
|
}
|
|
12115
|
-
let verificationWindow = Date.now();
|
|
12116
|
-
let verifications = 0;
|
|
12117
|
-
const browserVerification = /* @__PURE__ */ new WeakMap();
|
|
12118
|
-
function verifyBrowser(request, token) {
|
|
12119
|
-
const previous = browserVerification.get(request);
|
|
12120
|
-
if (previous) return previous;
|
|
12121
|
-
if (Date.now() - verificationWindow >= RATE_LIMIT_WINDOW_MS) {
|
|
12122
|
-
verificationWindow = Date.now();
|
|
12123
|
-
verifications = 0;
|
|
12124
|
-
}
|
|
12125
|
-
if (++verifications > RATE_LIMIT_MAX) throw new AgentServerHttpError(429, "rate_limited", "Too many authorization attempts");
|
|
12126
|
-
if (!oauth) throw new AgentServerHttpError(503, "oauth_unavailable", "Local OAuth is not configured");
|
|
12127
|
-
const pending = oauth.verifyBrowser(token);
|
|
12128
|
-
browserVerification.set(request, pending);
|
|
12129
|
-
return pending;
|
|
12130
|
-
}
|
|
12131
12797
|
function hasValidNativeGrant(origin, token) {
|
|
12132
12798
|
if (origin !== "moltnet-agent-desktop://native" || typeof token !== "string" || token.length === 0) return false;
|
|
12133
12799
|
try {
|
|
@@ -12158,56 +12824,20 @@ function buildAgentServer(input) {
|
|
|
12158
12824
|
max: options.rateLimitMax ?? RATE_LIMIT_MAX,
|
|
12159
12825
|
timeWindow: RATE_LIMIT_WINDOW_MS,
|
|
12160
12826
|
errorResponseBuilder: () => new AgentServerHttpError(429, "rate_limited", "Too many requests"),
|
|
12161
|
-
keyGenerator:
|
|
12827
|
+
keyGenerator: (request) => {
|
|
12162
12828
|
const origin = request.headers.origin;
|
|
12163
12829
|
if (!isConfiguredOrigin(origin, options)) return `ip:${request.ip}`;
|
|
12164
12830
|
const presented = request.headers[AGENT_SERVER_TOKEN_HEADER];
|
|
12165
|
-
|
|
12166
|
-
if (typeof presented === "string" && presented.length > 0) try {
|
|
12167
|
-
if (origin === "moltnet-agent-desktop://native") authenticated = hasValidNativeGrant(origin, presented);
|
|
12168
|
-
else {
|
|
12169
|
-
if (!oauth) return `unauth:${origin}:${request.ip}`;
|
|
12170
|
-
await verifyBrowser(request, presented);
|
|
12171
|
-
authenticated = true;
|
|
12172
|
-
}
|
|
12173
|
-
} catch (error) {
|
|
12174
|
-
if (error instanceof AgentServerHttpError && error.statusCode === 429) throw error;
|
|
12175
|
-
if (origin === "moltnet-agent-desktop://native") throw error;
|
|
12176
|
-
}
|
|
12177
|
-
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}`;
|
|
12178
12832
|
}
|
|
12179
12833
|
});
|
|
12180
12834
|
const requireAuthorizedOrigin = async (request) => {
|
|
12181
12835
|
if (restartRequired) throw new AgentServerHttpError(409, "restart_required", "Restart the Agent Server to apply connection settings");
|
|
12182
12836
|
const origin = requireOriginHeader(request.headers);
|
|
12183
12837
|
const token = request.headers[AGENT_SERVER_TOKEN_HEADER];
|
|
12184
|
-
if (typeof token !== "string" || token.length === 0) throw new AgentServerHttpError(401, "authorization_required", "
|
|
12185
|
-
if (origin
|
|
12186
|
-
|
|
12187
|
-
if (!oauth) throw new AgentServerHttpError(503, "oauth_unavailable", "Local OAuth is not configured");
|
|
12188
|
-
const admission = browserVerification.get(request);
|
|
12189
|
-
browserVerification.delete(request);
|
|
12190
|
-
await (admission ?? oauth.verifyBrowser(token));
|
|
12191
|
-
} catch (error) {
|
|
12192
|
-
if (error instanceof AgentServerHttpError) throw error;
|
|
12193
|
-
const code = error && typeof error === "object" && "code" in error ? error.code : void 0;
|
|
12194
|
-
const rejected = error instanceof InvalidOperatorGrantError || typeof code === "string" && [
|
|
12195
|
-
"ERR_JWT_EXPIRED",
|
|
12196
|
-
"ERR_JWT_CLAIM_VALIDATION_FAILED",
|
|
12197
|
-
"ERR_JWS_SIGNATURE_VERIFICATION_FAILED",
|
|
12198
|
-
"ERR_JWS_INVALID",
|
|
12199
|
-
"ERR_JWT_INVALID",
|
|
12200
|
-
"ERR_JOSE_ALG_NOT_ALLOWED",
|
|
12201
|
-
"ERR_JWKS_NO_MATCHING_KEY"
|
|
12202
|
-
].includes(code);
|
|
12203
|
-
request.log.warn({
|
|
12204
|
-
stage: "local-control-authorization",
|
|
12205
|
-
outcome: rejected ? "rejected" : "unavailable",
|
|
12206
|
-
code: typeof code === "string" ? code : void 0
|
|
12207
|
-
}, "Local control authorization failed");
|
|
12208
|
-
if (!rejected) throw new AgentServerHttpError(503, "authorization_unavailable", "Local authorization is unavailable. Check Server settings or retry shortly.");
|
|
12209
|
-
throw new AgentServerHttpError(401, "authorization_required", "Sign in to authorize local control");
|
|
12210
|
-
}
|
|
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);
|
|
12211
12841
|
return origin;
|
|
12212
12842
|
};
|
|
12213
12843
|
app.after(() => {
|
|
@@ -12216,13 +12846,12 @@ function buildAgentServer(input) {
|
|
|
12216
12846
|
});
|
|
12217
12847
|
app.get("/health", { schema: AgentServerRouteSchemas.health }, async () => ({ status: "ok" }));
|
|
12218
12848
|
app.get("/v1/native/connection-settings", { schema: { hide: true } }, async (request) => {
|
|
12219
|
-
|
|
12220
|
-
return options.connectionSettings.view();
|
|
12849
|
+
return (await requireNativeOrigin(requireAuthorizedOrigin, request, options.connectionSettings)).view();
|
|
12221
12850
|
});
|
|
12222
12851
|
app.post("/v1/native/connection-settings", { schema: { hide: true } }, async (request) => {
|
|
12223
|
-
|
|
12852
|
+
const store = await requireNativeOrigin(requireAuthorizedOrigin, request, options.connectionSettings);
|
|
12224
12853
|
try {
|
|
12225
|
-
const settings = options.runs.prepareServerRestart(() =>
|
|
12854
|
+
const settings = options.runs.prepareServerRestart(() => store.save(request.body));
|
|
12226
12855
|
oauth?.cancel();
|
|
12227
12856
|
restartRequired = true;
|
|
12228
12857
|
return settings;
|
|
@@ -12230,40 +12859,6 @@ function buildAgentServer(input) {
|
|
|
12230
12859
|
throw new AgentServerHttpError(400, "invalid_connection_settings", error instanceof Error ? error.message : "Invalid connection settings");
|
|
12231
12860
|
}
|
|
12232
12861
|
});
|
|
12233
|
-
app.get("/oauth/metadata", { schema: {
|
|
12234
|
-
operationId: "getAgentServerOAuthMetadata",
|
|
12235
|
-
tags: ["operator"],
|
|
12236
|
-
response: { 200: {
|
|
12237
|
-
type: "object",
|
|
12238
|
-
required: [
|
|
12239
|
-
"protocolVersion",
|
|
12240
|
-
"instance",
|
|
12241
|
-
"issuer",
|
|
12242
|
-
"authorizationUrl",
|
|
12243
|
-
"tokenUrl",
|
|
12244
|
-
"clientId",
|
|
12245
|
-
"operatorConfigured"
|
|
12246
|
-
],
|
|
12247
|
-
properties: {
|
|
12248
|
-
protocolVersion: {
|
|
12249
|
-
type: "integer",
|
|
12250
|
-
const: OPERATOR_OAUTH.protocolVersion
|
|
12251
|
-
},
|
|
12252
|
-
instance: {
|
|
12253
|
-
type: "string",
|
|
12254
|
-
format: "uuid"
|
|
12255
|
-
},
|
|
12256
|
-
issuer: { type: "string" },
|
|
12257
|
-
authorizationUrl: { type: "string" },
|
|
12258
|
-
tokenUrl: { type: "string" },
|
|
12259
|
-
clientId: { type: "string" },
|
|
12260
|
-
operatorConfigured: { type: "boolean" }
|
|
12261
|
-
}
|
|
12262
|
-
} }
|
|
12263
|
-
} }, async () => {
|
|
12264
|
-
if (!oauth) throw new AgentServerHttpError(503, "oauth_unavailable", "Local OAuth is not configured");
|
|
12265
|
-
return oauth.metadata();
|
|
12266
|
-
});
|
|
12267
12862
|
app.post("/v1/operator/sign-in", { schema: {
|
|
12268
12863
|
operationId: "signInAgentServerOperator",
|
|
12269
12864
|
response: { 200: {
|
|
@@ -12272,10 +12867,12 @@ function buildAgentServer(input) {
|
|
|
12272
12867
|
required: ["state"]
|
|
12273
12868
|
} }
|
|
12274
12869
|
} }, async (request) => {
|
|
12275
|
-
|
|
12276
|
-
await oauth.authorize(void 0, requestOperationSignal(request, options.shutdownSignal));
|
|
12870
|
+
await (await requireNativeOrigin(requireAuthorizedOrigin, request, oauth)).authorize(void 0, requestOperationSignal(request, options.shutdownSignal));
|
|
12277
12871
|
return { state: "authorized" };
|
|
12278
12872
|
});
|
|
12873
|
+
app.get("/v1/native/operator", { schema: { hide: true } }, async (request) => {
|
|
12874
|
+
return { operatorConfigured: (await requireNativeOrigin(requireAuthorizedOrigin, request, oauth)).operatorConfigured() };
|
|
12875
|
+
});
|
|
12279
12876
|
app.post("/v1/operator/cancel", { schema: {
|
|
12280
12877
|
operationId: "cancelAgentServerOperatorApproval",
|
|
12281
12878
|
tags: ["operator"],
|
|
@@ -12289,8 +12886,7 @@ function buildAgentServer(input) {
|
|
|
12289
12886
|
required: ["state"]
|
|
12290
12887
|
} }
|
|
12291
12888
|
} }, async (request) => {
|
|
12292
|
-
|
|
12293
|
-
oauth.cancel();
|
|
12889
|
+
(await requireNativeOrigin(requireAuthorizedOrigin, request, oauth)).cancel();
|
|
12294
12890
|
return { state: "cancelled" };
|
|
12295
12891
|
});
|
|
12296
12892
|
app.delete("/v1/operator", { schema: {
|
|
@@ -12306,8 +12902,7 @@ function buildAgentServer(input) {
|
|
|
12306
12902
|
required: ["state"]
|
|
12307
12903
|
} }
|
|
12308
12904
|
} }, async (request) => {
|
|
12309
|
-
|
|
12310
|
-
oauth.removeOperator();
|
|
12905
|
+
(await requireNativeOrigin(requireAuthorizedOrigin, request, oauth)).removeOperator();
|
|
12311
12906
|
return { state: "removed" };
|
|
12312
12907
|
});
|
|
12313
12908
|
registerStatusRoute(app, options, requireAuthorizedOrigin);
|
|
@@ -12316,6 +12911,7 @@ function buildAgentServer(input) {
|
|
|
12316
12911
|
registerSubscriptionRoutes(app, options, requireAuthorizedOrigin);
|
|
12317
12912
|
registerRunRoutes(app, options, requireAuthorizedOrigin);
|
|
12318
12913
|
registerCatalogueRoute(app, options, requireAuthorizedOrigin);
|
|
12914
|
+
registerProjectLocationRoutes(app, options, requireAuthorizedOrigin);
|
|
12319
12915
|
});
|
|
12320
12916
|
app.addHook("preClose", async () => {
|
|
12321
12917
|
options.operatorOAuth?.cancel();
|
|
@@ -12342,6 +12938,103 @@ function buildAgentServer(input) {
|
|
|
12342
12938
|
});
|
|
12343
12939
|
return app;
|
|
12344
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
|
+
}
|
|
12345
13038
|
function registerCatalogueRoute(app, options, requireAuthorizedOrigin) {
|
|
12346
13039
|
app.get("/v1/catalogue", {
|
|
12347
13040
|
schema: AgentServerRouteSchemas.catalogue,
|
|
@@ -12350,34 +13043,42 @@ function registerCatalogueRoute(app, options, requireAuthorizedOrigin) {
|
|
|
12350
13043
|
await requireAuthorizedOrigin(request);
|
|
12351
13044
|
const { identity } = request.query ?? {};
|
|
12352
13045
|
if (!identity || identity.trim().length === 0) throw new AgentServerHttpError(400, "invalid_query", "\"identity\" is required");
|
|
12353
|
-
|
|
12354
|
-
requireActivation(options.store, alias);
|
|
12355
|
-
return buildCatalogue({
|
|
12356
|
-
agent: await (options.catalogueAgentFor ? options.catalogueAgentFor(alias) : defaultCatalogueAgent(options, alias)),
|
|
12357
|
-
machine: machineCapabilities(options),
|
|
12358
|
-
identityDefault: readIdentityDefaultBinding(options.store.identityDir(alias))
|
|
12359
|
-
});
|
|
13046
|
+
return readIdentityCatalogue(options, identity.trim(), request.log);
|
|
12360
13047
|
});
|
|
12361
13048
|
}
|
|
12362
13049
|
/** Resolve and verify each indexed team independently with its exact key. */
|
|
12363
13050
|
async function defaultCatalogueAgent(options, alias) {
|
|
12364
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
|
+
};
|
|
12365
13058
|
return {
|
|
12366
13059
|
teamIds: Object.keys(config.agent_key_refs ?? {}),
|
|
12367
13060
|
lastVerified: (teamId) => requireActivation(options.store, alias).credentialHealth?.[teamId],
|
|
12368
|
-
readTeam: async (teamId) => {
|
|
12369
|
-
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);
|
|
12370
13065
|
const [team, diaries, profiles] = await Promise.all([
|
|
12371
13066
|
client.teams.get(teamId),
|
|
12372
13067
|
client.diaries.list(),
|
|
12373
13068
|
client.runtimeProfiles.list({ teamId })
|
|
12374
13069
|
]);
|
|
13070
|
+
clients.set(teamId, client);
|
|
12375
13071
|
return {
|
|
12376
13072
|
team,
|
|
12377
13073
|
diaries: diaries.items,
|
|
12378
13074
|
profiles: profiles.items,
|
|
12379
13075
|
credential: metadata
|
|
12380
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);
|
|
12381
13082
|
}
|
|
12382
13083
|
};
|
|
12383
13084
|
}
|
|
@@ -12400,7 +13101,7 @@ function machineCapabilities(options) {
|
|
|
12400
13101
|
function registerStatusRoute(app, options, requireAuthorizedOrigin) {
|
|
12401
13102
|
const { store, runs } = options;
|
|
12402
13103
|
app.get("/v1/status", { schema: AgentServerRouteSchemas.status }, async (request) => {
|
|
12403
|
-
await requireAuthorizedOrigin(request);
|
|
13104
|
+
const origin = await requireAuthorizedOrigin(request);
|
|
12404
13105
|
const selected = selectedIdentity(store, options.activeIdentity);
|
|
12405
13106
|
return {
|
|
12406
13107
|
version: options.version,
|
|
@@ -12410,7 +13111,7 @@ function registerStatusRoute(app, options, requireAuthorizedOrigin) {
|
|
|
12410
13111
|
identities: identityViews(store),
|
|
12411
13112
|
...selected ? { selectedIdentity: selected } : {},
|
|
12412
13113
|
providers: options.providers.list(),
|
|
12413
|
-
runs: await runViews(runs),
|
|
13114
|
+
runs: await runViews(runs, origin),
|
|
12414
13115
|
runtimeSettings: options.runtimeSettings ?? DEFAULT_LOCAL_OPERATIONAL_SETTINGS
|
|
12415
13116
|
};
|
|
12416
13117
|
});
|
|
@@ -12535,11 +13236,30 @@ function registerProviderRoutes(app, options, requireAuthorizedOrigin) {
|
|
|
12535
13236
|
return reply.code(204).send(null);
|
|
12536
13237
|
});
|
|
12537
13238
|
}
|
|
12538
|
-
async function runViews(runs) {
|
|
12539
|
-
return (await runs.listAsync(RUN_HISTORY_LIMIT)).map((record) => (
|
|
12540
|
-
|
|
12541
|
-
|
|
12542
|
-
|
|
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;
|
|
12543
13263
|
}
|
|
12544
13264
|
function registerSubscriptionRoutes(app, options, requireAuthorizedOrigin) {
|
|
12545
13265
|
app.get("/v1/subscriptions", { schema: AgentServerRouteSchemas.listSubscriptions }, async (request) => {
|
|
@@ -12566,17 +13286,22 @@ function registerSubscriptionRoutes(app, options, requireAuthorizedOrigin) {
|
|
|
12566
13286
|
function registerRunRoutes(app, options, requireAuthorizedOrigin) {
|
|
12567
13287
|
const { runs } = options;
|
|
12568
13288
|
app.get("/v1/runs", { schema: AgentServerRouteSchemas.listRuns }, async (request) => {
|
|
12569
|
-
await requireAuthorizedOrigin(request);
|
|
12570
|
-
return runViews(runs);
|
|
13289
|
+
return runViews(runs, await requireAuthorizedOrigin(request));
|
|
12571
13290
|
});
|
|
12572
13291
|
app.post("/v1/runs", {
|
|
12573
13292
|
schema: AgentServerRouteSchemas.startRun,
|
|
12574
13293
|
attachValidation: true
|
|
12575
13294
|
}, async (request, reply) => {
|
|
12576
|
-
await requireAuthorizedOrigin(request);
|
|
13295
|
+
const origin = await requireAuthorizedOrigin(request);
|
|
12577
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}`);
|
|
12578
13299
|
const diaryId = optionalString(body, "diaryId");
|
|
12579
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") } : {},
|
|
12580
13305
|
agent: requireString(body, "agent"),
|
|
12581
13306
|
teamId: requireString(body, "teamId"),
|
|
12582
13307
|
...diaryId ? { diaryId } : {},
|
|
@@ -12584,15 +13309,13 @@ function registerRunRoutes(app, options, requireAuthorizedOrigin) {
|
|
|
12584
13309
|
taskTypes: stringArray(body, "taskTypes"),
|
|
12585
13310
|
mode: requireString(body, "mode")
|
|
12586
13311
|
}, requestOperationSignal(request, options.shutdownSignal));
|
|
12587
|
-
return reply.code(201).send(
|
|
12588
|
-
...record,
|
|
12589
|
-
active: runs.isActive(record.id)
|
|
12590
|
-
});
|
|
13312
|
+
return reply.code(201).send(runView(record, runs.isActive(record.id), origin));
|
|
12591
13313
|
});
|
|
12592
13314
|
app.delete("/v1/runs/:runId", { schema: AgentServerRouteSchemas.stopRun }, async (request) => {
|
|
12593
|
-
await requireAuthorizedOrigin(request);
|
|
13315
|
+
const origin = await requireAuthorizedOrigin(request);
|
|
12594
13316
|
const { runId } = request.params;
|
|
12595
|
-
|
|
13317
|
+
const record = runs.stop(runId);
|
|
13318
|
+
return runView(record, runs.isActive(record.id), origin);
|
|
12596
13319
|
});
|
|
12597
13320
|
registerRunLogRoute(app, options, requireAuthorizedOrigin);
|
|
12598
13321
|
}
|
|
@@ -12619,9 +13342,10 @@ function registerRunLogRoute(app, options, requireAuthorizedOrigin) {
|
|
|
12619
13342
|
} }
|
|
12620
13343
|
} }
|
|
12621
13344
|
} }, async (request) => {
|
|
12622
|
-
await requireAuthorizedOrigin(request);
|
|
13345
|
+
const origin = await requireAuthorizedOrigin(request);
|
|
12623
13346
|
const { runId } = request.params;
|
|
12624
13347
|
const record = runs.status(runId);
|
|
13348
|
+
const redact = localPathRedactor(record, origin, options);
|
|
12625
13349
|
const handle = await open(store.resolveRunLogPath(record.id), constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
12626
13350
|
try {
|
|
12627
13351
|
const state = {
|
|
@@ -12629,20 +13353,22 @@ function registerRunLogRoute(app, options, requireAuthorizedOrigin) {
|
|
|
12629
13353
|
fragment: ""
|
|
12630
13354
|
};
|
|
12631
13355
|
const { lines, omitted } = await readAgentServerLogDelta(handle, state);
|
|
13356
|
+
const native = origin === NATIVE_CLIENT_ORIGIN;
|
|
12632
13357
|
return { lines: [
|
|
12633
13358
|
...omitted ? ["[older log output omitted]"] : [],
|
|
12634
|
-
...lines,
|
|
12635
|
-
...state.fragment ? [state.fragment] : []
|
|
12636
|
-
] };
|
|
13359
|
+
...omitted && !native ? lines.slice(1) : lines,
|
|
13360
|
+
...state.fragment && (native || !runs.isActive(record.id)) ? [state.fragment] : []
|
|
13361
|
+
].map(redact) };
|
|
12637
13362
|
} finally {
|
|
12638
13363
|
await handle.close();
|
|
12639
13364
|
}
|
|
12640
13365
|
});
|
|
12641
13366
|
let openStreams = 0;
|
|
12642
13367
|
app.get("/v1/runs/:runId/logs", { schema: AgentServerRouteSchemas.streamRunLogs }, async (request, reply) => {
|
|
12643
|
-
await requireAuthorizedOrigin(request);
|
|
13368
|
+
const origin = await requireAuthorizedOrigin(request);
|
|
12644
13369
|
const { runId } = request.params;
|
|
12645
13370
|
const record = runs.status(runId);
|
|
13371
|
+
const redact = localPathRedactor(record, origin, options);
|
|
12646
13372
|
store.resolveRunLogPath(record.id);
|
|
12647
13373
|
if (openStreams >= MAX_LOG_STREAMS) throw new AgentServerHttpError(429, "rate_limited", "Too many concurrent log streams");
|
|
12648
13374
|
openStreams += 1;
|
|
@@ -12683,12 +13409,12 @@ function registerRunLogRoute(app, options, requireAuthorizedOrigin) {
|
|
|
12683
13409
|
});
|
|
12684
13410
|
};
|
|
12685
13411
|
const push = async () => {
|
|
12686
|
-
if (request.headers.origin !== "moltnet-agent-desktop://native") await requireAuthorizedOrigin(request);
|
|
12687
13412
|
const handle = await open(store.resolveRunLogPath(record.id), constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
12688
13413
|
try {
|
|
12689
13414
|
const { lines, omitted } = await readAgentServerLogDelta(handle, readState);
|
|
12690
13415
|
if (omitted) await writeData("[older log output omitted]");
|
|
12691
|
-
|
|
13416
|
+
const complete = omitted && origin !== "moltnet-agent-desktop://native" ? lines.slice(1) : lines;
|
|
13417
|
+
for (const line of complete) await writeData(redact(line));
|
|
12692
13418
|
} finally {
|
|
12693
13419
|
await handle.close();
|
|
12694
13420
|
}
|
|
@@ -12723,6 +13449,43 @@ function registerRunLogRoute(app, options, requireAuthorizedOrigin) {
|
|
|
12723
13449
|
return reply;
|
|
12724
13450
|
});
|
|
12725
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
|
+
}
|
|
12726
13489
|
function corsHeadersFor(request, options) {
|
|
12727
13490
|
const origin = request.headers.origin;
|
|
12728
13491
|
if (isConfiguredOrigin(origin, options)) return {
|
|
@@ -12735,6 +13498,23 @@ function isConfiguredOrigin(origin, options) {
|
|
|
12735
13498
|
return typeof origin === "string" && (origin === "moltnet-agent-desktop://native" || !options.nativeOnly && (options.allowedOrigins.includes(origin) || origin === options.selfOrigin));
|
|
12736
13499
|
}
|
|
12737
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
|
+
}
|
|
12738
13518
|
if (error instanceof TeamCredentialError) return {
|
|
12739
13519
|
statusCode: 400,
|
|
12740
13520
|
code: error.blocker.code,
|
|
@@ -12819,7 +13599,7 @@ function nativeSocketValidationOptions(input) {
|
|
|
12819
13599
|
...input.cliAllowedOrigins ? { allowedOrigins: input.cliAllowedOrigins } : {}
|
|
12820
13600
|
};
|
|
12821
13601
|
}
|
|
12822
|
-
async function runAgentServer(argv) {
|
|
13602
|
+
async function runAgentServer(argv, ports = {}) {
|
|
12823
13603
|
if (isHelpFlag(argv)) {
|
|
12824
13604
|
console.log(AGENT_SERVER_HELP);
|
|
12825
13605
|
return 0;
|
|
@@ -12897,9 +13677,12 @@ async function runAgentServer(argv) {
|
|
|
12897
13677
|
logger
|
|
12898
13678
|
});
|
|
12899
13679
|
const runtimeRegistry = new RuntimeRegistry(store.root);
|
|
13680
|
+
const supplied = ports.configure?.(store);
|
|
12900
13681
|
const runs = new RunManager({
|
|
13682
|
+
...supplied?.runOptions,
|
|
12901
13683
|
store,
|
|
12902
13684
|
storeRoot: settingsRoot,
|
|
13685
|
+
projectRoot: settingsRoot,
|
|
12903
13686
|
secretProviders,
|
|
12904
13687
|
externalSecretProviders,
|
|
12905
13688
|
baseEnv: processEnvSnapshot(),
|
|
@@ -12909,16 +13692,17 @@ async function runAgentServer(argv) {
|
|
|
12909
13692
|
});
|
|
12910
13693
|
if (nativeSocket) await validateNativeSocket(nativeSocket);
|
|
12911
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);
|
|
12912
13703
|
const app = buildAgentServer({
|
|
12913
|
-
|
|
12914
|
-
|
|
12915
|
-
authorizationUrl: new URL("/oauth2/auth", connection.publicUrl).href,
|
|
12916
|
-
tokenUrl: new URL("/oauth2/token", connection.publicUrl).href,
|
|
12917
|
-
jwksUrl: new URL("/.well-known/jwks.json", connection.publicUrl).href,
|
|
12918
|
-
nativeClientId: connection.nativeClientId,
|
|
12919
|
-
consoleClientId: connection.consoleClientId,
|
|
12920
|
-
callbackPort: OPERATOR_OAUTH.callbackPort
|
|
12921
|
-
}, root),
|
|
13704
|
+
...supplied?.catalogueAgentFor ? { catalogueAgentFor: supplied.catalogueAgentFor } : {},
|
|
13705
|
+
operatorOAuth,
|
|
12922
13706
|
nativeOnly: Boolean(nativeSocket),
|
|
12923
13707
|
connectionSettings,
|
|
12924
13708
|
operatorApiUrl: connection.apiUrl,
|
|
@@ -13403,7 +14187,7 @@ async function writeCache(cache) {
|
|
|
13403
14187
|
}
|
|
13404
14188
|
//#endregion
|
|
13405
14189
|
//#region src/version.ts
|
|
13406
|
-
var DAEMON_VERSION = "0.
|
|
14190
|
+
var DAEMON_VERSION = "0.64.0";
|
|
13407
14191
|
//#endregion
|
|
13408
14192
|
//#region src/cli.ts
|
|
13409
14193
|
async function runAgentDaemonCli(options) {
|