@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.
Files changed (3) hide show
  1. package/README.md +6 -2
  2. package/dist/cli.js +1177 -344
  3. 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
- Type.Object({
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
- async function validateGitSource(source) {
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
- /** Resolve once at worker startup. No credentials, remote calls, hooks or workspace creation. */
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
- if (workspaceStrategy === "isolated-directory" || binding?.hooks?.afterCreate || binding?.hooks?.beforeRun) throw new ProjectConfigError("selection", `Binding ${binding?.name ?? "(run override)"} in ${configPath}: this runtime does not support isolated-directory preparation or setup hooks; choose a supported binding`);
3942
- if (workspaceStrategy === "git-worktree" && source) await validateGitSource(source);
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
- if (selection.strategy === "isolated-directory") throw new Error("This runtime does not yet support isolated-directory preparation");
3960
- if (selection.binding?.hooks?.afterCreate || selection.binding?.hooks?.beforeRun) throw new Error("This runtime does not yet support project setup hooks");
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 Console approval"));
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: clientId === this.config.nativeClientId ? OPERATOR_OAUTH.nativeLifetimeSeconds : OPERATOR_OAUTH.consoleLifetimeSeconds
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 Console approval")));
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 Console approval"));
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 Console approval with the required desktop scopes."
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 agent = await (this.options.verifyActivationImpl ?? verifyTeamActivation)(this.store, spec.agent, this.options.secretProviders, this.options.externalSecretProviders, void 0, signal, spec.teamId).catch((cause) => {
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 profiles = await resolveRuntimeProfiles({
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(profiles.map((profile) => profile.runtimeKind))];
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
- assertStartOpen(signal) {
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/identity-binding.ts
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
- * The identity-wide team/diary binding, read from `<identityDir>/env`.
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 readIdentityDefaultBinding(identityDir) {
11519
- let contents;
12128
+ async function stored(work) {
11520
12129
  try {
11521
- contents = readFileSync(join(identityDir, "env"), "utf8");
11522
- } catch {
11523
- return {};
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
- const env = parseEnv(contents);
11526
- const teamId = env["MOLTNET_TEAM_ID"]?.trim();
11527
- const diaryId = env["MOLTNET_DIARY_ID"]?.trim();
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 a native process grant or an OAuth token bound to
11963
- * the native operator and this server instance. Origin checks apply to both.
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: async (request) => {
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
- let authenticated = false;
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", "Local control token is required");
12185
- if (origin === "moltnet-agent-desktop://native") requireNativeGrant(request);
12186
- else try {
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
- if (await requireAuthorizedOrigin(request) !== "moltnet-agent-desktop://native" || !options.connectionSettings) throw new AgentServerHttpError(403, "native_required", "Native administration required");
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
- if (await requireAuthorizedOrigin(request) !== "moltnet-agent-desktop://native" || !options.connectionSettings) throw new AgentServerHttpError(403, "native_required", "Native administration required");
12876
+ const store = await requireNativeOrigin(requireAuthorizedOrigin, request, options.connectionSettings);
12224
12877
  try {
12225
- const settings = options.runs.prepareServerRestart(() => options.connectionSettings.save(request.body));
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
- if (await requireAuthorizedOrigin(request) !== "moltnet-agent-desktop://native" || !oauth) throw new AgentServerHttpError(403, "native_required", "Native administration required");
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
- if (await requireAuthorizedOrigin(request) !== "moltnet-agent-desktop://native" || !oauth) throw new AgentServerHttpError(403, "native_required", "Native administration required");
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
- if (await requireAuthorizedOrigin(request) !== "moltnet-agent-desktop://native" || !oauth) throw new AgentServerHttpError(403, "native_required", "Native administration required");
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
- const alias = identity.trim();
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 { client, metadata } = requireCredentialSnapshot(await verifyTeamActivation(options.store, alias, options.secretProviders, options.externalSecretProviders, void 0, options.shutdownSignal, teamId));
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
- ...record,
12541
- active: runs.isActive(record.id)
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
- return runs.stop(runId);
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
- for (const line of lines) await writeData(line);
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
- operatorOAuth: new OperatorOAuth({
12914
- issuer: connection.issuer,
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.63.1";
14239
+ var DAEMON_VERSION = "0.65.0";
13407
14240
  //#endregion
13408
14241
  //#region src/cli.ts
13409
14242
  async function runAgentDaemonCli(options) {