@themoltnet/agent-daemon 0.63.1 → 0.65.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 +1177 -344
- 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) {
|
|
@@ -9553,6 +9580,20 @@ async function validateNativeSocket(path) {
|
|
|
9553
9580
|
//#region src/lib/agent-server/operator-oauth.ts
|
|
9554
9581
|
var InvalidOperatorGrantError = class extends Error {};
|
|
9555
9582
|
var LOCAL_SCOPE = OPERATOR_OAUTH.localControlScope;
|
|
9583
|
+
function readOperatorTeams(value) {
|
|
9584
|
+
if (!Array.isArray(value) || value.length > 256) return [];
|
|
9585
|
+
const teams = value;
|
|
9586
|
+
if (!teams.every(isOperatorTeam)) return [];
|
|
9587
|
+
return teams.map(({ id, name }) => ({
|
|
9588
|
+
id,
|
|
9589
|
+
name
|
|
9590
|
+
}));
|
|
9591
|
+
}
|
|
9592
|
+
function isOperatorTeam(value) {
|
|
9593
|
+
if (!value || typeof value !== "object") return false;
|
|
9594
|
+
const team = value;
|
|
9595
|
+
return typeof team["id"] === "string" && /^[0-9a-f-]{36}$/i.test(team["id"]) && typeof team["name"] === "string" && team["name"].length > 0 && team["name"].length <= 200;
|
|
9596
|
+
}
|
|
9556
9597
|
/** Trusted native controller owns the verifier, callback and token exchange. */
|
|
9557
9598
|
var OperatorOAuth = class {
|
|
9558
9599
|
instance = randomUUID();
|
|
@@ -9564,7 +9605,7 @@ var OperatorOAuth = class {
|
|
|
9564
9605
|
const command = process.platform === "darwin" ? "open" : process.platform === "win32" ? "explorer.exe" : "xdg-open";
|
|
9565
9606
|
return new Promise((resolve, reject) => {
|
|
9566
9607
|
execFile(command, [url], (error) => {
|
|
9567
|
-
if (error) reject(/* @__PURE__ */ new Error("Could not open
|
|
9608
|
+
if (error) reject(/* @__PURE__ */ new Error("Could not open browser approval"));
|
|
9568
9609
|
else resolve();
|
|
9569
9610
|
});
|
|
9570
9611
|
});
|
|
@@ -9584,7 +9625,8 @@ var OperatorOAuth = class {
|
|
|
9584
9625
|
if (!value || typeof value !== "object" || !("issuer" in value) || !("subject" in value) || typeof value.issuer !== "string" || typeof value.subject !== "string") throw new Error("Invalid operator");
|
|
9585
9626
|
this.operator = {
|
|
9586
9627
|
issuer: value.issuer,
|
|
9587
|
-
subject: value.subject
|
|
9628
|
+
subject: value.subject,
|
|
9629
|
+
teams: "teams" in value ? readOperatorTeams(value.teams) : []
|
|
9588
9630
|
};
|
|
9589
9631
|
} catch (error) {
|
|
9590
9632
|
if (error.code !== "ENOENT") throw error;
|
|
@@ -9594,22 +9636,17 @@ var OperatorOAuth = class {
|
|
|
9594
9636
|
cancel() {
|
|
9595
9637
|
this.pending?.abort();
|
|
9596
9638
|
}
|
|
9639
|
+
operatorConfigured() {
|
|
9640
|
+
return this.operator !== null;
|
|
9641
|
+
}
|
|
9642
|
+
listTeams() {
|
|
9643
|
+
return this.operator?.teams ?? [];
|
|
9644
|
+
}
|
|
9597
9645
|
removeOperator() {
|
|
9598
9646
|
this.cancel();
|
|
9599
9647
|
rmSync(join(this.root, "operator.json"), { force: true });
|
|
9600
9648
|
this.operator = null;
|
|
9601
9649
|
}
|
|
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
9650
|
async verify(token, scope, clientId) {
|
|
9614
9651
|
const { payload } = await jwtVerify(token, this.keys, {
|
|
9615
9652
|
algorithms: ["RS256"],
|
|
@@ -9620,7 +9657,7 @@ var OperatorOAuth = class {
|
|
|
9620
9657
|
"iat",
|
|
9621
9658
|
"sub"
|
|
9622
9659
|
],
|
|
9623
|
-
maxTokenAge:
|
|
9660
|
+
maxTokenAge: OPERATOR_OAUTH.nativeLifetimeSeconds
|
|
9624
9661
|
});
|
|
9625
9662
|
const claims = payload.ext;
|
|
9626
9663
|
const scopes = typeof payload.scope === "string" ? payload.scope.split(" ") : payload.scp;
|
|
@@ -9628,13 +9665,10 @@ var OperatorOAuth = class {
|
|
|
9628
9665
|
return {
|
|
9629
9666
|
issuer: payload.iss,
|
|
9630
9667
|
subject: payload.sub,
|
|
9631
|
-
provisioning: claims["moltnet:provisioning"]
|
|
9668
|
+
provisioning: claims["moltnet:provisioning"],
|
|
9669
|
+
teams: readOperatorTeams(claims["moltnet:operator_teams"])
|
|
9632
9670
|
};
|
|
9633
9671
|
}
|
|
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
9672
|
async authorize(grant, signal) {
|
|
9639
9673
|
if (this.active) throw new Error("An approval is already pending");
|
|
9640
9674
|
this.active = true;
|
|
@@ -9688,9 +9722,9 @@ var OperatorOAuth = class {
|
|
|
9688
9722
|
...grant ? { provisioning: JSON.stringify(grant) } : {}
|
|
9689
9723
|
})) url.searchParams.set(key, value);
|
|
9690
9724
|
try {
|
|
9691
|
-
Promise.resolve(this.openBrowser(url.href)).catch(() => reject(/* @__PURE__ */ new Error("Could not open
|
|
9725
|
+
Promise.resolve(this.openBrowser(url.href)).catch(() => reject(/* @__PURE__ */ new Error("Could not open browser approval")));
|
|
9692
9726
|
} catch {
|
|
9693
|
-
reject(/* @__PURE__ */ new Error("Could not open
|
|
9727
|
+
reject(/* @__PURE__ */ new Error("Could not open browser approval"));
|
|
9694
9728
|
}
|
|
9695
9729
|
});
|
|
9696
9730
|
});
|
|
@@ -9721,7 +9755,8 @@ var OperatorOAuth = class {
|
|
|
9721
9755
|
try {
|
|
9722
9756
|
writeFileSync(join(this.root, "operator.json"), JSON.stringify({
|
|
9723
9757
|
issuer: operator.issuer,
|
|
9724
|
-
subject: operator.subject
|
|
9758
|
+
subject: operator.subject,
|
|
9759
|
+
teams: grant ? [] : operator.teams
|
|
9725
9760
|
}), {
|
|
9726
9761
|
mode: 384,
|
|
9727
9762
|
flag: "wx"
|
|
@@ -9733,8 +9768,12 @@ var OperatorOAuth = class {
|
|
|
9733
9768
|
}
|
|
9734
9769
|
this.operator = {
|
|
9735
9770
|
issuer: operator.issuer,
|
|
9736
|
-
subject: operator.subject
|
|
9771
|
+
subject: operator.subject,
|
|
9772
|
+
teams: grant ? [] : operator.teams
|
|
9737
9773
|
};
|
|
9774
|
+
} else if (!grant) {
|
|
9775
|
+
this.operator.teams = operator.teams;
|
|
9776
|
+
writeFileSync(join(this.root, "operator.json"), JSON.stringify(this.operator), { mode: 384 });
|
|
9738
9777
|
}
|
|
9739
9778
|
return tokens.access_token;
|
|
9740
9779
|
} finally {
|
|
@@ -10101,6 +10140,94 @@ function snapshot(login) {
|
|
|
10101
10140
|
};
|
|
10102
10141
|
}
|
|
10103
10142
|
//#endregion
|
|
10143
|
+
//#region src/lib/agent-server/catalogue-project-reader.ts
|
|
10144
|
+
var PAGE_SIZE = 100;
|
|
10145
|
+
var ProjectPaginationError = class extends Error {
|
|
10146
|
+
name = "ProjectPaginationError";
|
|
10147
|
+
};
|
|
10148
|
+
async function readCatalogueProjects(projects, teamId) {
|
|
10149
|
+
const items = [];
|
|
10150
|
+
let offset = 0;
|
|
10151
|
+
for (let page = 0; page < 10; page++) {
|
|
10152
|
+
const result = await projects.list({
|
|
10153
|
+
includeArchived: false,
|
|
10154
|
+
limit: PAGE_SIZE,
|
|
10155
|
+
offset
|
|
10156
|
+
}, { teamId });
|
|
10157
|
+
items.push(...result.items);
|
|
10158
|
+
if (result.nextOffset === null) return {
|
|
10159
|
+
items,
|
|
10160
|
+
truncated: false
|
|
10161
|
+
};
|
|
10162
|
+
if (!Number.isSafeInteger(result.nextOffset) || result.nextOffset <= offset) throw new ProjectPaginationError("Invalid project pagination offset");
|
|
10163
|
+
offset = result.nextOffset;
|
|
10164
|
+
}
|
|
10165
|
+
return {
|
|
10166
|
+
items,
|
|
10167
|
+
truncated: true
|
|
10168
|
+
};
|
|
10169
|
+
}
|
|
10170
|
+
/** One project by id; null when the credential cannot see it. */
|
|
10171
|
+
async function readCatalogueProject(projects, teamId, projectId) {
|
|
10172
|
+
try {
|
|
10173
|
+
return await projects.get(projectId, { teamId });
|
|
10174
|
+
} catch (error) {
|
|
10175
|
+
if (error instanceof MoltNetError && (error.statusCode === 403 || error.statusCode === 404)) return null;
|
|
10176
|
+
throw error;
|
|
10177
|
+
}
|
|
10178
|
+
}
|
|
10179
|
+
//#endregion
|
|
10180
|
+
//#region src/lib/agent-server/readiness.ts
|
|
10181
|
+
/**
|
|
10182
|
+
* Whether a runtime profile can execute on *this* machine.
|
|
10183
|
+
*
|
|
10184
|
+
* A profile is authored in Console against a team; whether it can run depends
|
|
10185
|
+
* on local facts only the server knows — which provider keys are configured
|
|
10186
|
+
* here and which runtime kinds this machine can produce.
|
|
10187
|
+
*
|
|
10188
|
+
* The prerequisite comparison itself is **not** reimplemented here. Run start
|
|
10189
|
+
* calls `validateRuntimeProfilePrerequisites`, and the catalogue calls the same
|
|
10190
|
+
* function, so the composer cannot promise a run that startup would reject for
|
|
10191
|
+
* a reason the two evaluated differently.
|
|
10192
|
+
*/
|
|
10193
|
+
function deriveProfileReadiness(profile, machine) {
|
|
10194
|
+
const blockers = [];
|
|
10195
|
+
const env = {};
|
|
10196
|
+
for (const [name, configured] of machine.providerEnv) if (configured) env[name] = "configured";
|
|
10197
|
+
try {
|
|
10198
|
+
validateRuntimeProfilePrerequisites(profile, env, {
|
|
10199
|
+
tools: machine.inventory?.tools ?? profile.requiredTools,
|
|
10200
|
+
executables: machine.inventory?.executables ?? profile.requiredExecutables
|
|
10201
|
+
});
|
|
10202
|
+
} catch (error) {
|
|
10203
|
+
if (!(error instanceof RuntimeProfilePrerequisiteError)) throw error;
|
|
10204
|
+
for (const name of error.missingEnv) blockers.push({
|
|
10205
|
+
code: "env_missing",
|
|
10206
|
+
message: `${name} is not configured on this machine.`,
|
|
10207
|
+
remedy: "Add the key under Providers, then reopen this run."
|
|
10208
|
+
});
|
|
10209
|
+
for (const name of error.missingTools) blockers.push({
|
|
10210
|
+
code: "tool_missing",
|
|
10211
|
+
message: `The runtime does not provide the tool ${name}.`,
|
|
10212
|
+
remedy: `Use a profile whose runtime provides ${name}, or change the profile in Console.`
|
|
10213
|
+
});
|
|
10214
|
+
for (const name of error.missingExecutables) blockers.push({
|
|
10215
|
+
code: "executable_missing",
|
|
10216
|
+
message: `The runtime does not provide the executable ${name}.`,
|
|
10217
|
+
remedy: `Use a runtime that ships ${name}, or drop the requirement in Console.`
|
|
10218
|
+
});
|
|
10219
|
+
}
|
|
10220
|
+
if (!machine.runtimeKinds.has(profile.runtimeKind)) blockers.push({
|
|
10221
|
+
code: "runtime_unregistered",
|
|
10222
|
+
message: `Runtime kind ${profile.runtimeKind} is not available on this machine.`,
|
|
10223
|
+
remedy: "Register the runtime under Runtimes, then reopen this run."
|
|
10224
|
+
});
|
|
10225
|
+
return {
|
|
10226
|
+
ready: blockers.length === 0,
|
|
10227
|
+
blockers
|
|
10228
|
+
};
|
|
10229
|
+
}
|
|
10230
|
+
//#endregion
|
|
10104
10231
|
//#region src/lib/agent-server/identity.ts
|
|
10105
10232
|
/**
|
|
10106
10233
|
* AgentServer identity activation (#2061/#1834 boundary).
|
|
@@ -10544,7 +10671,7 @@ async function verifyTeamActivation(store, alias, managed, external, connectImpl
|
|
|
10544
10671
|
if (missing.length) throw new TeamCredentialError({
|
|
10545
10672
|
code: "agent_key_scopes_insufficient",
|
|
10546
10673
|
message: `This credential lacks ${missing.join(", ")}.`,
|
|
10547
|
-
remedy: "Renew through
|
|
10674
|
+
remedy: "Renew through browser approval with the required desktop scopes."
|
|
10548
10675
|
});
|
|
10549
10676
|
activated.boundTeamId = teamId;
|
|
10550
10677
|
return captureTeamCredential(activated, {
|
|
@@ -10554,6 +10681,367 @@ async function verifyTeamActivation(store, alias, managed, external, connectImpl
|
|
|
10554
10681
|
});
|
|
10555
10682
|
}
|
|
10556
10683
|
//#endregion
|
|
10684
|
+
//#region src/lib/agent-server/catalogue.ts
|
|
10685
|
+
/**
|
|
10686
|
+
* The team/profile catalogue the desktop Run Center composes runs from.
|
|
10687
|
+
*
|
|
10688
|
+
* Scoped to the *selected agent identity*, never a human session: the daemon
|
|
10689
|
+
* already reads runtime profiles with agent credentials on every run, so this
|
|
10690
|
+
* reads the same data with the same authority, earlier. An agent-scoped list is
|
|
10691
|
+
* also authoritative about what can actually run, which a human-scoped list is
|
|
10692
|
+
* not — Console needs a "this agent cannot poll that team" error precisely
|
|
10693
|
+
* because it offers teams the agent cannot serve.
|
|
10694
|
+
*
|
|
10695
|
+
* Team and diary travel together here for the same reason `RunSpec` pairs
|
|
10696
|
+
* them: the CLI's context store refuses one without the other, and a diary
|
|
10697
|
+
* that drifts from its team means entries land in the wrong place.
|
|
10698
|
+
*/
|
|
10699
|
+
async function buildCatalogue(options) {
|
|
10700
|
+
const { agent, machine, identityDefault, logger } = options;
|
|
10701
|
+
const entries = await Promise.all(agent.teamIds.map(async (teamId) => {
|
|
10702
|
+
try {
|
|
10703
|
+
const result = await agent.readTeam(teamId);
|
|
10704
|
+
if (result.team.id !== teamId) throw new Error("Team response mismatch");
|
|
10705
|
+
const diaries = result.diaries.filter((diary) => diary.teamId === teamId).map(({ id, name }) => ({
|
|
10706
|
+
id,
|
|
10707
|
+
name
|
|
10708
|
+
}));
|
|
10709
|
+
const team = {
|
|
10710
|
+
teamId,
|
|
10711
|
+
teamName: result.team.name,
|
|
10712
|
+
available: true,
|
|
10713
|
+
blockers: [],
|
|
10714
|
+
credential: result.credential,
|
|
10715
|
+
diaries,
|
|
10716
|
+
defaultDiaryId: resolveDefaultDiary(teamId, diaries, identityDefault)
|
|
10717
|
+
};
|
|
10718
|
+
const profiles = result.profiles.filter((profile) => profile.teamId === teamId).map((profile) => ({
|
|
10719
|
+
...profile,
|
|
10720
|
+
...deriveProfileReadiness(profile, machine)
|
|
10721
|
+
}));
|
|
10722
|
+
try {
|
|
10723
|
+
const page = await agent.readProjects(teamId);
|
|
10724
|
+
return {
|
|
10725
|
+
team,
|
|
10726
|
+
profiles,
|
|
10727
|
+
projects: page.items.filter((project) => project.teamId === teamId && !project.archived),
|
|
10728
|
+
projectErrors: page.truncated ? [{
|
|
10729
|
+
teamId,
|
|
10730
|
+
code: "truncated",
|
|
10731
|
+
message: "Only the first projects are listed. Archive unused projects to see the rest."
|
|
10732
|
+
}] : []
|
|
10733
|
+
};
|
|
10734
|
+
} catch (error) {
|
|
10735
|
+
logger?.warn({
|
|
10736
|
+
...safeErrorContext(error),
|
|
10737
|
+
teamId,
|
|
10738
|
+
code: "agent_server_project_discovery_failed"
|
|
10739
|
+
}, "AgentServer project discovery failed");
|
|
10740
|
+
return {
|
|
10741
|
+
team,
|
|
10742
|
+
profiles,
|
|
10743
|
+
projects: [],
|
|
10744
|
+
projectErrors: [projectError(teamId, error)]
|
|
10745
|
+
};
|
|
10746
|
+
}
|
|
10747
|
+
} catch (error) {
|
|
10748
|
+
const blocker = credentialBlocker(error);
|
|
10749
|
+
logger?.warn({
|
|
10750
|
+
...safeErrorContext(error),
|
|
10751
|
+
teamId,
|
|
10752
|
+
blocker: blocker.code,
|
|
10753
|
+
code: "agent_server_team_unavailable"
|
|
10754
|
+
}, "AgentServer team credential unavailable");
|
|
10755
|
+
return {
|
|
10756
|
+
team: {
|
|
10757
|
+
teamId,
|
|
10758
|
+
teamName: teamId,
|
|
10759
|
+
available: false,
|
|
10760
|
+
blockers: [blocker],
|
|
10761
|
+
credential: agent.lastVerified(teamId),
|
|
10762
|
+
diaries: [],
|
|
10763
|
+
defaultDiaryId: null
|
|
10764
|
+
},
|
|
10765
|
+
profiles: [],
|
|
10766
|
+
projects: [],
|
|
10767
|
+
projectErrors: []
|
|
10768
|
+
};
|
|
10769
|
+
}
|
|
10770
|
+
}));
|
|
10771
|
+
const teams = entries.map(({ team }) => team);
|
|
10772
|
+
const available = teams.filter((team) => team.available);
|
|
10773
|
+
return {
|
|
10774
|
+
teams,
|
|
10775
|
+
defaultTeamId: available.find((team) => team.teamId === identityDefault.teamId)?.teamId ?? available[0]?.teamId ?? null,
|
|
10776
|
+
profiles: entries.flatMap((entry) => entry.profiles),
|
|
10777
|
+
projects: entries.flatMap((entry) => entry.projects),
|
|
10778
|
+
projectErrors: entries.flatMap((entry) => entry.projectErrors)
|
|
10779
|
+
};
|
|
10780
|
+
}
|
|
10781
|
+
function projectError(teamId, error) {
|
|
10782
|
+
if (error instanceof MoltNetError && (error.statusCode === 401 || error.statusCode === 403)) return {
|
|
10783
|
+
teamId,
|
|
10784
|
+
code: "forbidden",
|
|
10785
|
+
message: "This team credential cannot list projects. Renew it with project access."
|
|
10786
|
+
};
|
|
10787
|
+
if (error instanceof ProjectPaginationError) return {
|
|
10788
|
+
teamId,
|
|
10789
|
+
code: "invalid_response",
|
|
10790
|
+
message: "The server returned an unreadable project list."
|
|
10791
|
+
};
|
|
10792
|
+
return {
|
|
10793
|
+
teamId,
|
|
10794
|
+
code: "unreachable",
|
|
10795
|
+
message: "Projects could not be loaded. Retry project discovery."
|
|
10796
|
+
};
|
|
10797
|
+
}
|
|
10798
|
+
/** The one default-diary rule, shared with General run start. */
|
|
10799
|
+
function resolveDefaultDiary(teamId, diaries, identityDefault) {
|
|
10800
|
+
const bound = diaries.find((diary) => diary.id === identityDefault.diaryId);
|
|
10801
|
+
if (bound && identityDefault.teamId === teamId) return bound.id;
|
|
10802
|
+
return diaries.length === 1 ? diaries[0]?.id ?? null : null;
|
|
10803
|
+
}
|
|
10804
|
+
//#endregion
|
|
10805
|
+
//#region src/lib/agent-server/http-error.ts
|
|
10806
|
+
/** A route failure with a stable wire code; the server error handler maps it verbatim. */
|
|
10807
|
+
var AgentServerHttpError = class extends Error {
|
|
10808
|
+
name = "AgentServerHttpError";
|
|
10809
|
+
constructor(statusCode, code, message, options) {
|
|
10810
|
+
super(message, options);
|
|
10811
|
+
this.statusCode = statusCode;
|
|
10812
|
+
this.code = code;
|
|
10813
|
+
}
|
|
10814
|
+
};
|
|
10815
|
+
//#endregion
|
|
10816
|
+
//#region src/lib/agent-server/identity-binding.ts
|
|
10817
|
+
/**
|
|
10818
|
+
* The identity-wide team/diary binding, read from `<identityDir>/env`.
|
|
10819
|
+
*
|
|
10820
|
+
* This mirrors the Go CLI's `identityDefaultBinding` (`project_selection.go`),
|
|
10821
|
+
* which is the fallback the CLI uses when a working directory has no
|
|
10822
|
+
* registered project binding.
|
|
10823
|
+
*
|
|
10824
|
+
* The desktop cannot use the CLI's *location* bindings at all — its composer
|
|
10825
|
+
* has no working directory to key on — but it can honour this identity-wide
|
|
10826
|
+
* default, so an operator sees their familiar team preselected rather than an
|
|
10827
|
+
* arbitrary first entry.
|
|
10828
|
+
*
|
|
10829
|
+
* Like the CLI, a half-filled pair is treated as no binding: a team without a
|
|
10830
|
+
* diary is ignored.
|
|
10831
|
+
*/
|
|
10832
|
+
function readIdentityDefaultBinding(identityDir) {
|
|
10833
|
+
let contents;
|
|
10834
|
+
try {
|
|
10835
|
+
contents = readFileSync(join(identityDir, "env"), "utf8");
|
|
10836
|
+
} catch {
|
|
10837
|
+
return {};
|
|
10838
|
+
}
|
|
10839
|
+
const env = parseEnv(contents);
|
|
10840
|
+
const teamId = env["MOLTNET_TEAM_ID"]?.trim();
|
|
10841
|
+
const diaryId = env["MOLTNET_DIARY_ID"]?.trim();
|
|
10842
|
+
if (!teamId || !diaryId) return {};
|
|
10843
|
+
return {
|
|
10844
|
+
teamId,
|
|
10845
|
+
diaryId
|
|
10846
|
+
};
|
|
10847
|
+
}
|
|
10848
|
+
async function verifyProjectTarget(reader, target, options) {
|
|
10849
|
+
const { signal, logger } = options;
|
|
10850
|
+
let project = null;
|
|
10851
|
+
let diary = null;
|
|
10852
|
+
try {
|
|
10853
|
+
await untilAborted(signal, async () => {
|
|
10854
|
+
if (target.projectId) project = await reader.readProject(target.teamId, target.projectId, signal);
|
|
10855
|
+
if (target.diaryId) diary = await reader.readDiary(target.teamId, target.diaryId, signal);
|
|
10856
|
+
});
|
|
10857
|
+
} catch (error) {
|
|
10858
|
+
if (!signal.aborted) {
|
|
10859
|
+
if (error instanceof TeamCredentialError) throw error;
|
|
10860
|
+
if (notVisible(error)) throw projectUnavailable();
|
|
10861
|
+
}
|
|
10862
|
+
logger?.warn({
|
|
10863
|
+
...safeErrorContext(error),
|
|
10864
|
+
teamId: target.teamId,
|
|
10865
|
+
...target.projectId ? { projectId: target.projectId } : {},
|
|
10866
|
+
...target.diaryId ? { diaryId: target.diaryId } : {},
|
|
10867
|
+
code: signal.aborted ? "agent_server_project_check_aborted" : "agent_server_project_check_failed"
|
|
10868
|
+
}, "AgentServer project check failed");
|
|
10869
|
+
throw checkUnavailable(error, signal.aborted);
|
|
10870
|
+
}
|
|
10871
|
+
const found = project;
|
|
10872
|
+
if (target.projectId && (!found || found.id !== target.projectId || found.teamId !== target.teamId || found.archived)) throw projectUnavailable();
|
|
10873
|
+
const foundDiary = diary;
|
|
10874
|
+
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");
|
|
10875
|
+
return { project: found };
|
|
10876
|
+
}
|
|
10877
|
+
/** Null when the credential cannot see the resource; other failures propagate. */
|
|
10878
|
+
async function visibleOrNull(read) {
|
|
10879
|
+
try {
|
|
10880
|
+
return await read();
|
|
10881
|
+
} catch (error) {
|
|
10882
|
+
if (notVisible(error)) return null;
|
|
10883
|
+
throw error;
|
|
10884
|
+
}
|
|
10885
|
+
}
|
|
10886
|
+
/**
|
|
10887
|
+
* Settles when `work` does or `signal` aborts. The SDK's team, project and
|
|
10888
|
+
* diary reads take no signal, so an in-flight GET may finish in the background;
|
|
10889
|
+
* readers check the signal between steps so nothing further starts.
|
|
10890
|
+
*/
|
|
10891
|
+
async function untilAborted(signal, work) {
|
|
10892
|
+
signal.throwIfAborted();
|
|
10893
|
+
const aborted = new Promise((_, reject) => {
|
|
10894
|
+
signal.addEventListener("abort", () => {
|
|
10895
|
+
const reason = signal.reason;
|
|
10896
|
+
reject(reason instanceof Error ? reason : new Error(String(reason)));
|
|
10897
|
+
}, { once: true });
|
|
10898
|
+
});
|
|
10899
|
+
return Promise.race([work(), aborted]);
|
|
10900
|
+
}
|
|
10901
|
+
/** True only when `cause` is the abort itself, not a failure seen after it fired. */
|
|
10902
|
+
function causedByAbort(cause, signal) {
|
|
10903
|
+
if (!signal.aborted) return false;
|
|
10904
|
+
if (cause === signal.reason) return true;
|
|
10905
|
+
const error = cause;
|
|
10906
|
+
return error?.cause === signal.reason || error?.name === "AbortError" || error?.name === "TimeoutError";
|
|
10907
|
+
}
|
|
10908
|
+
function checkUnavailable(cause, timedOut) {
|
|
10909
|
+
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 });
|
|
10910
|
+
}
|
|
10911
|
+
function projectUnavailable() {
|
|
10912
|
+
return new AgentServerHttpError(400, "project_unavailable", "Verify team access and choose an available project");
|
|
10913
|
+
}
|
|
10914
|
+
function notVisible(error) {
|
|
10915
|
+
return error instanceof MoltNetError && [
|
|
10916
|
+
401,
|
|
10917
|
+
403,
|
|
10918
|
+
404
|
|
10919
|
+
].includes(error.statusCode ?? 0);
|
|
10920
|
+
}
|
|
10921
|
+
//#endregion
|
|
10922
|
+
//#region src/lib/agent-server/protected-roots.ts
|
|
10923
|
+
/**
|
|
10924
|
+
* A worker holds agent and provider keys, so neither a run nor a saved
|
|
10925
|
+
* location may use a folder inside, or containing, the MoltNet store or its
|
|
10926
|
+
* secrets. Both sides are compared canonically.
|
|
10927
|
+
*/
|
|
10928
|
+
function isProtectedFolder(source, roots) {
|
|
10929
|
+
const folder = canonicalOrSelf(source);
|
|
10930
|
+
return roots.some((root) => {
|
|
10931
|
+
const protectedRoot = canonicalOrSelf(root);
|
|
10932
|
+
return within(folder, protectedRoot) || within(protectedRoot, folder);
|
|
10933
|
+
});
|
|
10934
|
+
}
|
|
10935
|
+
var PROTECTED_FOLDER_MESSAGE = "Choose a folder outside the MoltNet configuration store";
|
|
10936
|
+
function canonicalOrSelf(path) {
|
|
10937
|
+
try {
|
|
10938
|
+
return realpathSync.native(path);
|
|
10939
|
+
} catch {
|
|
10940
|
+
return path;
|
|
10941
|
+
}
|
|
10942
|
+
}
|
|
10943
|
+
function within(child, parent) {
|
|
10944
|
+
const suffix = relative(parent, child);
|
|
10945
|
+
return suffix === "" || !isAbsolute(suffix) && suffix !== ".." && !suffix.startsWith(`..${sep}`);
|
|
10946
|
+
}
|
|
10947
|
+
//#endregion
|
|
10948
|
+
//#region src/lib/agent-server/managed-project-selection.ts
|
|
10949
|
+
/** True when the start request names any project selection field. */
|
|
10950
|
+
function requestsProjectSelection(spec) {
|
|
10951
|
+
return spec.projectId !== void 0 || spec.location !== void 0 || spec.source !== void 0 || spec.strategy !== void 0;
|
|
10952
|
+
}
|
|
10953
|
+
/**
|
|
10954
|
+
* Resolve against the base store once; the worker receives only this captured
|
|
10955
|
+
* config. The request in `spec` is never modified: resolved values are
|
|
10956
|
+
* returned in `workspace` and `effective`, so a run can be replayed from what
|
|
10957
|
+
* the caller asked for.
|
|
10958
|
+
*/
|
|
10959
|
+
async function resolveManagedProjectSelection(options) {
|
|
10960
|
+
const { spec, root, cwd, apiUrl, client, signal } = options;
|
|
10961
|
+
if (spec.source !== void 0) {
|
|
10962
|
+
if (!isAbsolute(spec.source)) throw new ProjectConfigError("selection", "Select an absolute source folder");
|
|
10963
|
+
let canonical;
|
|
10964
|
+
try {
|
|
10965
|
+
canonical = await canonicalDirectory(spec.source);
|
|
10966
|
+
} catch (cause) {
|
|
10967
|
+
throw new ProjectConfigError("selection", "The selected folder is unavailable. Choose an existing folder.", { cause });
|
|
10968
|
+
}
|
|
10969
|
+
assertOutsideProtected(canonical, options.protectedRoots);
|
|
10970
|
+
}
|
|
10971
|
+
if (spec.projectId === null && spec.location) throw new ProjectConfigError("selection", "General work cannot also name a location");
|
|
10972
|
+
const selection = await resolveRunProjectSelection({
|
|
10973
|
+
agent: spec.agent,
|
|
10974
|
+
cwd,
|
|
10975
|
+
team: spec.teamId,
|
|
10976
|
+
apiUrl,
|
|
10977
|
+
"config-file": getProjectConfigPath({ root }),
|
|
10978
|
+
general: !spec.projectId && !spec.location,
|
|
10979
|
+
project: spec.projectId ?? void 0,
|
|
10980
|
+
binding: spec.location,
|
|
10981
|
+
source: spec.source,
|
|
10982
|
+
"workspace-strategy": spec.strategy
|
|
10983
|
+
}, {
|
|
10984
|
+
signal,
|
|
10985
|
+
guardSource: (source) => assertOutsideProtected(source, options.protectedRoots)
|
|
10986
|
+
});
|
|
10987
|
+
const chosenSource = selection.workspaceExplicit ? selection.source : void 0;
|
|
10988
|
+
const reader = {
|
|
10989
|
+
readProject: (teamId, projectId, readSignal) => {
|
|
10990
|
+
readSignal.throwIfAborted();
|
|
10991
|
+
return visibleOrNull(() => client.projects.get(projectId, { teamId }));
|
|
10992
|
+
},
|
|
10993
|
+
readDiary: (teamId, diaryId, readSignal) => {
|
|
10994
|
+
readSignal.throwIfAborted();
|
|
10995
|
+
return visibleOrNull(() => client.diaries.get(diaryId, { teamId }));
|
|
10996
|
+
}
|
|
10997
|
+
};
|
|
10998
|
+
const check = {
|
|
10999
|
+
signal,
|
|
11000
|
+
...options.logger ? { logger: options.logger } : {}
|
|
11001
|
+
};
|
|
11002
|
+
const { project } = await verifyProjectTarget(reader, {
|
|
11003
|
+
teamId: spec.teamId,
|
|
11004
|
+
projectId: selection.projectId
|
|
11005
|
+
}, check);
|
|
11006
|
+
const diaryId = spec.diaryId ?? selection.binding?.diaryId ?? project?.defaultDiaryId ?? (selection.projectId === null ? await options.generalDefaultDiary?.() : void 0) ?? void 0;
|
|
11007
|
+
if (diaryId) await verifyProjectTarget(reader, {
|
|
11008
|
+
teamId: spec.teamId,
|
|
11009
|
+
projectId: null,
|
|
11010
|
+
diaryId
|
|
11011
|
+
}, check);
|
|
11012
|
+
const resolvedBinding = selection.binding ? {
|
|
11013
|
+
...selection.binding,
|
|
11014
|
+
...diaryId ? { diaryId } : {}
|
|
11015
|
+
} : void 0;
|
|
11016
|
+
const config = {
|
|
11017
|
+
version: 1,
|
|
11018
|
+
bindings: resolvedBinding ? [resolvedBinding] : []
|
|
11019
|
+
};
|
|
11020
|
+
const workspace = {
|
|
11021
|
+
projectId: selection.projectId,
|
|
11022
|
+
...resolvedBinding ? { location: resolvedBinding.name } : {},
|
|
11023
|
+
...diaryId ? { diaryId } : {},
|
|
11024
|
+
...chosenSource ? { source: chosenSource } : {},
|
|
11025
|
+
strategy: selection.workspaceExplicit ? selection.strategy : PROFILE_DEFAULT_STRATEGY
|
|
11026
|
+
};
|
|
11027
|
+
return {
|
|
11028
|
+
selection: {
|
|
11029
|
+
...selection,
|
|
11030
|
+
binding: resolvedBinding
|
|
11031
|
+
},
|
|
11032
|
+
workspace,
|
|
11033
|
+
config,
|
|
11034
|
+
effective: {
|
|
11035
|
+
...spec,
|
|
11036
|
+
projectId: selection.projectId,
|
|
11037
|
+
...diaryId ? { diaryId } : {}
|
|
11038
|
+
}
|
|
11039
|
+
};
|
|
11040
|
+
}
|
|
11041
|
+
function assertOutsideProtected(source, roots) {
|
|
11042
|
+
if (isProtectedFolder(source, roots)) throw new ProjectConfigError("selection", PROTECTED_FOLDER_MESSAGE);
|
|
11043
|
+
}
|
|
11044
|
+
//#endregion
|
|
10557
11045
|
//#region src/lib/agent-server/runs.ts
|
|
10558
11046
|
var STOP_GRACE_MS = 1e4;
|
|
10559
11047
|
var STOP_FORCE_MS = 2e3;
|
|
@@ -10642,7 +11130,7 @@ var RunManager = class {
|
|
|
10642
11130
|
};
|
|
10643
11131
|
}
|
|
10644
11132
|
/** Assemble child env + args for a run. Exposed for tests. */
|
|
10645
|
-
async prepare(spec, agent, piDir, providers = this.store.readProviders(), runtimeModule) {
|
|
11133
|
+
async prepare(spec, agent, piDir, providers = this.store.readProviders(), runtimeModule, workspace) {
|
|
10646
11134
|
const { activation, config } = agent;
|
|
10647
11135
|
const homeDir = join(dirname(piDir), "home");
|
|
10648
11136
|
const env = {
|
|
@@ -10687,7 +11175,16 @@ var RunManager = class {
|
|
|
10687
11175
|
String(runtimeSettings.heartbeatIntervalMs),
|
|
10688
11176
|
"--warm-retention-sec",
|
|
10689
11177
|
String(runtimeSettings.warmRetentionSec),
|
|
10690
|
-
...target.extraArgs
|
|
11178
|
+
...target.extraArgs,
|
|
11179
|
+
...workspace ? projectRunArgs({
|
|
11180
|
+
"config-file": workspace.configPath,
|
|
11181
|
+
...workspace.location ? { binding: workspace.location } : { general: true },
|
|
11182
|
+
...workspace.strategy === "profile-default" ? {} : {
|
|
11183
|
+
source: workspace.source,
|
|
11184
|
+
"workspace-strategy": workspace.strategy
|
|
11185
|
+
},
|
|
11186
|
+
"state-dir": this.runStateDir(spec, workspace)
|
|
11187
|
+
}) : []
|
|
10691
11188
|
];
|
|
10692
11189
|
env["MOLTNET_AGENT_KEY"] = requireCredentialSnapshot(agent).agentKey;
|
|
10693
11190
|
env["MOLTNET_API_URL"] = activation.apiUrl ?? (activation.source === "external" ? activation.configApiUrl : "");
|
|
@@ -10718,10 +11215,11 @@ var RunManager = class {
|
|
|
10718
11215
|
return {
|
|
10719
11216
|
args,
|
|
10720
11217
|
env,
|
|
10721
|
-
cwd: target.cwd
|
|
11218
|
+
cwd: workspace?.source ?? (workspace ? join(dirname(piDir), "workspace") : target.cwd)
|
|
10722
11219
|
};
|
|
10723
11220
|
}
|
|
10724
11221
|
async start(spec, signal) {
|
|
11222
|
+
spec = structuredClone(spec);
|
|
10725
11223
|
validateRunSpec(spec);
|
|
10726
11224
|
const releaseStart = this.reserveStart(spec.agent);
|
|
10727
11225
|
try {
|
|
@@ -10732,24 +11230,65 @@ var RunManager = class {
|
|
|
10732
11230
|
}
|
|
10733
11231
|
async startReserved(spec, signal) {
|
|
10734
11232
|
this.assertStartOpen(signal);
|
|
10735
|
-
const
|
|
11233
|
+
const deadline = AbortSignal.any([...signal ? [signal] : [], AbortSignal.timeout(this.options.startTimeoutMs ?? 1e4)]);
|
|
11234
|
+
const verify = this.options.verifyActivationImpl ?? verifyTeamActivation;
|
|
11235
|
+
const agent = await untilAborted(deadline, () => verify(this.store, spec.agent, this.options.secretProviders, this.options.externalSecretProviders, void 0, deadline, spec.teamId)).catch((cause) => {
|
|
11236
|
+
this.assertStartOpen(signal, deadline, cause);
|
|
10736
11237
|
if (cause instanceof TeamCredentialError || cause instanceof AgentServerStoreError) throw cause;
|
|
10737
11238
|
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
11239
|
});
|
|
10739
|
-
this.assertStartOpen(signal);
|
|
11240
|
+
this.assertStartOpen(signal, deadline);
|
|
10740
11241
|
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
11242
|
const id = `${Date.now().toString(36)}-${randomBytes(4).toString("hex")}`;
|
|
10742
|
-
const runDir = this.store.runDir(id);
|
|
11243
|
+
const runDir = resolve(this.store.runDir(id));
|
|
10743
11244
|
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
11245
|
let child;
|
|
10749
11246
|
let logStream;
|
|
10750
11247
|
let logLimiter;
|
|
11248
|
+
let workspace;
|
|
11249
|
+
let effective = spec;
|
|
11250
|
+
let selection;
|
|
10751
11251
|
try {
|
|
10752
11252
|
const { logPath } = this.store.createRunDir(id);
|
|
11253
|
+
const executionDir = join(runDir, "workspace");
|
|
11254
|
+
if (requestsProjectSelection(spec)) {
|
|
11255
|
+
mkdirSync(executionDir, {
|
|
11256
|
+
recursive: true,
|
|
11257
|
+
mode: 448
|
|
11258
|
+
});
|
|
11259
|
+
const projectRoot = this.options.projectRoot ?? this.store.root;
|
|
11260
|
+
const resolved = await resolveManagedProjectSelection({
|
|
11261
|
+
spec,
|
|
11262
|
+
root: projectRoot,
|
|
11263
|
+
cwd: executionDir,
|
|
11264
|
+
apiUrl: agent.activation.apiUrl ?? agent.config.endpoints.api,
|
|
11265
|
+
client: requireCredentialSnapshot(agent).client,
|
|
11266
|
+
protectedRoots: [
|
|
11267
|
+
projectRoot,
|
|
11268
|
+
this.store.root,
|
|
11269
|
+
this.store.secretsDir
|
|
11270
|
+
],
|
|
11271
|
+
signal: deadline,
|
|
11272
|
+
logger: { warn: (context, message) => this.log("warn", message, context) },
|
|
11273
|
+
generalDefaultDiary: () => this.generalDefaultDiary(spec, agent, deadline)
|
|
11274
|
+
});
|
|
11275
|
+
workspace = {
|
|
11276
|
+
...resolved.workspace,
|
|
11277
|
+
configPath: join(runDir, "projects.json")
|
|
11278
|
+
};
|
|
11279
|
+
effective = resolved.effective;
|
|
11280
|
+
selection = resolved.selection;
|
|
11281
|
+
writeRunSnapshot(workspace.configPath, resolved.config);
|
|
11282
|
+
mkdirSync(this.runStateDir(spec, workspace), {
|
|
11283
|
+
recursive: true,
|
|
11284
|
+
mode: 448
|
|
11285
|
+
});
|
|
11286
|
+
}
|
|
11287
|
+
const providers = this.store.readProviders();
|
|
11288
|
+
const executionCwd = workspace ? workspace.source ?? executionDir : dirname(piDir);
|
|
11289
|
+
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);
|
|
11290
|
+
const { args, env, cwd } = await this.prepare(effective, agent, piDir, providers, runtimeModule, workspace);
|
|
11291
|
+
this.assertStartOpen(signal, deadline);
|
|
10753
11292
|
for (const dir of [
|
|
10754
11293
|
env.HOME,
|
|
10755
11294
|
env.XDG_CACHE_HOME,
|
|
@@ -10780,7 +11319,7 @@ var RunManager = class {
|
|
|
10780
11319
|
child?.kill("SIGKILL");
|
|
10781
11320
|
});
|
|
10782
11321
|
const spawnImpl = this.options.spawnImpl ?? spawn;
|
|
10783
|
-
this.assertStartOpen(signal);
|
|
11322
|
+
this.assertStartOpen(signal, deadline);
|
|
10784
11323
|
child = spawnImpl(entry.execPath, [
|
|
10785
11324
|
...entry.execArgv,
|
|
10786
11325
|
entry.scriptPath,
|
|
@@ -10808,6 +11347,7 @@ var RunManager = class {
|
|
|
10808
11347
|
child.stderr?.pipe(logLimiter, { end: false });
|
|
10809
11348
|
const record = {
|
|
10810
11349
|
...spec,
|
|
11350
|
+
...workspace ? { workspace } : {},
|
|
10811
11351
|
id,
|
|
10812
11352
|
status: "running",
|
|
10813
11353
|
pid: child.pid,
|
|
@@ -10850,6 +11390,7 @@ var RunManager = class {
|
|
|
10850
11390
|
this.store.writeRun(record);
|
|
10851
11391
|
this.log("info", "agent server run started", {
|
|
10852
11392
|
...runContext(id, spec.agent, child),
|
|
11393
|
+
...selectionContext(spec, workspace),
|
|
10853
11394
|
transition: "running"
|
|
10854
11395
|
});
|
|
10855
11396
|
return record;
|
|
@@ -10864,20 +11405,29 @@ var RunManager = class {
|
|
|
10864
11405
|
});
|
|
10865
11406
|
this.log("error", "agent server run failed to start", {
|
|
10866
11407
|
...runContext(id, spec.agent, child),
|
|
11408
|
+
...selectionContext(spec, workspace),
|
|
10867
11409
|
transition: "start_failed",
|
|
10868
11410
|
...safeRunError(cause)
|
|
10869
11411
|
});
|
|
11412
|
+
if (!signal?.aborted && !this.closing && !(cause instanceof AgentServerHttpError) && causedByAbort(cause, deadline)) throw startTimedOut(cause);
|
|
10870
11413
|
throw cause;
|
|
10871
11414
|
}
|
|
10872
11415
|
}
|
|
10873
|
-
async resolveRuntimeModule(spec, activated, cwd) {
|
|
10874
|
-
const
|
|
11416
|
+
async resolveRuntimeModule(spec, activated, cwd, selection) {
|
|
11417
|
+
const effectiveProfiles = (await resolveRuntimeProfiles({
|
|
10875
11418
|
agent: await this.connectAgent(activated, spec.teamId),
|
|
10876
11419
|
profiles: spec.profiles,
|
|
10877
11420
|
teamId: spec.teamId,
|
|
10878
11421
|
cwd
|
|
11422
|
+
})).map((profile) => {
|
|
11423
|
+
if (!selection) return profile;
|
|
11424
|
+
try {
|
|
11425
|
+
return applyProjectWorkspacePolicy(profile, selection);
|
|
11426
|
+
} catch (error) {
|
|
11427
|
+
throw new AgentServerRunError("invalid_spec", error instanceof Error ? error.message : "This profile does not support the selected workspace strategy");
|
|
11428
|
+
}
|
|
10879
11429
|
});
|
|
10880
|
-
const kinds = [...new Set(
|
|
11430
|
+
const kinds = [...new Set(effectiveProfiles.map((profile) => profile.runtimeKind))];
|
|
10881
11431
|
if (kinds.length !== 1) throw new AgentServerRunError("invalid_spec", "All profiles in a server-managed run must use the same runtime kind.");
|
|
10882
11432
|
const kind = kinds[0];
|
|
10883
11433
|
let registration;
|
|
@@ -11022,8 +11572,10 @@ var RunManager = class {
|
|
|
11022
11572
|
else this.startingByAgent.delete(agent);
|
|
11023
11573
|
};
|
|
11024
11574
|
}
|
|
11025
|
-
|
|
11575
|
+
/** Shutdown or disconnect wins; otherwise an expired budget is a retryable 503. */
|
|
11576
|
+
assertStartOpen(signal, deadline, cause) {
|
|
11026
11577
|
if (this.closing || signal?.aborted) throw new AgentServerRunError("invalid_spec", "agent server is shutting down");
|
|
11578
|
+
if (deadline?.aborted && (cause === void 0 || causedByAbort(cause, deadline))) throw startTimedOut(cause);
|
|
11027
11579
|
}
|
|
11028
11580
|
pruneCompletedRuns() {
|
|
11029
11581
|
const removed = this.store.pruneCompletedRuns({
|
|
@@ -11055,6 +11607,36 @@ var RunManager = class {
|
|
|
11055
11607
|
});
|
|
11056
11608
|
}
|
|
11057
11609
|
}
|
|
11610
|
+
/**
|
|
11611
|
+
* Same rule as the catalogue. Best effort: without it the worker resolves its
|
|
11612
|
+
* own diary, as it did before project selection existed.
|
|
11613
|
+
*/
|
|
11614
|
+
async generalDefaultDiary(spec, agent, deadline) {
|
|
11615
|
+
try {
|
|
11616
|
+
const { items } = await untilAborted(deadline, () => requireCredentialSnapshot(agent).client.diaries.list());
|
|
11617
|
+
return resolveDefaultDiary(spec.teamId, items.filter((diary) => diary.teamId === spec.teamId), readIdentityDefaultBinding(this.store.identityDir(spec.agent))) ?? void 0;
|
|
11618
|
+
} catch (error) {
|
|
11619
|
+
this.log("warn", "agent server default diary lookup failed", {
|
|
11620
|
+
...safeRunError(error),
|
|
11621
|
+
agent: spec.agent,
|
|
11622
|
+
teamId: spec.teamId
|
|
11623
|
+
});
|
|
11624
|
+
return;
|
|
11625
|
+
}
|
|
11626
|
+
}
|
|
11627
|
+
/**
|
|
11628
|
+
* Stable across runs, so retries and continuations find their execution-plan
|
|
11629
|
+
* cache and task workspaces. Keyed by agent and location, under the store,
|
|
11630
|
+
* never inside the user's folder.
|
|
11631
|
+
*/
|
|
11632
|
+
runStateDir(spec, workspace) {
|
|
11633
|
+
const scope = createHash("sha256").update(JSON.stringify([
|
|
11634
|
+
spec.teamId,
|
|
11635
|
+
workspace.projectId,
|
|
11636
|
+
workspace.source ?? null
|
|
11637
|
+
])).digest("hex").slice(0, 12);
|
|
11638
|
+
return join(this.store.root, "run-state", spec.agent, `${workspace.location ? locationSegment(workspace.location) : "general"}-${scope}`);
|
|
11639
|
+
}
|
|
11058
11640
|
log(level, message, context) {
|
|
11059
11641
|
this.options.logger?.[level](context, message);
|
|
11060
11642
|
}
|
|
@@ -11082,6 +11664,33 @@ function createByteLimitTransform(limit, onTruncated) {
|
|
|
11082
11664
|
callback();
|
|
11083
11665
|
} });
|
|
11084
11666
|
}
|
|
11667
|
+
/** Non-secret ids that tie a run's logs to its project selection. */
|
|
11668
|
+
function selectionContext(spec, workspace) {
|
|
11669
|
+
return {
|
|
11670
|
+
...spec.projectId !== void 0 ? { requestedProjectId: spec.projectId } : {},
|
|
11671
|
+
...spec.location ? { requestedLocation: spec.location } : {},
|
|
11672
|
+
...workspace ? {
|
|
11673
|
+
projectId: workspace.projectId,
|
|
11674
|
+
...workspace.location ? { location: workspace.location } : {},
|
|
11675
|
+
...workspace.diaryId ? { diaryId: workspace.diaryId } : {},
|
|
11676
|
+
strategy: workspace.strategy
|
|
11677
|
+
} : {}
|
|
11678
|
+
};
|
|
11679
|
+
}
|
|
11680
|
+
/** Readable when it is a safe path segment; hashed otherwise. */
|
|
11681
|
+
function locationSegment(name) {
|
|
11682
|
+
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)}`;
|
|
11683
|
+
}
|
|
11684
|
+
function startTimedOut(cause) {
|
|
11685
|
+
return new AgentServerHttpError(503, "start_timeout", "The run could not be prepared in time, so it was not started. Retry in a moment.", { cause });
|
|
11686
|
+
}
|
|
11687
|
+
/** Created exclusively and owner-only: a run never reuses or widens a snapshot. */
|
|
11688
|
+
function writeRunSnapshot(path, config) {
|
|
11689
|
+
writeFileSync(path, JSON.stringify(config), {
|
|
11690
|
+
mode: 384,
|
|
11691
|
+
flag: "wx"
|
|
11692
|
+
});
|
|
11693
|
+
}
|
|
11085
11694
|
function runContext(runId, agent, child) {
|
|
11086
11695
|
return {
|
|
11087
11696
|
runId,
|
|
@@ -11309,112 +11918,6 @@ function createAgentServerSecretProviders(settings, store) {
|
|
|
11309
11918
|
};
|
|
11310
11919
|
}
|
|
11311
11920
|
//#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
11921
|
//#region src/lib/agent-server/enrollment.ts
|
|
11419
11922
|
/** Native callers receive metadata only; approval and storage stay local. */
|
|
11420
11923
|
async function enrollIdentityTeam(options) {
|
|
@@ -11499,37 +12002,139 @@ async function enrollIdentityTeam(options) {
|
|
|
11499
12002
|
}
|
|
11500
12003
|
}
|
|
11501
12004
|
//#endregion
|
|
11502
|
-
//#region src/lib/agent-server/
|
|
12005
|
+
//#region src/lib/agent-server/project-bindings.ts
|
|
12006
|
+
/** Each check may spawn `git`; bound how many run at once. */
|
|
12007
|
+
var READINESS_CONCURRENCY = 4;
|
|
12008
|
+
var PREPARATION_COPY = {
|
|
12009
|
+
unsupported_strategy: "Isolated directory preparation is unavailable. Choose Work here or a Git worktree.",
|
|
12010
|
+
hooks_unavailable: "Preparation hooks are unavailable. Remove the hooks or choose another location."
|
|
12011
|
+
};
|
|
12012
|
+
/** Normalize with the Go-compatible grammar, reporting a rejection as a coded error. */
|
|
12013
|
+
function locationEndpoint(value) {
|
|
12014
|
+
try {
|
|
12015
|
+
return normalizeProjectEndpoint(value);
|
|
12016
|
+
} catch {
|
|
12017
|
+
throw new AgentServerHttpError(400, "endpoint_unsupported", "Local project locations need an HTTPS server endpoint; HTTP is allowed only on loopback");
|
|
12018
|
+
}
|
|
12019
|
+
}
|
|
12020
|
+
/** Machine registrations live at the base store, outside connection directories. */
|
|
12021
|
+
var LocalProjectBindings = class {
|
|
12022
|
+
root;
|
|
12023
|
+
path;
|
|
12024
|
+
apiUrl;
|
|
12025
|
+
/** `protectedRoots`: store and secrets directories no location may use. */
|
|
12026
|
+
constructor(root, apiUrl, protectedRoots = []) {
|
|
12027
|
+
this.protectedRoots = protectedRoots;
|
|
12028
|
+
this.root = resolveStoreRoot({ root });
|
|
12029
|
+
this.path = getProjectConfigPath({ root: this.root });
|
|
12030
|
+
this.apiUrl = locationEndpoint(apiUrl);
|
|
12031
|
+
}
|
|
12032
|
+
async list() {
|
|
12033
|
+
const bindings = (await stored(() => readProjectConfig(this.path))).bindings.filter((binding) => normalizeProjectEndpoint(binding.apiUrl) === this.apiUrl);
|
|
12034
|
+
const locations = [];
|
|
12035
|
+
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))));
|
|
12036
|
+
return locations;
|
|
12037
|
+
}
|
|
12038
|
+
/** Once `signal` aborts, nothing is written, not even after the lock is taken. */
|
|
12039
|
+
async save(value, options = {}) {
|
|
12040
|
+
const input = structuredClone(value);
|
|
12041
|
+
validateProjectConfig({
|
|
12042
|
+
version: 1,
|
|
12043
|
+
bindings: [input]
|
|
12044
|
+
});
|
|
12045
|
+
if (locationEndpoint(input.apiUrl) !== this.apiUrl) throw new AgentServerHttpError(400, "endpoint_mismatch", "Location belongs to another endpoint");
|
|
12046
|
+
if (input.source && !isAbsolute(input.source)) throw new ProjectConfigError("validation", "Select an absolute source folder");
|
|
12047
|
+
const location = await this.describe(input, options.signal);
|
|
12048
|
+
if (!location.readiness.ready) throw new AgentServerHttpError(400, location.readiness.code ?? "location_unavailable", location.readiness.message ?? "Location is unavailable");
|
|
12049
|
+
const binding = {
|
|
12050
|
+
...input,
|
|
12051
|
+
apiUrl: this.apiUrl,
|
|
12052
|
+
...location.effectiveSource ? { source: location.effectiveSource } : {}
|
|
12053
|
+
};
|
|
12054
|
+
options.signal?.throwIfAborted();
|
|
12055
|
+
await this.update((config) => {
|
|
12056
|
+
options.signal?.throwIfAborted();
|
|
12057
|
+
const index = config.bindings.findIndex((entry) => entry.name === binding.name);
|
|
12058
|
+
const previous = config.bindings[index];
|
|
12059
|
+
if (previous && normalizeProjectEndpoint(previous.apiUrl) !== this.apiUrl) throw conflict("This name belongs to another endpoint");
|
|
12060
|
+
if (previous && hasPreparationHooks(previous.hooks)) throw conflict("Remove preparation hooks from the configuration before editing this location");
|
|
12061
|
+
if (previous && (previous.teamId !== binding.teamId || previous.projectId !== binding.projectId)) throw conflict("This name belongs to another project");
|
|
12062
|
+
if (binding.default) {
|
|
12063
|
+
for (const entry of config.bindings) if (normalizeProjectEndpoint(entry.apiUrl) === this.apiUrl && entry.teamId === binding.teamId && entry.projectId === binding.projectId) entry.default = false;
|
|
12064
|
+
}
|
|
12065
|
+
if (index < 0) config.bindings.push(binding);
|
|
12066
|
+
else config.bindings[index] = binding;
|
|
12067
|
+
});
|
|
12068
|
+
return this.describe(binding);
|
|
12069
|
+
}
|
|
12070
|
+
async remove(name) {
|
|
12071
|
+
await this.update((config) => {
|
|
12072
|
+
const index = config.bindings.findIndex((entry) => entry.name === name && normalizeProjectEndpoint(entry.apiUrl) === this.apiUrl);
|
|
12073
|
+
if (index < 0) throw new AgentServerHttpError(404, "location_not_found", "Location not found");
|
|
12074
|
+
config.bindings.splice(index, 1);
|
|
12075
|
+
});
|
|
12076
|
+
}
|
|
12077
|
+
update(mutate) {
|
|
12078
|
+
return stored(() => updateProjectConfig(this.path, mutate));
|
|
12079
|
+
}
|
|
12080
|
+
async describe(binding, signal) {
|
|
12081
|
+
const { effectiveSource, readiness } = await this.readiness(binding, signal);
|
|
12082
|
+
return {
|
|
12083
|
+
...binding,
|
|
12084
|
+
effectiveSource,
|
|
12085
|
+
readiness
|
|
12086
|
+
};
|
|
12087
|
+
}
|
|
12088
|
+
async readiness(binding, signal) {
|
|
12089
|
+
const declared = binding.source ? resolve(this.root, binding.source) : null;
|
|
12090
|
+
const unavailable = (code, message, effectiveSource = declared) => ({
|
|
12091
|
+
effectiveSource,
|
|
12092
|
+
readiness: {
|
|
12093
|
+
ready: false,
|
|
12094
|
+
code,
|
|
12095
|
+
message
|
|
12096
|
+
}
|
|
12097
|
+
});
|
|
12098
|
+
const blocker = preparationBlocker(binding.strategy, binding.hooks);
|
|
12099
|
+
if (blocker) return unavailable(blocker.code, PREPARATION_COPY[blocker.code]);
|
|
12100
|
+
if (binding.strategy === "none") return {
|
|
12101
|
+
effectiveSource: declared,
|
|
12102
|
+
readiness: { ready: true }
|
|
12103
|
+
};
|
|
12104
|
+
if (!declared) return unavailable("folder_missing", "Choose an existing source folder.");
|
|
12105
|
+
let source;
|
|
12106
|
+
try {
|
|
12107
|
+
source = await canonicalDirectory(declared);
|
|
12108
|
+
} catch {
|
|
12109
|
+
return unavailable("folder_missing", "The source folder is unavailable. Choose an existing folder.");
|
|
12110
|
+
}
|
|
12111
|
+
if (isProtectedFolder(source, this.protectedRoots)) return unavailable("folder_protected", `${PROTECTED_FOLDER_MESSAGE}.`, source);
|
|
12112
|
+
if (binding.strategy === "git-worktree") try {
|
|
12113
|
+
await validateGitSource(source, signal);
|
|
12114
|
+
} catch (cause) {
|
|
12115
|
+
if (signal?.aborted) throw cause;
|
|
12116
|
+
return unavailable("git_unavailable", "Choose a Git repository root with a committed revision, or choose Work here.", source);
|
|
12117
|
+
}
|
|
12118
|
+
return {
|
|
12119
|
+
effectiveSource: source,
|
|
12120
|
+
readiness: { ready: true }
|
|
12121
|
+
};
|
|
12122
|
+
}
|
|
12123
|
+
};
|
|
11503
12124
|
/**
|
|
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.
|
|
12125
|
+
* A stored file that fails validation is server-side state, not a bad request.
|
|
12126
|
+
* The detail names a local path, so it goes to the logs via `cause`.
|
|
11517
12127
|
*/
|
|
11518
|
-
function
|
|
11519
|
-
let contents;
|
|
12128
|
+
async function stored(work) {
|
|
11520
12129
|
try {
|
|
11521
|
-
|
|
11522
|
-
} catch {
|
|
11523
|
-
|
|
12130
|
+
return await work();
|
|
12131
|
+
} catch (error) {
|
|
12132
|
+
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 });
|
|
12133
|
+
throw error;
|
|
11524
12134
|
}
|
|
11525
|
-
|
|
11526
|
-
|
|
11527
|
-
|
|
11528
|
-
if (!teamId || !diaryId) return {};
|
|
11529
|
-
return {
|
|
11530
|
-
teamId,
|
|
11531
|
-
diaryId
|
|
11532
|
-
};
|
|
12135
|
+
}
|
|
12136
|
+
function conflict(message) {
|
|
12137
|
+
return new AgentServerHttpError(409, "location_conflict", message);
|
|
11533
12138
|
}
|
|
11534
12139
|
//#endregion
|
|
11535
12140
|
//#region src/lib/agent-server/protocol.ts
|
|
@@ -11631,10 +12236,83 @@ var AgentServerCatalogueProfileSchema = Type.Intersect([Type.Pick(RuntimeProfile
|
|
|
11631
12236
|
var AgentServerCatalogueSchema = Type.Object({
|
|
11632
12237
|
teams: Type.Array(schemaRef(AgentServerCatalogueTeamSchema)),
|
|
11633
12238
|
defaultTeamId: Type.Union([Type.String(), Type.Null()]),
|
|
11634
|
-
profiles: Type.Array(schemaRef(AgentServerCatalogueProfileSchema))
|
|
12239
|
+
profiles: Type.Array(schemaRef(AgentServerCatalogueProfileSchema)),
|
|
12240
|
+
projects: Type.Array(Type.Pick(ProjectResponseSchema, [
|
|
12241
|
+
"id",
|
|
12242
|
+
"teamId",
|
|
12243
|
+
"name",
|
|
12244
|
+
"description",
|
|
12245
|
+
"defaultDiaryId",
|
|
12246
|
+
"archived"
|
|
12247
|
+
])),
|
|
12248
|
+
projectErrors: Type.Array(Type.Object({
|
|
12249
|
+
teamId: Type.String(),
|
|
12250
|
+
code: Type.Union([
|
|
12251
|
+
"forbidden",
|
|
12252
|
+
"unreachable",
|
|
12253
|
+
"invalid_response",
|
|
12254
|
+
"truncated"
|
|
12255
|
+
].map((value) => Type.Literal(value))),
|
|
12256
|
+
message: Type.String()
|
|
12257
|
+
}))
|
|
11635
12258
|
}, { $id: "AgentServerCatalogue" });
|
|
11636
12259
|
var CatalogueQuerySchema = Type.Object({ identity: Type.String({ minLength: 1 }) });
|
|
12260
|
+
var AgentServerProjectLocationSchema = Type.Object({
|
|
12261
|
+
name: Type.String({ minLength: 1 }),
|
|
12262
|
+
apiUrl: Type.String(),
|
|
12263
|
+
teamId: Type.String({ minLength: 1 }),
|
|
12264
|
+
projectId: Type.String({ minLength: 1 }),
|
|
12265
|
+
diaryId: Type.Optional(Type.String({ minLength: 1 })),
|
|
12266
|
+
source: Type.Optional(Type.String({ minLength: 1 })),
|
|
12267
|
+
strategy: Type.Union([
|
|
12268
|
+
"none",
|
|
12269
|
+
"existing",
|
|
12270
|
+
"git-worktree",
|
|
12271
|
+
"isolated-directory"
|
|
12272
|
+
].map((value) => Type.Literal(value))),
|
|
12273
|
+
default: Type.Optional(Type.Boolean()),
|
|
12274
|
+
effectiveSource: Type.Union([Type.String(), Type.Null()]),
|
|
12275
|
+
readiness: Type.Object({
|
|
12276
|
+
ready: Type.Boolean(),
|
|
12277
|
+
code: Type.Optional(Type.String()),
|
|
12278
|
+
message: Type.Optional(Type.String())
|
|
12279
|
+
})
|
|
12280
|
+
}, { $id: "AgentServerProjectLocation" });
|
|
12281
|
+
/** Closed: hooks and other stored fields are never writable over this route. */
|
|
12282
|
+
var SaveProjectLocationSchema = Type.Object({
|
|
12283
|
+
identity: Type.String({ minLength: 1 }),
|
|
12284
|
+
teamId: Type.String({ minLength: 1 }),
|
|
12285
|
+
projectId: Type.String({ minLength: 1 }),
|
|
12286
|
+
diaryId: Type.Optional(Type.String({ minLength: 1 })),
|
|
12287
|
+
source: Type.Optional(Type.String({ minLength: 1 })),
|
|
12288
|
+
strategy: Type.Union([
|
|
12289
|
+
"none",
|
|
12290
|
+
"existing",
|
|
12291
|
+
"git-worktree"
|
|
12292
|
+
].map((value) => Type.Literal(value))),
|
|
12293
|
+
default: Type.Optional(Type.Boolean())
|
|
12294
|
+
}, { additionalProperties: false });
|
|
12295
|
+
var ProjectLocationParamsSchema = Type.Object({ name: Type.String({ minLength: 1 }) });
|
|
12296
|
+
/** Local project selection on a run request; only the native client may set these. */
|
|
12297
|
+
var RunProjectFields = {
|
|
12298
|
+
projectId: Type.Optional(Type.Union([Type.String({ minLength: 1 }), Type.Null()])),
|
|
12299
|
+
location: Type.Optional(Type.String({ minLength: 1 })),
|
|
12300
|
+
source: Type.Optional(Type.String({ minLength: 1 })),
|
|
12301
|
+
strategy: Type.Optional(AgentServerProjectLocationSchema.properties.strategy)
|
|
12302
|
+
};
|
|
12303
|
+
/** Names gated to the native client; derived so a new field is gated automatically. */
|
|
12304
|
+
var NATIVE_RUN_FIELDS = Object.keys(RunProjectFields);
|
|
12305
|
+
/** What a run resolved to; the record's top-level fields stay as requested. */
|
|
12306
|
+
var RunWorkspaceSchema = Type.Object({
|
|
12307
|
+
projectId: Type.Union([Type.String(), Type.Null()]),
|
|
12308
|
+
location: Type.Optional(Type.String()),
|
|
12309
|
+
diaryId: Type.Optional(Type.String()),
|
|
12310
|
+
source: Type.Optional(Type.String()),
|
|
12311
|
+
strategy: Type.Union([AgentServerProjectLocationSchema.properties.strategy, Type.Literal(PROFILE_DEFAULT_STRATEGY)])
|
|
12312
|
+
});
|
|
11637
12313
|
var AgentServerRunRecordSchema = Type.Object({
|
|
12314
|
+
...RunProjectFields,
|
|
12315
|
+
workspace: Type.Optional(RunWorkspaceSchema),
|
|
11638
12316
|
id: Type.String(),
|
|
11639
12317
|
agent: Type.String(),
|
|
11640
12318
|
teamId: Type.String(),
|
|
@@ -11714,6 +12392,7 @@ var PutProviderSchema = Type.Object({
|
|
|
11714
12392
|
});
|
|
11715
12393
|
var DiscoverModelsSchema = Type.Object({ models: ProviderModelList }, { $id: "DiscoveredModels" });
|
|
11716
12394
|
var StartRunSchema = Type.Object({
|
|
12395
|
+
...RunProjectFields,
|
|
11717
12396
|
agent: Type.String(),
|
|
11718
12397
|
teamId: Type.String(),
|
|
11719
12398
|
diaryId: Type.Optional(Type.String()),
|
|
@@ -11730,6 +12409,7 @@ var LogStreamSchema = Type.String({
|
|
|
11730
12409
|
contentMediaType: "text/event-stream"
|
|
11731
12410
|
});
|
|
11732
12411
|
var AGENT_SERVER_SCHEMAS = [
|
|
12412
|
+
AgentServerProjectLocationSchema,
|
|
11733
12413
|
AgentServerHealthSchema,
|
|
11734
12414
|
AgentServerProblemSchema,
|
|
11735
12415
|
AgentServerAgentSchema,
|
|
@@ -11752,6 +12432,39 @@ var AGENT_SERVER_SCHEMAS = [
|
|
|
11752
12432
|
var localControlSecurity = [{ agentServerToken: [] }];
|
|
11753
12433
|
var problemResponse = { default: schemaRef(AgentServerProblemSchema) };
|
|
11754
12434
|
var AgentServerRouteSchemas = {
|
|
12435
|
+
listProjectLocations: {
|
|
12436
|
+
operationId: "listNativeProjectLocations",
|
|
12437
|
+
tags: ["native-projects"],
|
|
12438
|
+
security: localControlSecurity,
|
|
12439
|
+
description: "Requires the Desktop native grant; browser authorization is insufficient.",
|
|
12440
|
+
response: {
|
|
12441
|
+
200: Type.Object({ locations: Type.Array(schemaRef(AgentServerProjectLocationSchema)) }),
|
|
12442
|
+
...problemResponse
|
|
12443
|
+
}
|
|
12444
|
+
},
|
|
12445
|
+
saveProjectLocation: {
|
|
12446
|
+
operationId: "saveNativeProjectLocation",
|
|
12447
|
+
tags: ["native-projects"],
|
|
12448
|
+
security: localControlSecurity,
|
|
12449
|
+
description: "Creates or replaces the named location. Requires the Desktop native grant.",
|
|
12450
|
+
params: ProjectLocationParamsSchema,
|
|
12451
|
+
body: SaveProjectLocationSchema,
|
|
12452
|
+
response: {
|
|
12453
|
+
200: schemaRef(AgentServerProjectLocationSchema),
|
|
12454
|
+
...problemResponse
|
|
12455
|
+
}
|
|
12456
|
+
},
|
|
12457
|
+
removeProjectLocation: {
|
|
12458
|
+
operationId: "removeNativeProjectLocation",
|
|
12459
|
+
tags: ["native-projects"],
|
|
12460
|
+
security: localControlSecurity,
|
|
12461
|
+
description: "Removes the registration only; the folder is left untouched. Requires the Desktop native grant.",
|
|
12462
|
+
params: ProjectLocationParamsSchema,
|
|
12463
|
+
response: {
|
|
12464
|
+
200: Type.Object({ removed: Type.Boolean() }),
|
|
12465
|
+
...problemResponse
|
|
12466
|
+
}
|
|
12467
|
+
},
|
|
11755
12468
|
health: {
|
|
11756
12469
|
operationId: "getAgentServerHealth",
|
|
11757
12470
|
tags: ["system"],
|
|
@@ -11925,6 +12638,7 @@ var AgentServerRouteSchemas = {
|
|
|
11925
12638
|
operationId: "startAgentServerRun",
|
|
11926
12639
|
tags: ["runs"],
|
|
11927
12640
|
security: localControlSecurity,
|
|
12641
|
+
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
12642
|
body: StartRunSchema,
|
|
11929
12643
|
response: {
|
|
11930
12644
|
201: schemaRef(AgentServerRunSchema),
|
|
@@ -11959,8 +12673,8 @@ var AgentServerRouteSchemas = {
|
|
|
11959
12673
|
* loopback-companion security profile (#2066): loopback Host enforcement,
|
|
11960
12674
|
* exact-origin CORS, Fetch-Metadata guards, strict JSON parsing.
|
|
11961
12675
|
*
|
|
11962
|
-
* Control routes require
|
|
11963
|
-
*
|
|
12676
|
+
* Control routes require the process-scoped native Desktop grant. OAuth is
|
|
12677
|
+
* used only for native operator sign-in and credential provisioning.
|
|
11964
12678
|
*/
|
|
11965
12679
|
var AGENT_SERVER_TOKEN_HEADER = "x-moltnet-agent-server-token";
|
|
11966
12680
|
var BODY_LIMIT = 64 * 1024;
|
|
@@ -12019,14 +12733,6 @@ async function readAgentServerLogDelta(handle, state, limit = LOG_READ_LIMIT_BYT
|
|
|
12019
12733
|
omitted
|
|
12020
12734
|
};
|
|
12021
12735
|
}
|
|
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
12736
|
function requireBody(request) {
|
|
12031
12737
|
const body = request.body;
|
|
12032
12738
|
if (typeof body !== "object" || body === null || Array.isArray(body)) throw new AgentServerHttpError(400, "invalid_body", "JSON object body required");
|
|
@@ -12112,22 +12818,6 @@ function buildAgentServer(input) {
|
|
|
12112
12818
|
isOriginAllowed: (origin) => origin === "moltnet-agent-desktop://native" || origin === options.selfOrigin || browserOrigins.has(origin)
|
|
12113
12819
|
});
|
|
12114
12820
|
}
|
|
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
12821
|
function hasValidNativeGrant(origin, token) {
|
|
12132
12822
|
if (origin !== "moltnet-agent-desktop://native" || typeof token !== "string" || token.length === 0) return false;
|
|
12133
12823
|
try {
|
|
@@ -12158,56 +12848,20 @@ function buildAgentServer(input) {
|
|
|
12158
12848
|
max: options.rateLimitMax ?? RATE_LIMIT_MAX,
|
|
12159
12849
|
timeWindow: RATE_LIMIT_WINDOW_MS,
|
|
12160
12850
|
errorResponseBuilder: () => new AgentServerHttpError(429, "rate_limited", "Too many requests"),
|
|
12161
|
-
keyGenerator:
|
|
12851
|
+
keyGenerator: (request) => {
|
|
12162
12852
|
const origin = request.headers.origin;
|
|
12163
12853
|
if (!isConfiguredOrigin(origin, options)) return `ip:${request.ip}`;
|
|
12164
12854
|
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}`;
|
|
12855
|
+
return typeof presented === "string" && presented.length > 0 && hasValidNativeGrant(origin, presented) ? `origin:${origin}` : `unauth:${origin}:${request.ip}`;
|
|
12178
12856
|
}
|
|
12179
12857
|
});
|
|
12180
12858
|
const requireAuthorizedOrigin = async (request) => {
|
|
12181
12859
|
if (restartRequired) throw new AgentServerHttpError(409, "restart_required", "Restart the Agent Server to apply connection settings");
|
|
12182
12860
|
const origin = requireOriginHeader(request.headers);
|
|
12183
12861
|
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
|
-
}
|
|
12862
|
+
if (typeof token !== "string" || token.length === 0) throw new AgentServerHttpError(401, "authorization_required", "Native authorization is required");
|
|
12863
|
+
if (origin !== "moltnet-agent-desktop://native") throw new AgentServerHttpError(403, "native_required", "Native Desktop authorization required");
|
|
12864
|
+
requireNativeGrant(request);
|
|
12211
12865
|
return origin;
|
|
12212
12866
|
};
|
|
12213
12867
|
app.after(() => {
|
|
@@ -12216,13 +12870,12 @@ function buildAgentServer(input) {
|
|
|
12216
12870
|
});
|
|
12217
12871
|
app.get("/health", { schema: AgentServerRouteSchemas.health }, async () => ({ status: "ok" }));
|
|
12218
12872
|
app.get("/v1/native/connection-settings", { schema: { hide: true } }, async (request) => {
|
|
12219
|
-
|
|
12220
|
-
return options.connectionSettings.view();
|
|
12873
|
+
return (await requireNativeOrigin(requireAuthorizedOrigin, request, options.connectionSettings)).view();
|
|
12221
12874
|
});
|
|
12222
12875
|
app.post("/v1/native/connection-settings", { schema: { hide: true } }, async (request) => {
|
|
12223
|
-
|
|
12876
|
+
const store = await requireNativeOrigin(requireAuthorizedOrigin, request, options.connectionSettings);
|
|
12224
12877
|
try {
|
|
12225
|
-
const settings = options.runs.prepareServerRestart(() =>
|
|
12878
|
+
const settings = options.runs.prepareServerRestart(() => store.save(request.body));
|
|
12226
12879
|
oauth?.cancel();
|
|
12227
12880
|
restartRequired = true;
|
|
12228
12881
|
return settings;
|
|
@@ -12230,40 +12883,6 @@ function buildAgentServer(input) {
|
|
|
12230
12883
|
throw new AgentServerHttpError(400, "invalid_connection_settings", error instanceof Error ? error.message : "Invalid connection settings");
|
|
12231
12884
|
}
|
|
12232
12885
|
});
|
|
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
12886
|
app.post("/v1/operator/sign-in", { schema: {
|
|
12268
12887
|
operationId: "signInAgentServerOperator",
|
|
12269
12888
|
response: { 200: {
|
|
@@ -12272,10 +12891,37 @@ function buildAgentServer(input) {
|
|
|
12272
12891
|
required: ["state"]
|
|
12273
12892
|
} }
|
|
12274
12893
|
} }, async (request) => {
|
|
12275
|
-
|
|
12276
|
-
await oauth.authorize(void 0, requestOperationSignal(request, options.shutdownSignal));
|
|
12894
|
+
await (await requireNativeOrigin(requireAuthorizedOrigin, request, oauth)).authorize(void 0, requestOperationSignal(request, options.shutdownSignal));
|
|
12277
12895
|
return { state: "authorized" };
|
|
12278
12896
|
});
|
|
12897
|
+
app.get("/v1/native/operator", { schema: { hide: true } }, async (request) => {
|
|
12898
|
+
return { operatorConfigured: (await requireNativeOrigin(requireAuthorizedOrigin, request, oauth)).operatorConfigured() };
|
|
12899
|
+
});
|
|
12900
|
+
app.get("/v1/operator/teams", { schema: {
|
|
12901
|
+
operationId: "listAgentServerOperatorTeams",
|
|
12902
|
+
tags: ["operator"],
|
|
12903
|
+
security: [{ agentServerToken: [] }],
|
|
12904
|
+
response: { 200: {
|
|
12905
|
+
type: "object",
|
|
12906
|
+
required: ["items"],
|
|
12907
|
+
properties: { items: {
|
|
12908
|
+
type: "array",
|
|
12909
|
+
items: {
|
|
12910
|
+
type: "object",
|
|
12911
|
+
required: ["id", "name"],
|
|
12912
|
+
properties: {
|
|
12913
|
+
id: {
|
|
12914
|
+
type: "string",
|
|
12915
|
+
format: "uuid"
|
|
12916
|
+
},
|
|
12917
|
+
name: { type: "string" }
|
|
12918
|
+
}
|
|
12919
|
+
}
|
|
12920
|
+
} }
|
|
12921
|
+
} }
|
|
12922
|
+
} }, async (request) => {
|
|
12923
|
+
return { items: (await requireNativeOrigin(requireAuthorizedOrigin, request, oauth)).listTeams() };
|
|
12924
|
+
});
|
|
12279
12925
|
app.post("/v1/operator/cancel", { schema: {
|
|
12280
12926
|
operationId: "cancelAgentServerOperatorApproval",
|
|
12281
12927
|
tags: ["operator"],
|
|
@@ -12289,8 +12935,7 @@ function buildAgentServer(input) {
|
|
|
12289
12935
|
required: ["state"]
|
|
12290
12936
|
} }
|
|
12291
12937
|
} }, async (request) => {
|
|
12292
|
-
|
|
12293
|
-
oauth.cancel();
|
|
12938
|
+
(await requireNativeOrigin(requireAuthorizedOrigin, request, oauth)).cancel();
|
|
12294
12939
|
return { state: "cancelled" };
|
|
12295
12940
|
});
|
|
12296
12941
|
app.delete("/v1/operator", { schema: {
|
|
@@ -12306,8 +12951,7 @@ function buildAgentServer(input) {
|
|
|
12306
12951
|
required: ["state"]
|
|
12307
12952
|
} }
|
|
12308
12953
|
} }, async (request) => {
|
|
12309
|
-
|
|
12310
|
-
oauth.removeOperator();
|
|
12954
|
+
(await requireNativeOrigin(requireAuthorizedOrigin, request, oauth)).removeOperator();
|
|
12311
12955
|
return { state: "removed" };
|
|
12312
12956
|
});
|
|
12313
12957
|
registerStatusRoute(app, options, requireAuthorizedOrigin);
|
|
@@ -12316,6 +12960,7 @@ function buildAgentServer(input) {
|
|
|
12316
12960
|
registerSubscriptionRoutes(app, options, requireAuthorizedOrigin);
|
|
12317
12961
|
registerRunRoutes(app, options, requireAuthorizedOrigin);
|
|
12318
12962
|
registerCatalogueRoute(app, options, requireAuthorizedOrigin);
|
|
12963
|
+
registerProjectLocationRoutes(app, options, requireAuthorizedOrigin);
|
|
12319
12964
|
});
|
|
12320
12965
|
app.addHook("preClose", async () => {
|
|
12321
12966
|
options.operatorOAuth?.cancel();
|
|
@@ -12342,6 +12987,103 @@ function buildAgentServer(input) {
|
|
|
12342
12987
|
});
|
|
12343
12988
|
return app;
|
|
12344
12989
|
}
|
|
12990
|
+
async function requireNativeOrigin(authorize, request, ...resource) {
|
|
12991
|
+
if (await authorize(request) !== "moltnet-agent-desktop://native" || resource.length > 0 && resource[0] === void 0) throw new AgentServerHttpError(403, "native_required", "Native administration required");
|
|
12992
|
+
return resource[0];
|
|
12993
|
+
}
|
|
12994
|
+
function registerProjectLocationRoutes(app, options, authorize) {
|
|
12995
|
+
let locations;
|
|
12996
|
+
const getLocations = () => {
|
|
12997
|
+
const root = options.connectionSettings?.root;
|
|
12998
|
+
if (!root) throw new AgentServerHttpError(503, "locations_unavailable", "Project locations need the Desktop connection store");
|
|
12999
|
+
locations ??= new LocalProjectBindings(root, options.defaultApiUrl, [
|
|
13000
|
+
root,
|
|
13001
|
+
options.store.root,
|
|
13002
|
+
options.store.secretsDir
|
|
13003
|
+
]);
|
|
13004
|
+
return locations;
|
|
13005
|
+
};
|
|
13006
|
+
const requireNativeRequest = async (request) => {
|
|
13007
|
+
await requireNativeOrigin(authorize, request);
|
|
13008
|
+
if (request.validationError) throw new AgentServerHttpError(400, "invalid_location", "Check the project location fields");
|
|
13009
|
+
};
|
|
13010
|
+
app.get("/v1/native/project-locations", { schema: AgentServerRouteSchemas.listProjectLocations }, async (request) => {
|
|
13011
|
+
await requireNativeRequest(request);
|
|
13012
|
+
return { locations: await getLocations().list() };
|
|
13013
|
+
});
|
|
13014
|
+
const saveFields = new Set(Object.keys(AgentServerRouteSchemas.saveProjectLocation.body.properties));
|
|
13015
|
+
app.put("/v1/native/project-locations/:name", {
|
|
13016
|
+
schema: AgentServerRouteSchemas.saveProjectLocation,
|
|
13017
|
+
attachValidation: true,
|
|
13018
|
+
preValidation: async (request) => {
|
|
13019
|
+
await requireNativeOrigin(authorize, request);
|
|
13020
|
+
const body = request.body;
|
|
13021
|
+
if (body && typeof body === "object" && Object.keys(body).some((key) => !saveFields.has(key))) throw new AgentServerHttpError(400, "invalid_location", "Check the project location fields");
|
|
13022
|
+
}
|
|
13023
|
+
}, async (request) => {
|
|
13024
|
+
await requireNativeRequest(request);
|
|
13025
|
+
const bindings = getLocations();
|
|
13026
|
+
const { name } = request.params;
|
|
13027
|
+
const { identity, ...location } = requireBody(request);
|
|
13028
|
+
const alias = identity.trim();
|
|
13029
|
+
requireActivation(options.store, alias);
|
|
13030
|
+
const { config } = await loadAgentActivation(options.store, alias);
|
|
13031
|
+
if (locationEndpoint(config.endpoints.api) !== bindings.apiUrl) throw new AgentServerHttpError(400, "endpoint_mismatch", "Choose an identity for the current server endpoint");
|
|
13032
|
+
const signal = AbortSignal.any([requestOperationSignal(request, options.shutdownSignal), AbortSignal.timeout(options.projectSaveTimeoutMs ?? 1e4)]);
|
|
13033
|
+
await verifyLocationTarget(options, alias, location, request.log, signal);
|
|
13034
|
+
try {
|
|
13035
|
+
return await bindings.save({
|
|
13036
|
+
...location,
|
|
13037
|
+
name,
|
|
13038
|
+
apiUrl: bindings.apiUrl
|
|
13039
|
+
}, { signal });
|
|
13040
|
+
} catch (error) {
|
|
13041
|
+
if (signal.aborted) throw checkUnavailable(error, true);
|
|
13042
|
+
throw error;
|
|
13043
|
+
}
|
|
13044
|
+
});
|
|
13045
|
+
app.delete("/v1/native/project-locations/:name", {
|
|
13046
|
+
schema: AgentServerRouteSchemas.removeProjectLocation,
|
|
13047
|
+
attachValidation: true
|
|
13048
|
+
}, async (request) => {
|
|
13049
|
+
await requireNativeRequest(request);
|
|
13050
|
+
await getLocations().remove(request.params.name);
|
|
13051
|
+
return { removed: true };
|
|
13052
|
+
});
|
|
13053
|
+
}
|
|
13054
|
+
/**
|
|
13055
|
+
* A location save checks only its target team: `readTeam` verifies the team
|
|
13056
|
+
* credential and returns its diaries, then the shared check reads the project.
|
|
13057
|
+
*/
|
|
13058
|
+
async function verifyLocationTarget(options, alias, target, logger, signal) {
|
|
13059
|
+
const agent = await catalogueAgent(options, alias);
|
|
13060
|
+
if (!agent.teamIds.includes(target.teamId)) throw projectUnavailable();
|
|
13061
|
+
let diaries = [];
|
|
13062
|
+
await verifyProjectTarget({
|
|
13063
|
+
readProject: async (teamId, projectId, readSignal) => {
|
|
13064
|
+
const team = await agent.readTeam(teamId, readSignal);
|
|
13065
|
+
if (team.team.id !== teamId) throw new Error("Team response mismatch");
|
|
13066
|
+
diaries = team.diaries;
|
|
13067
|
+
return agent.readProject(teamId, projectId, readSignal);
|
|
13068
|
+
},
|
|
13069
|
+
readDiary: async (teamId, diaryId) => diaries.find((diary) => diary.id === diaryId && diary.teamId === teamId) ?? null
|
|
13070
|
+
}, target, {
|
|
13071
|
+
signal,
|
|
13072
|
+
logger
|
|
13073
|
+
});
|
|
13074
|
+
}
|
|
13075
|
+
async function catalogueAgent(options, alias) {
|
|
13076
|
+
return options.catalogueAgentFor ? options.catalogueAgentFor(alias) : defaultCatalogueAgent(options, alias);
|
|
13077
|
+
}
|
|
13078
|
+
async function readIdentityCatalogue(options, alias, logger) {
|
|
13079
|
+
requireActivation(options.store, alias);
|
|
13080
|
+
return buildCatalogue({
|
|
13081
|
+
agent: await catalogueAgent(options, alias),
|
|
13082
|
+
machine: machineCapabilities(options),
|
|
13083
|
+
identityDefault: readIdentityDefaultBinding(options.store.identityDir(alias)),
|
|
13084
|
+
logger
|
|
13085
|
+
});
|
|
13086
|
+
}
|
|
12345
13087
|
function registerCatalogueRoute(app, options, requireAuthorizedOrigin) {
|
|
12346
13088
|
app.get("/v1/catalogue", {
|
|
12347
13089
|
schema: AgentServerRouteSchemas.catalogue,
|
|
@@ -12350,34 +13092,42 @@ function registerCatalogueRoute(app, options, requireAuthorizedOrigin) {
|
|
|
12350
13092
|
await requireAuthorizedOrigin(request);
|
|
12351
13093
|
const { identity } = request.query ?? {};
|
|
12352
13094
|
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
|
-
});
|
|
13095
|
+
return readIdentityCatalogue(options, identity.trim(), request.log);
|
|
12360
13096
|
});
|
|
12361
13097
|
}
|
|
12362
13098
|
/** Resolve and verify each indexed team independently with its exact key. */
|
|
12363
13099
|
async function defaultCatalogueAgent(options, alias) {
|
|
12364
13100
|
const { config } = await loadAgentActivation(options.store, alias);
|
|
13101
|
+
const clients = /* @__PURE__ */ new Map();
|
|
13102
|
+
const verifiedClient = (teamId) => {
|
|
13103
|
+
const client = clients.get(teamId);
|
|
13104
|
+
if (!client) throw new Error("Team must be verified before project discovery");
|
|
13105
|
+
return client;
|
|
13106
|
+
};
|
|
12365
13107
|
return {
|
|
12366
13108
|
teamIds: Object.keys(config.agent_key_refs ?? {}),
|
|
12367
13109
|
lastVerified: (teamId) => requireActivation(options.store, alias).credentialHealth?.[teamId],
|
|
12368
|
-
readTeam: async (teamId) => {
|
|
12369
|
-
const
|
|
13110
|
+
readTeam: async (teamId, signal) => {
|
|
13111
|
+
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);
|
|
13112
|
+
signal?.throwIfAborted();
|
|
13113
|
+
const { client, metadata } = requireCredentialSnapshot(activated);
|
|
12370
13114
|
const [team, diaries, profiles] = await Promise.all([
|
|
12371
13115
|
client.teams.get(teamId),
|
|
12372
13116
|
client.diaries.list(),
|
|
12373
13117
|
client.runtimeProfiles.list({ teamId })
|
|
12374
13118
|
]);
|
|
13119
|
+
clients.set(teamId, client);
|
|
12375
13120
|
return {
|
|
12376
13121
|
team,
|
|
12377
13122
|
diaries: diaries.items,
|
|
12378
13123
|
profiles: profiles.items,
|
|
12379
13124
|
credential: metadata
|
|
12380
13125
|
};
|
|
13126
|
+
},
|
|
13127
|
+
readProjects: async (teamId) => readCatalogueProjects(verifiedClient(teamId).projects, teamId),
|
|
13128
|
+
readProject: async (teamId, projectId, signal) => {
|
|
13129
|
+
signal?.throwIfAborted();
|
|
13130
|
+
return readCatalogueProject(verifiedClient(teamId).projects, teamId, projectId);
|
|
12381
13131
|
}
|
|
12382
13132
|
};
|
|
12383
13133
|
}
|
|
@@ -12400,7 +13150,7 @@ function machineCapabilities(options) {
|
|
|
12400
13150
|
function registerStatusRoute(app, options, requireAuthorizedOrigin) {
|
|
12401
13151
|
const { store, runs } = options;
|
|
12402
13152
|
app.get("/v1/status", { schema: AgentServerRouteSchemas.status }, async (request) => {
|
|
12403
|
-
await requireAuthorizedOrigin(request);
|
|
13153
|
+
const origin = await requireAuthorizedOrigin(request);
|
|
12404
13154
|
const selected = selectedIdentity(store, options.activeIdentity);
|
|
12405
13155
|
return {
|
|
12406
13156
|
version: options.version,
|
|
@@ -12410,7 +13160,7 @@ function registerStatusRoute(app, options, requireAuthorizedOrigin) {
|
|
|
12410
13160
|
identities: identityViews(store),
|
|
12411
13161
|
...selected ? { selectedIdentity: selected } : {},
|
|
12412
13162
|
providers: options.providers.list(),
|
|
12413
|
-
runs: await runViews(runs),
|
|
13163
|
+
runs: await runViews(runs, origin),
|
|
12414
13164
|
runtimeSettings: options.runtimeSettings ?? DEFAULT_LOCAL_OPERATIONAL_SETTINGS
|
|
12415
13165
|
};
|
|
12416
13166
|
});
|
|
@@ -12535,11 +13285,30 @@ function registerProviderRoutes(app, options, requireAuthorizedOrigin) {
|
|
|
12535
13285
|
return reply.code(204).send(null);
|
|
12536
13286
|
});
|
|
12537
13287
|
}
|
|
12538
|
-
async function runViews(runs) {
|
|
12539
|
-
return (await runs.listAsync(RUN_HISTORY_LIMIT)).map((record) => (
|
|
12540
|
-
|
|
12541
|
-
|
|
12542
|
-
|
|
13288
|
+
async function runViews(runs, origin) {
|
|
13289
|
+
return (await runs.listAsync(RUN_HISTORY_LIMIT)).map((record) => runView(record, runs.isActive(record.id), origin));
|
|
13290
|
+
}
|
|
13291
|
+
/**
|
|
13292
|
+
* Local folders are native-only, as project locations are: other origins see
|
|
13293
|
+
* the location name and project, never the path. The snapshot path is
|
|
13294
|
+
* daemon-internal for every origin.
|
|
13295
|
+
*/
|
|
13296
|
+
function runView(record, active, origin) {
|
|
13297
|
+
const native = origin === NATIVE_CLIENT_ORIGIN;
|
|
13298
|
+
const { source, workspace, ...rest } = record;
|
|
13299
|
+
const view = {
|
|
13300
|
+
...rest,
|
|
13301
|
+
...native && source !== void 0 ? { source } : {},
|
|
13302
|
+
active
|
|
13303
|
+
};
|
|
13304
|
+
if (workspace) {
|
|
13305
|
+
const { configPath: _configPath, source: resolvedSource, ...shared } = workspace;
|
|
13306
|
+
view.workspace = {
|
|
13307
|
+
...shared,
|
|
13308
|
+
...native && resolvedSource !== void 0 ? { source: resolvedSource } : {}
|
|
13309
|
+
};
|
|
13310
|
+
}
|
|
13311
|
+
return view;
|
|
12543
13312
|
}
|
|
12544
13313
|
function registerSubscriptionRoutes(app, options, requireAuthorizedOrigin) {
|
|
12545
13314
|
app.get("/v1/subscriptions", { schema: AgentServerRouteSchemas.listSubscriptions }, async (request) => {
|
|
@@ -12566,17 +13335,22 @@ function registerSubscriptionRoutes(app, options, requireAuthorizedOrigin) {
|
|
|
12566
13335
|
function registerRunRoutes(app, options, requireAuthorizedOrigin) {
|
|
12567
13336
|
const { runs } = options;
|
|
12568
13337
|
app.get("/v1/runs", { schema: AgentServerRouteSchemas.listRuns }, async (request) => {
|
|
12569
|
-
await requireAuthorizedOrigin(request);
|
|
12570
|
-
return runViews(runs);
|
|
13338
|
+
return runViews(runs, await requireAuthorizedOrigin(request));
|
|
12571
13339
|
});
|
|
12572
13340
|
app.post("/v1/runs", {
|
|
12573
13341
|
schema: AgentServerRouteSchemas.startRun,
|
|
12574
13342
|
attachValidation: true
|
|
12575
13343
|
}, async (request, reply) => {
|
|
12576
|
-
await requireAuthorizedOrigin(request);
|
|
13344
|
+
const origin = await requireAuthorizedOrigin(request);
|
|
12577
13345
|
const body = requireBody(request);
|
|
13346
|
+
if (NATIVE_RUN_FIELDS.some((key) => body[key] !== void 0 && body[key] !== null)) await requireNativeOrigin(async () => origin, request);
|
|
13347
|
+
if (request.validationError) throw new AgentServerHttpError(400, "invalid_spec", `Check the run fields: ${request.validationError.message}`);
|
|
12578
13348
|
const diaryId = optionalString(body, "diaryId");
|
|
12579
13349
|
const record = await runs.start({
|
|
13350
|
+
...body.projectId === null ? { projectId: null } : body.projectId !== void 0 ? { projectId: requireString(body, "projectId") } : {},
|
|
13351
|
+
...body.location !== void 0 ? { location: requireString(body, "location") } : {},
|
|
13352
|
+
...body.source !== void 0 ? { source: requireString(body, "source") } : {},
|
|
13353
|
+
...body.strategy !== void 0 ? { strategy: requireString(body, "strategy") } : {},
|
|
12580
13354
|
agent: requireString(body, "agent"),
|
|
12581
13355
|
teamId: requireString(body, "teamId"),
|
|
12582
13356
|
...diaryId ? { diaryId } : {},
|
|
@@ -12584,15 +13358,13 @@ function registerRunRoutes(app, options, requireAuthorizedOrigin) {
|
|
|
12584
13358
|
taskTypes: stringArray(body, "taskTypes"),
|
|
12585
13359
|
mode: requireString(body, "mode")
|
|
12586
13360
|
}, requestOperationSignal(request, options.shutdownSignal));
|
|
12587
|
-
return reply.code(201).send(
|
|
12588
|
-
...record,
|
|
12589
|
-
active: runs.isActive(record.id)
|
|
12590
|
-
});
|
|
13361
|
+
return reply.code(201).send(runView(record, runs.isActive(record.id), origin));
|
|
12591
13362
|
});
|
|
12592
13363
|
app.delete("/v1/runs/:runId", { schema: AgentServerRouteSchemas.stopRun }, async (request) => {
|
|
12593
|
-
await requireAuthorizedOrigin(request);
|
|
13364
|
+
const origin = await requireAuthorizedOrigin(request);
|
|
12594
13365
|
const { runId } = request.params;
|
|
12595
|
-
|
|
13366
|
+
const record = runs.stop(runId);
|
|
13367
|
+
return runView(record, runs.isActive(record.id), origin);
|
|
12596
13368
|
});
|
|
12597
13369
|
registerRunLogRoute(app, options, requireAuthorizedOrigin);
|
|
12598
13370
|
}
|
|
@@ -12619,9 +13391,10 @@ function registerRunLogRoute(app, options, requireAuthorizedOrigin) {
|
|
|
12619
13391
|
} }
|
|
12620
13392
|
} }
|
|
12621
13393
|
} }, async (request) => {
|
|
12622
|
-
await requireAuthorizedOrigin(request);
|
|
13394
|
+
const origin = await requireAuthorizedOrigin(request);
|
|
12623
13395
|
const { runId } = request.params;
|
|
12624
13396
|
const record = runs.status(runId);
|
|
13397
|
+
const redact = localPathRedactor(record, origin, options);
|
|
12625
13398
|
const handle = await open(store.resolveRunLogPath(record.id), constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
12626
13399
|
try {
|
|
12627
13400
|
const state = {
|
|
@@ -12629,20 +13402,22 @@ function registerRunLogRoute(app, options, requireAuthorizedOrigin) {
|
|
|
12629
13402
|
fragment: ""
|
|
12630
13403
|
};
|
|
12631
13404
|
const { lines, omitted } = await readAgentServerLogDelta(handle, state);
|
|
13405
|
+
const native = origin === NATIVE_CLIENT_ORIGIN;
|
|
12632
13406
|
return { lines: [
|
|
12633
13407
|
...omitted ? ["[older log output omitted]"] : [],
|
|
12634
|
-
...lines,
|
|
12635
|
-
...state.fragment ? [state.fragment] : []
|
|
12636
|
-
] };
|
|
13408
|
+
...omitted && !native ? lines.slice(1) : lines,
|
|
13409
|
+
...state.fragment && (native || !runs.isActive(record.id)) ? [state.fragment] : []
|
|
13410
|
+
].map(redact) };
|
|
12637
13411
|
} finally {
|
|
12638
13412
|
await handle.close();
|
|
12639
13413
|
}
|
|
12640
13414
|
});
|
|
12641
13415
|
let openStreams = 0;
|
|
12642
13416
|
app.get("/v1/runs/:runId/logs", { schema: AgentServerRouteSchemas.streamRunLogs }, async (request, reply) => {
|
|
12643
|
-
await requireAuthorizedOrigin(request);
|
|
13417
|
+
const origin = await requireAuthorizedOrigin(request);
|
|
12644
13418
|
const { runId } = request.params;
|
|
12645
13419
|
const record = runs.status(runId);
|
|
13420
|
+
const redact = localPathRedactor(record, origin, options);
|
|
12646
13421
|
store.resolveRunLogPath(record.id);
|
|
12647
13422
|
if (openStreams >= MAX_LOG_STREAMS) throw new AgentServerHttpError(429, "rate_limited", "Too many concurrent log streams");
|
|
12648
13423
|
openStreams += 1;
|
|
@@ -12683,12 +13458,12 @@ function registerRunLogRoute(app, options, requireAuthorizedOrigin) {
|
|
|
12683
13458
|
});
|
|
12684
13459
|
};
|
|
12685
13460
|
const push = async () => {
|
|
12686
|
-
if (request.headers.origin !== "moltnet-agent-desktop://native") await requireAuthorizedOrigin(request);
|
|
12687
13461
|
const handle = await open(store.resolveRunLogPath(record.id), constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
12688
13462
|
try {
|
|
12689
13463
|
const { lines, omitted } = await readAgentServerLogDelta(handle, readState);
|
|
12690
13464
|
if (omitted) await writeData("[older log output omitted]");
|
|
12691
|
-
|
|
13465
|
+
const complete = omitted && origin !== "moltnet-agent-desktop://native" ? lines.slice(1) : lines;
|
|
13466
|
+
for (const line of complete) await writeData(redact(line));
|
|
12692
13467
|
} finally {
|
|
12693
13468
|
await handle.close();
|
|
12694
13469
|
}
|
|
@@ -12723,6 +13498,43 @@ function registerRunLogRoute(app, options, requireAuthorizedOrigin) {
|
|
|
12723
13498
|
return reply;
|
|
12724
13499
|
});
|
|
12725
13500
|
}
|
|
13501
|
+
/**
|
|
13502
|
+
* Worker logs name local folders (the chosen source, state and HOME under the
|
|
13503
|
+
* store). Non-native origins get them replaced, as `runView` does for records.
|
|
13504
|
+
* The user's home directory is included so any other path under it cannot
|
|
13505
|
+
* reveal the OS account name.
|
|
13506
|
+
*/
|
|
13507
|
+
/**
|
|
13508
|
+
* A service account's home can be `/` or `/root`; replacing it would rewrite
|
|
13509
|
+
* every slash in the log, so only a home with two or more segments counts.
|
|
13510
|
+
*/
|
|
13511
|
+
function redactableHome() {
|
|
13512
|
+
const home = homedir();
|
|
13513
|
+
return home.split(/[\\/]/u).filter(Boolean).length >= 2 ? home : void 0;
|
|
13514
|
+
}
|
|
13515
|
+
function localPathRedactor(record, origin, options) {
|
|
13516
|
+
if (origin === "moltnet-agent-desktop://native") return (line) => line;
|
|
13517
|
+
const paths = /* @__PURE__ */ new Set();
|
|
13518
|
+
for (const path of [
|
|
13519
|
+
record.source,
|
|
13520
|
+
record.workspace?.source,
|
|
13521
|
+
options.store.root,
|
|
13522
|
+
options.connectionSettings?.root,
|
|
13523
|
+
redactableHome()
|
|
13524
|
+
]) {
|
|
13525
|
+
if (!path) continue;
|
|
13526
|
+
const forms = [path];
|
|
13527
|
+
try {
|
|
13528
|
+
forms.push(realpathSync.native(path));
|
|
13529
|
+
} catch {}
|
|
13530
|
+
for (const form of forms) {
|
|
13531
|
+
paths.add(form);
|
|
13532
|
+
paths.add(JSON.stringify(form).slice(1, -1));
|
|
13533
|
+
}
|
|
13534
|
+
}
|
|
13535
|
+
const ordered = [...paths].sort((a, b) => b.length - a.length);
|
|
13536
|
+
return (line) => ordered.reduce((text, path) => text.split(path).join("<local path>"), line);
|
|
13537
|
+
}
|
|
12726
13538
|
function corsHeadersFor(request, options) {
|
|
12727
13539
|
const origin = request.headers.origin;
|
|
12728
13540
|
if (isConfiguredOrigin(origin, options)) return {
|
|
@@ -12735,6 +13547,23 @@ function isConfiguredOrigin(origin, options) {
|
|
|
12735
13547
|
return typeof origin === "string" && (origin === "moltnet-agent-desktop://native" || !options.nativeOnly && (options.allowedOrigins.includes(origin) || origin === options.selfOrigin));
|
|
12736
13548
|
}
|
|
12737
13549
|
function normalizeAgentServerError(error) {
|
|
13550
|
+
if (error instanceof ProjectConfigError) {
|
|
13551
|
+
if (error.kind === "version") return {
|
|
13552
|
+
statusCode: 409,
|
|
13553
|
+
code: "config_version",
|
|
13554
|
+
message: "The project locations file was written by a newer MoltNet. Update this app."
|
|
13555
|
+
};
|
|
13556
|
+
if (error.kind === "io") return {
|
|
13557
|
+
statusCode: 500,
|
|
13558
|
+
code: "config_unavailable",
|
|
13559
|
+
message: "The project locations file could not be read or written. Check its ownership and permissions."
|
|
13560
|
+
};
|
|
13561
|
+
return {
|
|
13562
|
+
statusCode: 400,
|
|
13563
|
+
code: "invalid_location",
|
|
13564
|
+
message: error.message
|
|
13565
|
+
};
|
|
13566
|
+
}
|
|
12738
13567
|
if (error instanceof TeamCredentialError) return {
|
|
12739
13568
|
statusCode: 400,
|
|
12740
13569
|
code: error.blocker.code,
|
|
@@ -12819,7 +13648,7 @@ function nativeSocketValidationOptions(input) {
|
|
|
12819
13648
|
...input.cliAllowedOrigins ? { allowedOrigins: input.cliAllowedOrigins } : {}
|
|
12820
13649
|
};
|
|
12821
13650
|
}
|
|
12822
|
-
async function runAgentServer(argv) {
|
|
13651
|
+
async function runAgentServer(argv, ports = {}) {
|
|
12823
13652
|
if (isHelpFlag(argv)) {
|
|
12824
13653
|
console.log(AGENT_SERVER_HELP);
|
|
12825
13654
|
return 0;
|
|
@@ -12897,9 +13726,12 @@ async function runAgentServer(argv) {
|
|
|
12897
13726
|
logger
|
|
12898
13727
|
});
|
|
12899
13728
|
const runtimeRegistry = new RuntimeRegistry(store.root);
|
|
13729
|
+
const supplied = ports.configure?.(store);
|
|
12900
13730
|
const runs = new RunManager({
|
|
13731
|
+
...supplied?.runOptions,
|
|
12901
13732
|
store,
|
|
12902
13733
|
storeRoot: settingsRoot,
|
|
13734
|
+
projectRoot: settingsRoot,
|
|
12903
13735
|
secretProviders,
|
|
12904
13736
|
externalSecretProviders,
|
|
12905
13737
|
baseEnv: processEnvSnapshot(),
|
|
@@ -12909,16 +13741,17 @@ async function runAgentServer(argv) {
|
|
|
12909
13741
|
});
|
|
12910
13742
|
if (nativeSocket) await validateNativeSocket(nativeSocket);
|
|
12911
13743
|
const selfOrigin = nativeSocket ? void 0 : `http://127.0.0.1:${port}`;
|
|
13744
|
+
const operatorOAuth = new OperatorOAuth({
|
|
13745
|
+
issuer: connection.issuer,
|
|
13746
|
+
authorizationUrl: new URL("/oauth2/auth", connection.publicUrl).href,
|
|
13747
|
+
tokenUrl: new URL("/oauth2/token", connection.publicUrl).href,
|
|
13748
|
+
jwksUrl: new URL("/.well-known/jwks.json", connection.publicUrl).href,
|
|
13749
|
+
nativeClientId: connection.nativeClientId,
|
|
13750
|
+
callbackPort: OPERATOR_OAUTH.callbackPort
|
|
13751
|
+
}, root);
|
|
12912
13752
|
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),
|
|
13753
|
+
...supplied?.catalogueAgentFor ? { catalogueAgentFor: supplied.catalogueAgentFor } : {},
|
|
13754
|
+
operatorOAuth,
|
|
12922
13755
|
nativeOnly: Boolean(nativeSocket),
|
|
12923
13756
|
connectionSettings,
|
|
12924
13757
|
operatorApiUrl: connection.apiUrl,
|
|
@@ -13403,7 +14236,7 @@ async function writeCache(cache) {
|
|
|
13403
14236
|
}
|
|
13404
14237
|
//#endregion
|
|
13405
14238
|
//#region src/version.ts
|
|
13406
|
-
var DAEMON_VERSION = "0.
|
|
14239
|
+
var DAEMON_VERSION = "0.65.0";
|
|
13407
14240
|
//#endregion
|
|
13408
14241
|
//#region src/cli.ts
|
|
13409
14242
|
async function runAgentDaemonCli(options) {
|