@themoltnet/agent-daemon 0.63.1 → 0.64.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +6 -2
  2. package/dist/cli.js +1129 -345
  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) {
@@ -9564,7 +9591,7 @@ var OperatorOAuth = class {
9564
9591
  const command = process.platform === "darwin" ? "open" : process.platform === "win32" ? "explorer.exe" : "xdg-open";
9565
9592
  return new Promise((resolve, reject) => {
9566
9593
  execFile(command, [url], (error) => {
9567
- if (error) reject(/* @__PURE__ */ new Error("Could not open Console approval"));
9594
+ if (error) reject(/* @__PURE__ */ new Error("Could not open browser approval"));
9568
9595
  else resolve();
9569
9596
  });
9570
9597
  });
@@ -9594,22 +9621,14 @@ var OperatorOAuth = class {
9594
9621
  cancel() {
9595
9622
  this.pending?.abort();
9596
9623
  }
9624
+ operatorConfigured() {
9625
+ return this.operator !== null;
9626
+ }
9597
9627
  removeOperator() {
9598
9628
  this.cancel();
9599
9629
  rmSync(join(this.root, "operator.json"), { force: true });
9600
9630
  this.operator = null;
9601
9631
  }
9602
- metadata() {
9603
- return {
9604
- protocolVersion: OPERATOR_OAUTH.protocolVersion,
9605
- instance: this.instance,
9606
- issuer: this.config.issuer,
9607
- authorizationUrl: this.config.authorizationUrl,
9608
- tokenUrl: this.config.tokenUrl,
9609
- clientId: this.config.consoleClientId,
9610
- operatorConfigured: !!this.operator
9611
- };
9612
- }
9613
9632
  async verify(token, scope, clientId) {
9614
9633
  const { payload } = await jwtVerify(token, this.keys, {
9615
9634
  algorithms: ["RS256"],
@@ -9620,7 +9639,7 @@ var OperatorOAuth = class {
9620
9639
  "iat",
9621
9640
  "sub"
9622
9641
  ],
9623
- maxTokenAge: clientId === this.config.nativeClientId ? OPERATOR_OAUTH.nativeLifetimeSeconds : OPERATOR_OAUTH.consoleLifetimeSeconds
9642
+ maxTokenAge: OPERATOR_OAUTH.nativeLifetimeSeconds
9624
9643
  });
9625
9644
  const claims = payload.ext;
9626
9645
  const scopes = typeof payload.scope === "string" ? payload.scope.split(" ") : payload.scp;
@@ -9631,10 +9650,6 @@ var OperatorOAuth = class {
9631
9650
  provisioning: claims["moltnet:provisioning"]
9632
9651
  };
9633
9652
  }
9634
- async verifyBrowser(token) {
9635
- const operator = await this.verify(token, LOCAL_SCOPE, this.config.consoleClientId);
9636
- if (!this.operator || operator.issuer !== this.operator.issuer || operator.subject !== this.operator.subject) throw new InvalidOperatorGrantError("Native operator sign-in required");
9637
- }
9638
9653
  async authorize(grant, signal) {
9639
9654
  if (this.active) throw new Error("An approval is already pending");
9640
9655
  this.active = true;
@@ -9688,9 +9703,9 @@ var OperatorOAuth = class {
9688
9703
  ...grant ? { provisioning: JSON.stringify(grant) } : {}
9689
9704
  })) url.searchParams.set(key, value);
9690
9705
  try {
9691
- Promise.resolve(this.openBrowser(url.href)).catch(() => reject(/* @__PURE__ */ new Error("Could not open Console approval")));
9706
+ Promise.resolve(this.openBrowser(url.href)).catch(() => reject(/* @__PURE__ */ new Error("Could not open browser approval")));
9692
9707
  } catch {
9693
- reject(/* @__PURE__ */ new Error("Could not open Console approval"));
9708
+ reject(/* @__PURE__ */ new Error("Could not open browser approval"));
9694
9709
  }
9695
9710
  });
9696
9711
  });
@@ -10101,6 +10116,94 @@ function snapshot(login) {
10101
10116
  };
10102
10117
  }
10103
10118
  //#endregion
10119
+ //#region src/lib/agent-server/catalogue-project-reader.ts
10120
+ var PAGE_SIZE = 100;
10121
+ var ProjectPaginationError = class extends Error {
10122
+ name = "ProjectPaginationError";
10123
+ };
10124
+ async function readCatalogueProjects(projects, teamId) {
10125
+ const items = [];
10126
+ let offset = 0;
10127
+ for (let page = 0; page < 10; page++) {
10128
+ const result = await projects.list({
10129
+ includeArchived: false,
10130
+ limit: PAGE_SIZE,
10131
+ offset
10132
+ }, { teamId });
10133
+ items.push(...result.items);
10134
+ if (result.nextOffset === null) return {
10135
+ items,
10136
+ truncated: false
10137
+ };
10138
+ if (!Number.isSafeInteger(result.nextOffset) || result.nextOffset <= offset) throw new ProjectPaginationError("Invalid project pagination offset");
10139
+ offset = result.nextOffset;
10140
+ }
10141
+ return {
10142
+ items,
10143
+ truncated: true
10144
+ };
10145
+ }
10146
+ /** One project by id; null when the credential cannot see it. */
10147
+ async function readCatalogueProject(projects, teamId, projectId) {
10148
+ try {
10149
+ return await projects.get(projectId, { teamId });
10150
+ } catch (error) {
10151
+ if (error instanceof MoltNetError && (error.statusCode === 403 || error.statusCode === 404)) return null;
10152
+ throw error;
10153
+ }
10154
+ }
10155
+ //#endregion
10156
+ //#region src/lib/agent-server/readiness.ts
10157
+ /**
10158
+ * Whether a runtime profile can execute on *this* machine.
10159
+ *
10160
+ * A profile is authored in Console against a team; whether it can run depends
10161
+ * on local facts only the server knows — which provider keys are configured
10162
+ * here and which runtime kinds this machine can produce.
10163
+ *
10164
+ * The prerequisite comparison itself is **not** reimplemented here. Run start
10165
+ * calls `validateRuntimeProfilePrerequisites`, and the catalogue calls the same
10166
+ * function, so the composer cannot promise a run that startup would reject for
10167
+ * a reason the two evaluated differently.
10168
+ */
10169
+ function deriveProfileReadiness(profile, machine) {
10170
+ const blockers = [];
10171
+ const env = {};
10172
+ for (const [name, configured] of machine.providerEnv) if (configured) env[name] = "configured";
10173
+ try {
10174
+ validateRuntimeProfilePrerequisites(profile, env, {
10175
+ tools: machine.inventory?.tools ?? profile.requiredTools,
10176
+ executables: machine.inventory?.executables ?? profile.requiredExecutables
10177
+ });
10178
+ } catch (error) {
10179
+ if (!(error instanceof RuntimeProfilePrerequisiteError)) throw error;
10180
+ for (const name of error.missingEnv) blockers.push({
10181
+ code: "env_missing",
10182
+ message: `${name} is not configured on this machine.`,
10183
+ remedy: "Add the key under Providers, then reopen this run."
10184
+ });
10185
+ for (const name of error.missingTools) blockers.push({
10186
+ code: "tool_missing",
10187
+ message: `The runtime does not provide the tool ${name}.`,
10188
+ remedy: `Use a profile whose runtime provides ${name}, or change the profile in Console.`
10189
+ });
10190
+ for (const name of error.missingExecutables) blockers.push({
10191
+ code: "executable_missing",
10192
+ message: `The runtime does not provide the executable ${name}.`,
10193
+ remedy: `Use a runtime that ships ${name}, or drop the requirement in Console.`
10194
+ });
10195
+ }
10196
+ if (!machine.runtimeKinds.has(profile.runtimeKind)) blockers.push({
10197
+ code: "runtime_unregistered",
10198
+ message: `Runtime kind ${profile.runtimeKind} is not available on this machine.`,
10199
+ remedy: "Register the runtime under Runtimes, then reopen this run."
10200
+ });
10201
+ return {
10202
+ ready: blockers.length === 0,
10203
+ blockers
10204
+ };
10205
+ }
10206
+ //#endregion
10104
10207
  //#region src/lib/agent-server/identity.ts
10105
10208
  /**
10106
10209
  * AgentServer identity activation (#2061/#1834 boundary).
@@ -10544,7 +10647,7 @@ async function verifyTeamActivation(store, alias, managed, external, connectImpl
10544
10647
  if (missing.length) throw new TeamCredentialError({
10545
10648
  code: "agent_key_scopes_insufficient",
10546
10649
  message: `This credential lacks ${missing.join(", ")}.`,
10547
- remedy: "Renew through Console approval with the required desktop scopes."
10650
+ remedy: "Renew through browser approval with the required desktop scopes."
10548
10651
  });
10549
10652
  activated.boundTeamId = teamId;
10550
10653
  return captureTeamCredential(activated, {
@@ -10554,6 +10657,367 @@ async function verifyTeamActivation(store, alias, managed, external, connectImpl
10554
10657
  });
10555
10658
  }
10556
10659
  //#endregion
10660
+ //#region src/lib/agent-server/catalogue.ts
10661
+ /**
10662
+ * The team/profile catalogue the desktop Run Center composes runs from.
10663
+ *
10664
+ * Scoped to the *selected agent identity*, never a human session: the daemon
10665
+ * already reads runtime profiles with agent credentials on every run, so this
10666
+ * reads the same data with the same authority, earlier. An agent-scoped list is
10667
+ * also authoritative about what can actually run, which a human-scoped list is
10668
+ * not — Console needs a "this agent cannot poll that team" error precisely
10669
+ * because it offers teams the agent cannot serve.
10670
+ *
10671
+ * Team and diary travel together here for the same reason `RunSpec` pairs
10672
+ * them: the CLI's context store refuses one without the other, and a diary
10673
+ * that drifts from its team means entries land in the wrong place.
10674
+ */
10675
+ async function buildCatalogue(options) {
10676
+ const { agent, machine, identityDefault, logger } = options;
10677
+ const entries = await Promise.all(agent.teamIds.map(async (teamId) => {
10678
+ try {
10679
+ const result = await agent.readTeam(teamId);
10680
+ if (result.team.id !== teamId) throw new Error("Team response mismatch");
10681
+ const diaries = result.diaries.filter((diary) => diary.teamId === teamId).map(({ id, name }) => ({
10682
+ id,
10683
+ name
10684
+ }));
10685
+ const team = {
10686
+ teamId,
10687
+ teamName: result.team.name,
10688
+ available: true,
10689
+ blockers: [],
10690
+ credential: result.credential,
10691
+ diaries,
10692
+ defaultDiaryId: resolveDefaultDiary(teamId, diaries, identityDefault)
10693
+ };
10694
+ const profiles = result.profiles.filter((profile) => profile.teamId === teamId).map((profile) => ({
10695
+ ...profile,
10696
+ ...deriveProfileReadiness(profile, machine)
10697
+ }));
10698
+ try {
10699
+ const page = await agent.readProjects(teamId);
10700
+ return {
10701
+ team,
10702
+ profiles,
10703
+ projects: page.items.filter((project) => project.teamId === teamId && !project.archived),
10704
+ projectErrors: page.truncated ? [{
10705
+ teamId,
10706
+ code: "truncated",
10707
+ message: "Only the first projects are listed. Archive unused projects to see the rest."
10708
+ }] : []
10709
+ };
10710
+ } catch (error) {
10711
+ logger?.warn({
10712
+ ...safeErrorContext(error),
10713
+ teamId,
10714
+ code: "agent_server_project_discovery_failed"
10715
+ }, "AgentServer project discovery failed");
10716
+ return {
10717
+ team,
10718
+ profiles,
10719
+ projects: [],
10720
+ projectErrors: [projectError(teamId, error)]
10721
+ };
10722
+ }
10723
+ } catch (error) {
10724
+ const blocker = credentialBlocker(error);
10725
+ logger?.warn({
10726
+ ...safeErrorContext(error),
10727
+ teamId,
10728
+ blocker: blocker.code,
10729
+ code: "agent_server_team_unavailable"
10730
+ }, "AgentServer team credential unavailable");
10731
+ return {
10732
+ team: {
10733
+ teamId,
10734
+ teamName: teamId,
10735
+ available: false,
10736
+ blockers: [blocker],
10737
+ credential: agent.lastVerified(teamId),
10738
+ diaries: [],
10739
+ defaultDiaryId: null
10740
+ },
10741
+ profiles: [],
10742
+ projects: [],
10743
+ projectErrors: []
10744
+ };
10745
+ }
10746
+ }));
10747
+ const teams = entries.map(({ team }) => team);
10748
+ const available = teams.filter((team) => team.available);
10749
+ return {
10750
+ teams,
10751
+ defaultTeamId: available.find((team) => team.teamId === identityDefault.teamId)?.teamId ?? available[0]?.teamId ?? null,
10752
+ profiles: entries.flatMap((entry) => entry.profiles),
10753
+ projects: entries.flatMap((entry) => entry.projects),
10754
+ projectErrors: entries.flatMap((entry) => entry.projectErrors)
10755
+ };
10756
+ }
10757
+ function projectError(teamId, error) {
10758
+ if (error instanceof MoltNetError && (error.statusCode === 401 || error.statusCode === 403)) return {
10759
+ teamId,
10760
+ code: "forbidden",
10761
+ message: "This team credential cannot list projects. Renew it with project access."
10762
+ };
10763
+ if (error instanceof ProjectPaginationError) return {
10764
+ teamId,
10765
+ code: "invalid_response",
10766
+ message: "The server returned an unreadable project list."
10767
+ };
10768
+ return {
10769
+ teamId,
10770
+ code: "unreachable",
10771
+ message: "Projects could not be loaded. Retry project discovery."
10772
+ };
10773
+ }
10774
+ /** The one default-diary rule, shared with General run start. */
10775
+ function resolveDefaultDiary(teamId, diaries, identityDefault) {
10776
+ const bound = diaries.find((diary) => diary.id === identityDefault.diaryId);
10777
+ if (bound && identityDefault.teamId === teamId) return bound.id;
10778
+ return diaries.length === 1 ? diaries[0]?.id ?? null : null;
10779
+ }
10780
+ //#endregion
10781
+ //#region src/lib/agent-server/http-error.ts
10782
+ /** A route failure with a stable wire code; the server error handler maps it verbatim. */
10783
+ var AgentServerHttpError = class extends Error {
10784
+ name = "AgentServerHttpError";
10785
+ constructor(statusCode, code, message, options) {
10786
+ super(message, options);
10787
+ this.statusCode = statusCode;
10788
+ this.code = code;
10789
+ }
10790
+ };
10791
+ //#endregion
10792
+ //#region src/lib/agent-server/identity-binding.ts
10793
+ /**
10794
+ * The identity-wide team/diary binding, read from `<identityDir>/env`.
10795
+ *
10796
+ * This mirrors the Go CLI's `identityDefaultBinding` (`project_selection.go`),
10797
+ * which is the fallback the CLI uses when a working directory has no
10798
+ * registered project binding.
10799
+ *
10800
+ * The desktop cannot use the CLI's *location* bindings at all — its composer
10801
+ * has no working directory to key on — but it can honour this identity-wide
10802
+ * default, so an operator sees their familiar team preselected rather than an
10803
+ * arbitrary first entry.
10804
+ *
10805
+ * Like the CLI, a half-filled pair is treated as no binding: a team without a
10806
+ * diary is ignored.
10807
+ */
10808
+ function readIdentityDefaultBinding(identityDir) {
10809
+ let contents;
10810
+ try {
10811
+ contents = readFileSync(join(identityDir, "env"), "utf8");
10812
+ } catch {
10813
+ return {};
10814
+ }
10815
+ const env = parseEnv(contents);
10816
+ const teamId = env["MOLTNET_TEAM_ID"]?.trim();
10817
+ const diaryId = env["MOLTNET_DIARY_ID"]?.trim();
10818
+ if (!teamId || !diaryId) return {};
10819
+ return {
10820
+ teamId,
10821
+ diaryId
10822
+ };
10823
+ }
10824
+ async function verifyProjectTarget(reader, target, options) {
10825
+ const { signal, logger } = options;
10826
+ let project = null;
10827
+ let diary = null;
10828
+ try {
10829
+ await untilAborted(signal, async () => {
10830
+ if (target.projectId) project = await reader.readProject(target.teamId, target.projectId, signal);
10831
+ if (target.diaryId) diary = await reader.readDiary(target.teamId, target.diaryId, signal);
10832
+ });
10833
+ } catch (error) {
10834
+ if (!signal.aborted) {
10835
+ if (error instanceof TeamCredentialError) throw error;
10836
+ if (notVisible(error)) throw projectUnavailable();
10837
+ }
10838
+ logger?.warn({
10839
+ ...safeErrorContext(error),
10840
+ teamId: target.teamId,
10841
+ ...target.projectId ? { projectId: target.projectId } : {},
10842
+ ...target.diaryId ? { diaryId: target.diaryId } : {},
10843
+ code: signal.aborted ? "agent_server_project_check_aborted" : "agent_server_project_check_failed"
10844
+ }, "AgentServer project check failed");
10845
+ throw checkUnavailable(error, signal.aborted);
10846
+ }
10847
+ const found = project;
10848
+ if (target.projectId && (!found || found.id !== target.projectId || found.teamId !== target.teamId || found.archived)) throw projectUnavailable();
10849
+ const foundDiary = diary;
10850
+ if (target.diaryId && (!foundDiary || foundDiary.id !== target.diaryId || foundDiary.teamId !== target.teamId)) throw new AgentServerHttpError(400, "diary_unavailable", "Choose a diary belonging to this team");
10851
+ return { project: found };
10852
+ }
10853
+ /** Null when the credential cannot see the resource; other failures propagate. */
10854
+ async function visibleOrNull(read) {
10855
+ try {
10856
+ return await read();
10857
+ } catch (error) {
10858
+ if (notVisible(error)) return null;
10859
+ throw error;
10860
+ }
10861
+ }
10862
+ /**
10863
+ * Settles when `work` does or `signal` aborts. The SDK's team, project and
10864
+ * diary reads take no signal, so an in-flight GET may finish in the background;
10865
+ * readers check the signal between steps so nothing further starts.
10866
+ */
10867
+ async function untilAborted(signal, work) {
10868
+ signal.throwIfAborted();
10869
+ const aborted = new Promise((_, reject) => {
10870
+ signal.addEventListener("abort", () => {
10871
+ const reason = signal.reason;
10872
+ reject(reason instanceof Error ? reason : new Error(String(reason)));
10873
+ }, { once: true });
10874
+ });
10875
+ return Promise.race([work(), aborted]);
10876
+ }
10877
+ /** True only when `cause` is the abort itself, not a failure seen after it fired. */
10878
+ function causedByAbort(cause, signal) {
10879
+ if (!signal.aborted) return false;
10880
+ if (cause === signal.reason) return true;
10881
+ const error = cause;
10882
+ return error?.cause === signal.reason || error?.name === "AbortError" || error?.name === "TimeoutError";
10883
+ }
10884
+ function checkUnavailable(cause, timedOut) {
10885
+ return new AgentServerHttpError(503, "project_check_unavailable", timedOut ? "The request could not be completed in time, so nothing was changed. Retry in a moment." : "The server could not confirm this project. Retry in a moment.", { cause });
10886
+ }
10887
+ function projectUnavailable() {
10888
+ return new AgentServerHttpError(400, "project_unavailable", "Verify team access and choose an available project");
10889
+ }
10890
+ function notVisible(error) {
10891
+ return error instanceof MoltNetError && [
10892
+ 401,
10893
+ 403,
10894
+ 404
10895
+ ].includes(error.statusCode ?? 0);
10896
+ }
10897
+ //#endregion
10898
+ //#region src/lib/agent-server/protected-roots.ts
10899
+ /**
10900
+ * A worker holds agent and provider keys, so neither a run nor a saved
10901
+ * location may use a folder inside, or containing, the MoltNet store or its
10902
+ * secrets. Both sides are compared canonically.
10903
+ */
10904
+ function isProtectedFolder(source, roots) {
10905
+ const folder = canonicalOrSelf(source);
10906
+ return roots.some((root) => {
10907
+ const protectedRoot = canonicalOrSelf(root);
10908
+ return within(folder, protectedRoot) || within(protectedRoot, folder);
10909
+ });
10910
+ }
10911
+ var PROTECTED_FOLDER_MESSAGE = "Choose a folder outside the MoltNet configuration store";
10912
+ function canonicalOrSelf(path) {
10913
+ try {
10914
+ return realpathSync.native(path);
10915
+ } catch {
10916
+ return path;
10917
+ }
10918
+ }
10919
+ function within(child, parent) {
10920
+ const suffix = relative(parent, child);
10921
+ return suffix === "" || !isAbsolute(suffix) && suffix !== ".." && !suffix.startsWith(`..${sep}`);
10922
+ }
10923
+ //#endregion
10924
+ //#region src/lib/agent-server/managed-project-selection.ts
10925
+ /** True when the start request names any project selection field. */
10926
+ function requestsProjectSelection(spec) {
10927
+ return spec.projectId !== void 0 || spec.location !== void 0 || spec.source !== void 0 || spec.strategy !== void 0;
10928
+ }
10929
+ /**
10930
+ * Resolve against the base store once; the worker receives only this captured
10931
+ * config. The request in `spec` is never modified: resolved values are
10932
+ * returned in `workspace` and `effective`, so a run can be replayed from what
10933
+ * the caller asked for.
10934
+ */
10935
+ async function resolveManagedProjectSelection(options) {
10936
+ const { spec, root, cwd, apiUrl, client, signal } = options;
10937
+ if (spec.source !== void 0) {
10938
+ if (!isAbsolute(spec.source)) throw new ProjectConfigError("selection", "Select an absolute source folder");
10939
+ let canonical;
10940
+ try {
10941
+ canonical = await canonicalDirectory(spec.source);
10942
+ } catch (cause) {
10943
+ throw new ProjectConfigError("selection", "The selected folder is unavailable. Choose an existing folder.", { cause });
10944
+ }
10945
+ assertOutsideProtected(canonical, options.protectedRoots);
10946
+ }
10947
+ if (spec.projectId === null && spec.location) throw new ProjectConfigError("selection", "General work cannot also name a location");
10948
+ const selection = await resolveRunProjectSelection({
10949
+ agent: spec.agent,
10950
+ cwd,
10951
+ team: spec.teamId,
10952
+ apiUrl,
10953
+ "config-file": getProjectConfigPath({ root }),
10954
+ general: !spec.projectId && !spec.location,
10955
+ project: spec.projectId ?? void 0,
10956
+ binding: spec.location,
10957
+ source: spec.source,
10958
+ "workspace-strategy": spec.strategy
10959
+ }, {
10960
+ signal,
10961
+ guardSource: (source) => assertOutsideProtected(source, options.protectedRoots)
10962
+ });
10963
+ const chosenSource = selection.workspaceExplicit ? selection.source : void 0;
10964
+ const reader = {
10965
+ readProject: (teamId, projectId, readSignal) => {
10966
+ readSignal.throwIfAborted();
10967
+ return visibleOrNull(() => client.projects.get(projectId, { teamId }));
10968
+ },
10969
+ readDiary: (teamId, diaryId, readSignal) => {
10970
+ readSignal.throwIfAborted();
10971
+ return visibleOrNull(() => client.diaries.get(diaryId, { teamId }));
10972
+ }
10973
+ };
10974
+ const check = {
10975
+ signal,
10976
+ ...options.logger ? { logger: options.logger } : {}
10977
+ };
10978
+ const { project } = await verifyProjectTarget(reader, {
10979
+ teamId: spec.teamId,
10980
+ projectId: selection.projectId
10981
+ }, check);
10982
+ const diaryId = spec.diaryId ?? selection.binding?.diaryId ?? project?.defaultDiaryId ?? (selection.projectId === null ? await options.generalDefaultDiary?.() : void 0) ?? void 0;
10983
+ if (diaryId) await verifyProjectTarget(reader, {
10984
+ teamId: spec.teamId,
10985
+ projectId: null,
10986
+ diaryId
10987
+ }, check);
10988
+ const resolvedBinding = selection.binding ? {
10989
+ ...selection.binding,
10990
+ ...diaryId ? { diaryId } : {}
10991
+ } : void 0;
10992
+ const config = {
10993
+ version: 1,
10994
+ bindings: resolvedBinding ? [resolvedBinding] : []
10995
+ };
10996
+ const workspace = {
10997
+ projectId: selection.projectId,
10998
+ ...resolvedBinding ? { location: resolvedBinding.name } : {},
10999
+ ...diaryId ? { diaryId } : {},
11000
+ ...chosenSource ? { source: chosenSource } : {},
11001
+ strategy: selection.workspaceExplicit ? selection.strategy : PROFILE_DEFAULT_STRATEGY
11002
+ };
11003
+ return {
11004
+ selection: {
11005
+ ...selection,
11006
+ binding: resolvedBinding
11007
+ },
11008
+ workspace,
11009
+ config,
11010
+ effective: {
11011
+ ...spec,
11012
+ projectId: selection.projectId,
11013
+ ...diaryId ? { diaryId } : {}
11014
+ }
11015
+ };
11016
+ }
11017
+ function assertOutsideProtected(source, roots) {
11018
+ if (isProtectedFolder(source, roots)) throw new ProjectConfigError("selection", PROTECTED_FOLDER_MESSAGE);
11019
+ }
11020
+ //#endregion
10557
11021
  //#region src/lib/agent-server/runs.ts
10558
11022
  var STOP_GRACE_MS = 1e4;
10559
11023
  var STOP_FORCE_MS = 2e3;
@@ -10642,7 +11106,7 @@ var RunManager = class {
10642
11106
  };
10643
11107
  }
10644
11108
  /** Assemble child env + args for a run. Exposed for tests. */
10645
- async prepare(spec, agent, piDir, providers = this.store.readProviders(), runtimeModule) {
11109
+ async prepare(spec, agent, piDir, providers = this.store.readProviders(), runtimeModule, workspace) {
10646
11110
  const { activation, config } = agent;
10647
11111
  const homeDir = join(dirname(piDir), "home");
10648
11112
  const env = {
@@ -10687,7 +11151,16 @@ var RunManager = class {
10687
11151
  String(runtimeSettings.heartbeatIntervalMs),
10688
11152
  "--warm-retention-sec",
10689
11153
  String(runtimeSettings.warmRetentionSec),
10690
- ...target.extraArgs
11154
+ ...target.extraArgs,
11155
+ ...workspace ? projectRunArgs({
11156
+ "config-file": workspace.configPath,
11157
+ ...workspace.location ? { binding: workspace.location } : { general: true },
11158
+ ...workspace.strategy === "profile-default" ? {} : {
11159
+ source: workspace.source,
11160
+ "workspace-strategy": workspace.strategy
11161
+ },
11162
+ "state-dir": this.runStateDir(spec, workspace)
11163
+ }) : []
10691
11164
  ];
10692
11165
  env["MOLTNET_AGENT_KEY"] = requireCredentialSnapshot(agent).agentKey;
10693
11166
  env["MOLTNET_API_URL"] = activation.apiUrl ?? (activation.source === "external" ? activation.configApiUrl : "");
@@ -10718,10 +11191,11 @@ var RunManager = class {
10718
11191
  return {
10719
11192
  args,
10720
11193
  env,
10721
- cwd: target.cwd
11194
+ cwd: workspace?.source ?? (workspace ? join(dirname(piDir), "workspace") : target.cwd)
10722
11195
  };
10723
11196
  }
10724
11197
  async start(spec, signal) {
11198
+ spec = structuredClone(spec);
10725
11199
  validateRunSpec(spec);
10726
11200
  const releaseStart = this.reserveStart(spec.agent);
10727
11201
  try {
@@ -10732,24 +11206,65 @@ var RunManager = class {
10732
11206
  }
10733
11207
  async startReserved(spec, signal) {
10734
11208
  this.assertStartOpen(signal);
10735
- const agent = await (this.options.verifyActivationImpl ?? verifyTeamActivation)(this.store, spec.agent, this.options.secretProviders, this.options.externalSecretProviders, void 0, signal, spec.teamId).catch((cause) => {
11209
+ const deadline = AbortSignal.any([...signal ? [signal] : [], AbortSignal.timeout(this.options.startTimeoutMs ?? 1e4)]);
11210
+ const verify = this.options.verifyActivationImpl ?? verifyTeamActivation;
11211
+ const agent = await untilAborted(deadline, () => verify(this.store, spec.agent, this.options.secretProviders, this.options.externalSecretProviders, void 0, deadline, spec.teamId)).catch((cause) => {
11212
+ this.assertStartOpen(signal, deadline, cause);
10736
11213
  if (cause instanceof TeamCredentialError || cause instanceof AgentServerStoreError) throw cause;
10737
11214
  throw new AgentServerIdentityError("verification_failed", `Cannot start agent "${spec.agent}" for team "${spec.teamId}": credential verification failed. Check the selected team key and activation.`);
10738
11215
  });
10739
- this.assertStartOpen(signal);
11216
+ this.assertStartOpen(signal, deadline);
10740
11217
  if (agent.boundTeamId && agent.boundTeamId !== spec.teamId) throw new AgentServerRunError("invalid_spec", `agent "${spec.agent}" has a key bound to team ${agent.boundTeamId}; start the run with that team, or create a new agent with an enrollment token from team ${spec.teamId}`);
10741
11218
  const id = `${Date.now().toString(36)}-${randomBytes(4).toString("hex")}`;
10742
- const runDir = this.store.runDir(id);
11219
+ const runDir = resolve(this.store.runDir(id));
10743
11220
  const piDir = join(runDir, "pi");
10744
- const providers = this.store.readProviders();
10745
- const runtimeModule = this.options.resolveRuntimeModule ? await this.options.resolveRuntimeModule(spec, agent, dirname(piDir)) : this.options.runtimeRegistry ? await this.resolveRuntimeModule(spec, agent, dirname(piDir)) : void 0;
10746
- const { args, env, cwd } = await this.prepare(spec, agent, piDir, providers, runtimeModule);
10747
- this.assertStartOpen(signal);
10748
11221
  let child;
10749
11222
  let logStream;
10750
11223
  let logLimiter;
11224
+ let workspace;
11225
+ let effective = spec;
11226
+ let selection;
10751
11227
  try {
10752
11228
  const { logPath } = this.store.createRunDir(id);
11229
+ const executionDir = join(runDir, "workspace");
11230
+ if (requestsProjectSelection(spec)) {
11231
+ mkdirSync(executionDir, {
11232
+ recursive: true,
11233
+ mode: 448
11234
+ });
11235
+ const projectRoot = this.options.projectRoot ?? this.store.root;
11236
+ const resolved = await resolveManagedProjectSelection({
11237
+ spec,
11238
+ root: projectRoot,
11239
+ cwd: executionDir,
11240
+ apiUrl: agent.activation.apiUrl ?? agent.config.endpoints.api,
11241
+ client: requireCredentialSnapshot(agent).client,
11242
+ protectedRoots: [
11243
+ projectRoot,
11244
+ this.store.root,
11245
+ this.store.secretsDir
11246
+ ],
11247
+ signal: deadline,
11248
+ logger: { warn: (context, message) => this.log("warn", message, context) },
11249
+ generalDefaultDiary: () => this.generalDefaultDiary(spec, agent, deadline)
11250
+ });
11251
+ workspace = {
11252
+ ...resolved.workspace,
11253
+ configPath: join(runDir, "projects.json")
11254
+ };
11255
+ effective = resolved.effective;
11256
+ selection = resolved.selection;
11257
+ writeRunSnapshot(workspace.configPath, resolved.config);
11258
+ mkdirSync(this.runStateDir(spec, workspace), {
11259
+ recursive: true,
11260
+ mode: 448
11261
+ });
11262
+ }
11263
+ const providers = this.store.readProviders();
11264
+ const executionCwd = workspace ? workspace.source ?? executionDir : dirname(piDir);
11265
+ const runtimeModule = await untilAborted(deadline, async () => this.options.resolveRuntimeModule ? this.options.resolveRuntimeModule(effective, agent, executionCwd) : this.options.runtimeRegistry ? this.resolveRuntimeModule(effective, agent, executionCwd, selection) : void 0);
11266
+ const { args, env, cwd } = await this.prepare(effective, agent, piDir, providers, runtimeModule, workspace);
11267
+ this.assertStartOpen(signal, deadline);
10753
11268
  for (const dir of [
10754
11269
  env.HOME,
10755
11270
  env.XDG_CACHE_HOME,
@@ -10780,7 +11295,7 @@ var RunManager = class {
10780
11295
  child?.kill("SIGKILL");
10781
11296
  });
10782
11297
  const spawnImpl = this.options.spawnImpl ?? spawn;
10783
- this.assertStartOpen(signal);
11298
+ this.assertStartOpen(signal, deadline);
10784
11299
  child = spawnImpl(entry.execPath, [
10785
11300
  ...entry.execArgv,
10786
11301
  entry.scriptPath,
@@ -10808,6 +11323,7 @@ var RunManager = class {
10808
11323
  child.stderr?.pipe(logLimiter, { end: false });
10809
11324
  const record = {
10810
11325
  ...spec,
11326
+ ...workspace ? { workspace } : {},
10811
11327
  id,
10812
11328
  status: "running",
10813
11329
  pid: child.pid,
@@ -10850,6 +11366,7 @@ var RunManager = class {
10850
11366
  this.store.writeRun(record);
10851
11367
  this.log("info", "agent server run started", {
10852
11368
  ...runContext(id, spec.agent, child),
11369
+ ...selectionContext(spec, workspace),
10853
11370
  transition: "running"
10854
11371
  });
10855
11372
  return record;
@@ -10864,20 +11381,29 @@ var RunManager = class {
10864
11381
  });
10865
11382
  this.log("error", "agent server run failed to start", {
10866
11383
  ...runContext(id, spec.agent, child),
11384
+ ...selectionContext(spec, workspace),
10867
11385
  transition: "start_failed",
10868
11386
  ...safeRunError(cause)
10869
11387
  });
11388
+ if (!signal?.aborted && !this.closing && !(cause instanceof AgentServerHttpError) && causedByAbort(cause, deadline)) throw startTimedOut(cause);
10870
11389
  throw cause;
10871
11390
  }
10872
11391
  }
10873
- async resolveRuntimeModule(spec, activated, cwd) {
10874
- const profiles = await resolveRuntimeProfiles({
11392
+ async resolveRuntimeModule(spec, activated, cwd, selection) {
11393
+ const effectiveProfiles = (await resolveRuntimeProfiles({
10875
11394
  agent: await this.connectAgent(activated, spec.teamId),
10876
11395
  profiles: spec.profiles,
10877
11396
  teamId: spec.teamId,
10878
11397
  cwd
11398
+ })).map((profile) => {
11399
+ if (!selection) return profile;
11400
+ try {
11401
+ return applyProjectWorkspacePolicy(profile, selection);
11402
+ } catch (error) {
11403
+ throw new AgentServerRunError("invalid_spec", error instanceof Error ? error.message : "This profile does not support the selected workspace strategy");
11404
+ }
10879
11405
  });
10880
- const kinds = [...new Set(profiles.map((profile) => profile.runtimeKind))];
11406
+ const kinds = [...new Set(effectiveProfiles.map((profile) => profile.runtimeKind))];
10881
11407
  if (kinds.length !== 1) throw new AgentServerRunError("invalid_spec", "All profiles in a server-managed run must use the same runtime kind.");
10882
11408
  const kind = kinds[0];
10883
11409
  let registration;
@@ -11022,8 +11548,10 @@ var RunManager = class {
11022
11548
  else this.startingByAgent.delete(agent);
11023
11549
  };
11024
11550
  }
11025
- assertStartOpen(signal) {
11551
+ /** Shutdown or disconnect wins; otherwise an expired budget is a retryable 503. */
11552
+ assertStartOpen(signal, deadline, cause) {
11026
11553
  if (this.closing || signal?.aborted) throw new AgentServerRunError("invalid_spec", "agent server is shutting down");
11554
+ if (deadline?.aborted && (cause === void 0 || causedByAbort(cause, deadline))) throw startTimedOut(cause);
11027
11555
  }
11028
11556
  pruneCompletedRuns() {
11029
11557
  const removed = this.store.pruneCompletedRuns({
@@ -11055,11 +11583,41 @@ var RunManager = class {
11055
11583
  });
11056
11584
  }
11057
11585
  }
11058
- log(level, message, context) {
11059
- this.options.logger?.[level](context, message);
11060
- }
11061
- };
11062
- function createByteLimitTransform(limit, onTruncated) {
11586
+ /**
11587
+ * Same rule as the catalogue. Best effort: without it the worker resolves its
11588
+ * own diary, as it did before project selection existed.
11589
+ */
11590
+ async generalDefaultDiary(spec, agent, deadline) {
11591
+ try {
11592
+ const { items } = await untilAborted(deadline, () => requireCredentialSnapshot(agent).client.diaries.list());
11593
+ return resolveDefaultDiary(spec.teamId, items.filter((diary) => diary.teamId === spec.teamId), readIdentityDefaultBinding(this.store.identityDir(spec.agent))) ?? void 0;
11594
+ } catch (error) {
11595
+ this.log("warn", "agent server default diary lookup failed", {
11596
+ ...safeRunError(error),
11597
+ agent: spec.agent,
11598
+ teamId: spec.teamId
11599
+ });
11600
+ return;
11601
+ }
11602
+ }
11603
+ /**
11604
+ * Stable across runs, so retries and continuations find their execution-plan
11605
+ * cache and task workspaces. Keyed by agent and location, under the store,
11606
+ * never inside the user's folder.
11607
+ */
11608
+ runStateDir(spec, workspace) {
11609
+ const scope = createHash("sha256").update(JSON.stringify([
11610
+ spec.teamId,
11611
+ workspace.projectId,
11612
+ workspace.source ?? null
11613
+ ])).digest("hex").slice(0, 12);
11614
+ return join(this.store.root, "run-state", spec.agent, `${workspace.location ? locationSegment(workspace.location) : "general"}-${scope}`);
11615
+ }
11616
+ log(level, message, context) {
11617
+ this.options.logger?.[level](context, message);
11618
+ }
11619
+ };
11620
+ function createByteLimitTransform(limit, onTruncated) {
11063
11621
  const byteLimit = Math.max(0, Math.floor(limit));
11064
11622
  const contentLimit = Math.max(0, byteLimit - LOG_TRUNCATION_MARKER.length);
11065
11623
  let emitted = 0;
@@ -11082,6 +11640,33 @@ function createByteLimitTransform(limit, onTruncated) {
11082
11640
  callback();
11083
11641
  } });
11084
11642
  }
11643
+ /** Non-secret ids that tie a run's logs to its project selection. */
11644
+ function selectionContext(spec, workspace) {
11645
+ return {
11646
+ ...spec.projectId !== void 0 ? { requestedProjectId: spec.projectId } : {},
11647
+ ...spec.location ? { requestedLocation: spec.location } : {},
11648
+ ...workspace ? {
11649
+ projectId: workspace.projectId,
11650
+ ...workspace.location ? { location: workspace.location } : {},
11651
+ ...workspace.diaryId ? { diaryId: workspace.diaryId } : {},
11652
+ strategy: workspace.strategy
11653
+ } : {}
11654
+ };
11655
+ }
11656
+ /** Readable when it is a safe path segment; hashed otherwise. */
11657
+ function locationSegment(name) {
11658
+ return /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/u.test(name) ? `location-${name}` : `location-${createHash("sha256").update(name).digest("hex").slice(0, 16)}`;
11659
+ }
11660
+ function startTimedOut(cause) {
11661
+ return new AgentServerHttpError(503, "start_timeout", "The run could not be prepared in time, so it was not started. Retry in a moment.", { cause });
11662
+ }
11663
+ /** Created exclusively and owner-only: a run never reuses or widens a snapshot. */
11664
+ function writeRunSnapshot(path, config) {
11665
+ writeFileSync(path, JSON.stringify(config), {
11666
+ mode: 384,
11667
+ flag: "wx"
11668
+ });
11669
+ }
11085
11670
  function runContext(runId, agent, child) {
11086
11671
  return {
11087
11672
  runId,
@@ -11309,112 +11894,6 @@ function createAgentServerSecretProviders(settings, store) {
11309
11894
  };
11310
11895
  }
11311
11896
  //#endregion
11312
- //#region src/lib/agent-server/readiness.ts
11313
- /**
11314
- * Whether a runtime profile can execute on *this* machine.
11315
- *
11316
- * A profile is authored in Console against a team; whether it can run depends
11317
- * on local facts only the server knows — which provider keys are configured
11318
- * here and which runtime kinds this machine can produce.
11319
- *
11320
- * The prerequisite comparison itself is **not** reimplemented here. Run start
11321
- * calls `validateRuntimeProfilePrerequisites`, and the catalogue calls the same
11322
- * function, so the composer cannot promise a run that startup would reject for
11323
- * a reason the two evaluated differently.
11324
- */
11325
- function deriveProfileReadiness(profile, machine) {
11326
- const blockers = [];
11327
- const env = {};
11328
- for (const [name, configured] of machine.providerEnv) if (configured) env[name] = "configured";
11329
- try {
11330
- validateRuntimeProfilePrerequisites(profile, env, {
11331
- tools: machine.inventory?.tools ?? profile.requiredTools,
11332
- executables: machine.inventory?.executables ?? profile.requiredExecutables
11333
- });
11334
- } catch (error) {
11335
- if (!(error instanceof RuntimeProfilePrerequisiteError)) throw error;
11336
- for (const name of error.missingEnv) blockers.push({
11337
- code: "env_missing",
11338
- message: `${name} is not configured on this machine.`,
11339
- remedy: "Add the key under Providers, then reopen this run."
11340
- });
11341
- for (const name of error.missingTools) blockers.push({
11342
- code: "tool_missing",
11343
- message: `The runtime does not provide the tool ${name}.`,
11344
- remedy: `Use a profile whose runtime provides ${name}, or change the profile in Console.`
11345
- });
11346
- for (const name of error.missingExecutables) blockers.push({
11347
- code: "executable_missing",
11348
- message: `The runtime does not provide the executable ${name}.`,
11349
- remedy: `Use a runtime that ships ${name}, or drop the requirement in Console.`
11350
- });
11351
- }
11352
- if (!machine.runtimeKinds.has(profile.runtimeKind)) blockers.push({
11353
- code: "runtime_unregistered",
11354
- message: `Runtime kind ${profile.runtimeKind} is not available on this machine.`,
11355
- remedy: "Register the runtime under Runtimes, then reopen this run."
11356
- });
11357
- return {
11358
- ready: blockers.length === 0,
11359
- blockers
11360
- };
11361
- }
11362
- //#endregion
11363
- //#region src/lib/agent-server/catalogue.ts
11364
- async function buildCatalogue(options) {
11365
- const { agent, machine, identityDefault } = options;
11366
- const entries = await Promise.all(agent.teamIds.map(async (teamId) => {
11367
- try {
11368
- const result = await agent.readTeam(teamId);
11369
- if (result.team.id !== teamId) throw new Error("Team response mismatch");
11370
- const diaries = result.diaries.filter((diary) => diary.teamId === teamId).map(({ id, name }) => ({
11371
- id,
11372
- name
11373
- }));
11374
- return {
11375
- team: {
11376
- teamId,
11377
- teamName: result.team.name,
11378
- available: true,
11379
- blockers: [],
11380
- credential: result.credential,
11381
- diaries,
11382
- defaultDiaryId: resolveDefaultDiary(teamId, diaries, identityDefault)
11383
- },
11384
- profiles: result.profiles.filter((profile) => profile.teamId === teamId).map((profile) => ({
11385
- ...profile,
11386
- ...deriveProfileReadiness(profile, machine)
11387
- }))
11388
- };
11389
- } catch (error) {
11390
- return {
11391
- team: {
11392
- teamId,
11393
- teamName: teamId,
11394
- available: false,
11395
- blockers: [credentialBlocker(error)],
11396
- credential: agent.lastVerified(teamId),
11397
- diaries: [],
11398
- defaultDiaryId: null
11399
- },
11400
- profiles: []
11401
- };
11402
- }
11403
- }));
11404
- const teams = entries.map(({ team }) => team);
11405
- const available = teams.filter((team) => team.available);
11406
- return {
11407
- teams,
11408
- defaultTeamId: available.find((team) => team.teamId === identityDefault.teamId)?.teamId ?? available[0]?.teamId ?? null,
11409
- profiles: entries.flatMap((entry) => entry.profiles)
11410
- };
11411
- }
11412
- function resolveDefaultDiary(teamId, diaries, identityDefault) {
11413
- const bound = diaries.find((diary) => diary.id === identityDefault.diaryId);
11414
- if (bound && identityDefault.teamId === teamId) return bound.id;
11415
- return diaries.length === 1 ? diaries[0]?.id ?? null : null;
11416
- }
11417
- //#endregion
11418
11897
  //#region src/lib/agent-server/enrollment.ts
11419
11898
  /** Native callers receive metadata only; approval and storage stay local. */
11420
11899
  async function enrollIdentityTeam(options) {
@@ -11499,37 +11978,139 @@ async function enrollIdentityTeam(options) {
11499
11978
  }
11500
11979
  }
11501
11980
  //#endregion
11502
- //#region src/lib/agent-server/identity-binding.ts
11981
+ //#region src/lib/agent-server/project-bindings.ts
11982
+ /** Each check may spawn `git`; bound how many run at once. */
11983
+ var READINESS_CONCURRENCY = 4;
11984
+ var PREPARATION_COPY = {
11985
+ unsupported_strategy: "Isolated directory preparation is unavailable. Choose Work here or a Git worktree.",
11986
+ hooks_unavailable: "Preparation hooks are unavailable. Remove the hooks or choose another location."
11987
+ };
11988
+ /** Normalize with the Go-compatible grammar, reporting a rejection as a coded error. */
11989
+ function locationEndpoint(value) {
11990
+ try {
11991
+ return normalizeProjectEndpoint(value);
11992
+ } catch {
11993
+ throw new AgentServerHttpError(400, "endpoint_unsupported", "Local project locations need an HTTPS server endpoint; HTTP is allowed only on loopback");
11994
+ }
11995
+ }
11996
+ /** Machine registrations live at the base store, outside connection directories. */
11997
+ var LocalProjectBindings = class {
11998
+ root;
11999
+ path;
12000
+ apiUrl;
12001
+ /** `protectedRoots`: store and secrets directories no location may use. */
12002
+ constructor(root, apiUrl, protectedRoots = []) {
12003
+ this.protectedRoots = protectedRoots;
12004
+ this.root = resolveStoreRoot({ root });
12005
+ this.path = getProjectConfigPath({ root: this.root });
12006
+ this.apiUrl = locationEndpoint(apiUrl);
12007
+ }
12008
+ async list() {
12009
+ const bindings = (await stored(() => readProjectConfig(this.path))).bindings.filter((binding) => normalizeProjectEndpoint(binding.apiUrl) === this.apiUrl);
12010
+ const locations = [];
12011
+ for (let i = 0; i < bindings.length; i += READINESS_CONCURRENCY) locations.push(...await Promise.all(bindings.slice(i, i + READINESS_CONCURRENCY).map((binding) => this.describe(binding))));
12012
+ return locations;
12013
+ }
12014
+ /** Once `signal` aborts, nothing is written, not even after the lock is taken. */
12015
+ async save(value, options = {}) {
12016
+ const input = structuredClone(value);
12017
+ validateProjectConfig({
12018
+ version: 1,
12019
+ bindings: [input]
12020
+ });
12021
+ if (locationEndpoint(input.apiUrl) !== this.apiUrl) throw new AgentServerHttpError(400, "endpoint_mismatch", "Location belongs to another endpoint");
12022
+ if (input.source && !isAbsolute(input.source)) throw new ProjectConfigError("validation", "Select an absolute source folder");
12023
+ const location = await this.describe(input, options.signal);
12024
+ if (!location.readiness.ready) throw new AgentServerHttpError(400, location.readiness.code ?? "location_unavailable", location.readiness.message ?? "Location is unavailable");
12025
+ const binding = {
12026
+ ...input,
12027
+ apiUrl: this.apiUrl,
12028
+ ...location.effectiveSource ? { source: location.effectiveSource } : {}
12029
+ };
12030
+ options.signal?.throwIfAborted();
12031
+ await this.update((config) => {
12032
+ options.signal?.throwIfAborted();
12033
+ const index = config.bindings.findIndex((entry) => entry.name === binding.name);
12034
+ const previous = config.bindings[index];
12035
+ if (previous && normalizeProjectEndpoint(previous.apiUrl) !== this.apiUrl) throw conflict("This name belongs to another endpoint");
12036
+ if (previous && hasPreparationHooks(previous.hooks)) throw conflict("Remove preparation hooks from the configuration before editing this location");
12037
+ if (previous && (previous.teamId !== binding.teamId || previous.projectId !== binding.projectId)) throw conflict("This name belongs to another project");
12038
+ if (binding.default) {
12039
+ for (const entry of config.bindings) if (normalizeProjectEndpoint(entry.apiUrl) === this.apiUrl && entry.teamId === binding.teamId && entry.projectId === binding.projectId) entry.default = false;
12040
+ }
12041
+ if (index < 0) config.bindings.push(binding);
12042
+ else config.bindings[index] = binding;
12043
+ });
12044
+ return this.describe(binding);
12045
+ }
12046
+ async remove(name) {
12047
+ await this.update((config) => {
12048
+ const index = config.bindings.findIndex((entry) => entry.name === name && normalizeProjectEndpoint(entry.apiUrl) === this.apiUrl);
12049
+ if (index < 0) throw new AgentServerHttpError(404, "location_not_found", "Location not found");
12050
+ config.bindings.splice(index, 1);
12051
+ });
12052
+ }
12053
+ update(mutate) {
12054
+ return stored(() => updateProjectConfig(this.path, mutate));
12055
+ }
12056
+ async describe(binding, signal) {
12057
+ const { effectiveSource, readiness } = await this.readiness(binding, signal);
12058
+ return {
12059
+ ...binding,
12060
+ effectiveSource,
12061
+ readiness
12062
+ };
12063
+ }
12064
+ async readiness(binding, signal) {
12065
+ const declared = binding.source ? resolve(this.root, binding.source) : null;
12066
+ const unavailable = (code, message, effectiveSource = declared) => ({
12067
+ effectiveSource,
12068
+ readiness: {
12069
+ ready: false,
12070
+ code,
12071
+ message
12072
+ }
12073
+ });
12074
+ const blocker = preparationBlocker(binding.strategy, binding.hooks);
12075
+ if (blocker) return unavailable(blocker.code, PREPARATION_COPY[blocker.code]);
12076
+ if (binding.strategy === "none") return {
12077
+ effectiveSource: declared,
12078
+ readiness: { ready: true }
12079
+ };
12080
+ if (!declared) return unavailable("folder_missing", "Choose an existing source folder.");
12081
+ let source;
12082
+ try {
12083
+ source = await canonicalDirectory(declared);
12084
+ } catch {
12085
+ return unavailable("folder_missing", "The source folder is unavailable. Choose an existing folder.");
12086
+ }
12087
+ if (isProtectedFolder(source, this.protectedRoots)) return unavailable("folder_protected", `${PROTECTED_FOLDER_MESSAGE}.`, source);
12088
+ if (binding.strategy === "git-worktree") try {
12089
+ await validateGitSource(source, signal);
12090
+ } catch (cause) {
12091
+ if (signal?.aborted) throw cause;
12092
+ return unavailable("git_unavailable", "Choose a Git repository root with a committed revision, or choose Work here.", source);
12093
+ }
12094
+ return {
12095
+ effectiveSource: source,
12096
+ readiness: { ready: true }
12097
+ };
12098
+ }
12099
+ };
11503
12100
  /**
11504
- * 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.
12101
+ * A stored file that fails validation is server-side state, not a bad request.
12102
+ * The detail names a local path, so it goes to the logs via `cause`.
11517
12103
  */
11518
- function readIdentityDefaultBinding(identityDir) {
11519
- let contents;
12104
+ async function stored(work) {
11520
12105
  try {
11521
- contents = readFileSync(join(identityDir, "env"), "utf8");
11522
- } catch {
11523
- return {};
12106
+ return await work();
12107
+ } catch (error) {
12108
+ if (error instanceof ProjectConfigError && error.kind === "validation") throw new AgentServerHttpError(500, "config_invalid", "The project locations file is invalid. Repair or remove it, then retry.", { cause: error });
12109
+ throw error;
11524
12110
  }
11525
- 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
- };
12111
+ }
12112
+ function conflict(message) {
12113
+ return new AgentServerHttpError(409, "location_conflict", message);
11533
12114
  }
11534
12115
  //#endregion
11535
12116
  //#region src/lib/agent-server/protocol.ts
@@ -11631,10 +12212,83 @@ var AgentServerCatalogueProfileSchema = Type.Intersect([Type.Pick(RuntimeProfile
11631
12212
  var AgentServerCatalogueSchema = Type.Object({
11632
12213
  teams: Type.Array(schemaRef(AgentServerCatalogueTeamSchema)),
11633
12214
  defaultTeamId: Type.Union([Type.String(), Type.Null()]),
11634
- profiles: Type.Array(schemaRef(AgentServerCatalogueProfileSchema))
12215
+ profiles: Type.Array(schemaRef(AgentServerCatalogueProfileSchema)),
12216
+ projects: Type.Array(Type.Pick(ProjectResponseSchema, [
12217
+ "id",
12218
+ "teamId",
12219
+ "name",
12220
+ "description",
12221
+ "defaultDiaryId",
12222
+ "archived"
12223
+ ])),
12224
+ projectErrors: Type.Array(Type.Object({
12225
+ teamId: Type.String(),
12226
+ code: Type.Union([
12227
+ "forbidden",
12228
+ "unreachable",
12229
+ "invalid_response",
12230
+ "truncated"
12231
+ ].map((value) => Type.Literal(value))),
12232
+ message: Type.String()
12233
+ }))
11635
12234
  }, { $id: "AgentServerCatalogue" });
11636
12235
  var CatalogueQuerySchema = Type.Object({ identity: Type.String({ minLength: 1 }) });
12236
+ var AgentServerProjectLocationSchema = Type.Object({
12237
+ name: Type.String({ minLength: 1 }),
12238
+ apiUrl: Type.String(),
12239
+ teamId: Type.String({ minLength: 1 }),
12240
+ projectId: Type.String({ minLength: 1 }),
12241
+ diaryId: Type.Optional(Type.String({ minLength: 1 })),
12242
+ source: Type.Optional(Type.String({ minLength: 1 })),
12243
+ strategy: Type.Union([
12244
+ "none",
12245
+ "existing",
12246
+ "git-worktree",
12247
+ "isolated-directory"
12248
+ ].map((value) => Type.Literal(value))),
12249
+ default: Type.Optional(Type.Boolean()),
12250
+ effectiveSource: Type.Union([Type.String(), Type.Null()]),
12251
+ readiness: Type.Object({
12252
+ ready: Type.Boolean(),
12253
+ code: Type.Optional(Type.String()),
12254
+ message: Type.Optional(Type.String())
12255
+ })
12256
+ }, { $id: "AgentServerProjectLocation" });
12257
+ /** Closed: hooks and other stored fields are never writable over this route. */
12258
+ var SaveProjectLocationSchema = Type.Object({
12259
+ identity: Type.String({ minLength: 1 }),
12260
+ teamId: Type.String({ minLength: 1 }),
12261
+ projectId: Type.String({ minLength: 1 }),
12262
+ diaryId: Type.Optional(Type.String({ minLength: 1 })),
12263
+ source: Type.Optional(Type.String({ minLength: 1 })),
12264
+ strategy: Type.Union([
12265
+ "none",
12266
+ "existing",
12267
+ "git-worktree"
12268
+ ].map((value) => Type.Literal(value))),
12269
+ default: Type.Optional(Type.Boolean())
12270
+ }, { additionalProperties: false });
12271
+ var ProjectLocationParamsSchema = Type.Object({ name: Type.String({ minLength: 1 }) });
12272
+ /** Local project selection on a run request; only the native client may set these. */
12273
+ var RunProjectFields = {
12274
+ projectId: Type.Optional(Type.Union([Type.String({ minLength: 1 }), Type.Null()])),
12275
+ location: Type.Optional(Type.String({ minLength: 1 })),
12276
+ source: Type.Optional(Type.String({ minLength: 1 })),
12277
+ strategy: Type.Optional(AgentServerProjectLocationSchema.properties.strategy)
12278
+ };
12279
+ /** Names gated to the native client; derived so a new field is gated automatically. */
12280
+ var NATIVE_RUN_FIELDS = Object.keys(RunProjectFields);
12281
+ /** What a run resolved to; the record's top-level fields stay as requested. */
12282
+ var RunWorkspaceSchema = Type.Object({
12283
+ projectId: Type.Union([Type.String(), Type.Null()]),
12284
+ location: Type.Optional(Type.String()),
12285
+ diaryId: Type.Optional(Type.String()),
12286
+ source: Type.Optional(Type.String()),
12287
+ strategy: Type.Union([AgentServerProjectLocationSchema.properties.strategy, Type.Literal(PROFILE_DEFAULT_STRATEGY)])
12288
+ });
11637
12289
  var AgentServerRunRecordSchema = Type.Object({
12290
+ ...RunProjectFields,
12291
+ workspace: Type.Optional(RunWorkspaceSchema),
11638
12292
  id: Type.String(),
11639
12293
  agent: Type.String(),
11640
12294
  teamId: Type.String(),
@@ -11714,6 +12368,7 @@ var PutProviderSchema = Type.Object({
11714
12368
  });
11715
12369
  var DiscoverModelsSchema = Type.Object({ models: ProviderModelList }, { $id: "DiscoveredModels" });
11716
12370
  var StartRunSchema = Type.Object({
12371
+ ...RunProjectFields,
11717
12372
  agent: Type.String(),
11718
12373
  teamId: Type.String(),
11719
12374
  diaryId: Type.Optional(Type.String()),
@@ -11730,6 +12385,7 @@ var LogStreamSchema = Type.String({
11730
12385
  contentMediaType: "text/event-stream"
11731
12386
  });
11732
12387
  var AGENT_SERVER_SCHEMAS = [
12388
+ AgentServerProjectLocationSchema,
11733
12389
  AgentServerHealthSchema,
11734
12390
  AgentServerProblemSchema,
11735
12391
  AgentServerAgentSchema,
@@ -11752,6 +12408,39 @@ var AGENT_SERVER_SCHEMAS = [
11752
12408
  var localControlSecurity = [{ agentServerToken: [] }];
11753
12409
  var problemResponse = { default: schemaRef(AgentServerProblemSchema) };
11754
12410
  var AgentServerRouteSchemas = {
12411
+ listProjectLocations: {
12412
+ operationId: "listNativeProjectLocations",
12413
+ tags: ["native-projects"],
12414
+ security: localControlSecurity,
12415
+ description: "Requires the Desktop native grant; browser authorization is insufficient.",
12416
+ response: {
12417
+ 200: Type.Object({ locations: Type.Array(schemaRef(AgentServerProjectLocationSchema)) }),
12418
+ ...problemResponse
12419
+ }
12420
+ },
12421
+ saveProjectLocation: {
12422
+ operationId: "saveNativeProjectLocation",
12423
+ tags: ["native-projects"],
12424
+ security: localControlSecurity,
12425
+ description: "Creates or replaces the named location. Requires the Desktop native grant.",
12426
+ params: ProjectLocationParamsSchema,
12427
+ body: SaveProjectLocationSchema,
12428
+ response: {
12429
+ 200: schemaRef(AgentServerProjectLocationSchema),
12430
+ ...problemResponse
12431
+ }
12432
+ },
12433
+ removeProjectLocation: {
12434
+ operationId: "removeNativeProjectLocation",
12435
+ tags: ["native-projects"],
12436
+ security: localControlSecurity,
12437
+ description: "Removes the registration only; the folder is left untouched. Requires the Desktop native grant.",
12438
+ params: ProjectLocationParamsSchema,
12439
+ response: {
12440
+ 200: Type.Object({ removed: Type.Boolean() }),
12441
+ ...problemResponse
12442
+ }
12443
+ },
11755
12444
  health: {
11756
12445
  operationId: "getAgentServerHealth",
11757
12446
  tags: ["system"],
@@ -11925,6 +12614,7 @@ var AgentServerRouteSchemas = {
11925
12614
  operationId: "startAgentServerRun",
11926
12615
  tags: ["runs"],
11927
12616
  security: localControlSecurity,
12617
+ description: "projectId (other than null), location, source and strategy are native-only: other origins receive 403 native_required. A request naming none of them runs without project workspace wiring. The record keeps these fields as requested; resolved values are in `workspace`.",
11928
12618
  body: StartRunSchema,
11929
12619
  response: {
11930
12620
  201: schemaRef(AgentServerRunSchema),
@@ -11959,8 +12649,8 @@ var AgentServerRouteSchemas = {
11959
12649
  * loopback-companion security profile (#2066): loopback Host enforcement,
11960
12650
  * exact-origin CORS, Fetch-Metadata guards, strict JSON parsing.
11961
12651
  *
11962
- * Control routes require a native process grant or an OAuth token bound to
11963
- * the native operator and this server instance. Origin checks apply to both.
12652
+ * Control routes require the process-scoped native Desktop grant. OAuth is
12653
+ * used only for native operator sign-in and credential provisioning.
11964
12654
  */
11965
12655
  var AGENT_SERVER_TOKEN_HEADER = "x-moltnet-agent-server-token";
11966
12656
  var BODY_LIMIT = 64 * 1024;
@@ -12019,14 +12709,6 @@ async function readAgentServerLogDelta(handle, state, limit = LOG_READ_LIMIT_BYT
12019
12709
  omitted
12020
12710
  };
12021
12711
  }
12022
- var AgentServerHttpError = class extends Error {
12023
- name = "AgentServerHttpError";
12024
- constructor(statusCode, code, message) {
12025
- super(message);
12026
- this.statusCode = statusCode;
12027
- this.code = code;
12028
- }
12029
- };
12030
12712
  function requireBody(request) {
12031
12713
  const body = request.body;
12032
12714
  if (typeof body !== "object" || body === null || Array.isArray(body)) throw new AgentServerHttpError(400, "invalid_body", "JSON object body required");
@@ -12112,22 +12794,6 @@ function buildAgentServer(input) {
12112
12794
  isOriginAllowed: (origin) => origin === "moltnet-agent-desktop://native" || origin === options.selfOrigin || browserOrigins.has(origin)
12113
12795
  });
12114
12796
  }
12115
- let verificationWindow = Date.now();
12116
- let verifications = 0;
12117
- const browserVerification = /* @__PURE__ */ new WeakMap();
12118
- function verifyBrowser(request, token) {
12119
- const previous = browserVerification.get(request);
12120
- if (previous) return previous;
12121
- if (Date.now() - verificationWindow >= RATE_LIMIT_WINDOW_MS) {
12122
- verificationWindow = Date.now();
12123
- verifications = 0;
12124
- }
12125
- if (++verifications > RATE_LIMIT_MAX) throw new AgentServerHttpError(429, "rate_limited", "Too many authorization attempts");
12126
- if (!oauth) throw new AgentServerHttpError(503, "oauth_unavailable", "Local OAuth is not configured");
12127
- const pending = oauth.verifyBrowser(token);
12128
- browserVerification.set(request, pending);
12129
- return pending;
12130
- }
12131
12797
  function hasValidNativeGrant(origin, token) {
12132
12798
  if (origin !== "moltnet-agent-desktop://native" || typeof token !== "string" || token.length === 0) return false;
12133
12799
  try {
@@ -12158,56 +12824,20 @@ function buildAgentServer(input) {
12158
12824
  max: options.rateLimitMax ?? RATE_LIMIT_MAX,
12159
12825
  timeWindow: RATE_LIMIT_WINDOW_MS,
12160
12826
  errorResponseBuilder: () => new AgentServerHttpError(429, "rate_limited", "Too many requests"),
12161
- keyGenerator: async (request) => {
12827
+ keyGenerator: (request) => {
12162
12828
  const origin = request.headers.origin;
12163
12829
  if (!isConfiguredOrigin(origin, options)) return `ip:${request.ip}`;
12164
12830
  const presented = request.headers[AGENT_SERVER_TOKEN_HEADER];
12165
- 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}`;
12831
+ return typeof presented === "string" && presented.length > 0 && hasValidNativeGrant(origin, presented) ? `origin:${origin}` : `unauth:${origin}:${request.ip}`;
12178
12832
  }
12179
12833
  });
12180
12834
  const requireAuthorizedOrigin = async (request) => {
12181
12835
  if (restartRequired) throw new AgentServerHttpError(409, "restart_required", "Restart the Agent Server to apply connection settings");
12182
12836
  const origin = requireOriginHeader(request.headers);
12183
12837
  const token = request.headers[AGENT_SERVER_TOKEN_HEADER];
12184
- if (typeof token !== "string" || token.length === 0) throw new AgentServerHttpError(401, "authorization_required", "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
- }
12838
+ if (typeof token !== "string" || token.length === 0) throw new AgentServerHttpError(401, "authorization_required", "Native authorization is required");
12839
+ if (origin !== "moltnet-agent-desktop://native") throw new AgentServerHttpError(403, "native_required", "Native Desktop authorization required");
12840
+ requireNativeGrant(request);
12211
12841
  return origin;
12212
12842
  };
12213
12843
  app.after(() => {
@@ -12216,13 +12846,12 @@ function buildAgentServer(input) {
12216
12846
  });
12217
12847
  app.get("/health", { schema: AgentServerRouteSchemas.health }, async () => ({ status: "ok" }));
12218
12848
  app.get("/v1/native/connection-settings", { schema: { hide: true } }, async (request) => {
12219
- if (await requireAuthorizedOrigin(request) !== "moltnet-agent-desktop://native" || !options.connectionSettings) throw new AgentServerHttpError(403, "native_required", "Native administration required");
12220
- return options.connectionSettings.view();
12849
+ return (await requireNativeOrigin(requireAuthorizedOrigin, request, options.connectionSettings)).view();
12221
12850
  });
12222
12851
  app.post("/v1/native/connection-settings", { schema: { hide: true } }, async (request) => {
12223
- if (await requireAuthorizedOrigin(request) !== "moltnet-agent-desktop://native" || !options.connectionSettings) throw new AgentServerHttpError(403, "native_required", "Native administration required");
12852
+ const store = await requireNativeOrigin(requireAuthorizedOrigin, request, options.connectionSettings);
12224
12853
  try {
12225
- const settings = options.runs.prepareServerRestart(() => options.connectionSettings.save(request.body));
12854
+ const settings = options.runs.prepareServerRestart(() => store.save(request.body));
12226
12855
  oauth?.cancel();
12227
12856
  restartRequired = true;
12228
12857
  return settings;
@@ -12230,40 +12859,6 @@ function buildAgentServer(input) {
12230
12859
  throw new AgentServerHttpError(400, "invalid_connection_settings", error instanceof Error ? error.message : "Invalid connection settings");
12231
12860
  }
12232
12861
  });
12233
- app.get("/oauth/metadata", { schema: {
12234
- operationId: "getAgentServerOAuthMetadata",
12235
- tags: ["operator"],
12236
- response: { 200: {
12237
- type: "object",
12238
- required: [
12239
- "protocolVersion",
12240
- "instance",
12241
- "issuer",
12242
- "authorizationUrl",
12243
- "tokenUrl",
12244
- "clientId",
12245
- "operatorConfigured"
12246
- ],
12247
- properties: {
12248
- protocolVersion: {
12249
- type: "integer",
12250
- const: OPERATOR_OAUTH.protocolVersion
12251
- },
12252
- instance: {
12253
- type: "string",
12254
- format: "uuid"
12255
- },
12256
- issuer: { type: "string" },
12257
- authorizationUrl: { type: "string" },
12258
- tokenUrl: { type: "string" },
12259
- clientId: { type: "string" },
12260
- operatorConfigured: { type: "boolean" }
12261
- }
12262
- } }
12263
- } }, async () => {
12264
- if (!oauth) throw new AgentServerHttpError(503, "oauth_unavailable", "Local OAuth is not configured");
12265
- return oauth.metadata();
12266
- });
12267
12862
  app.post("/v1/operator/sign-in", { schema: {
12268
12863
  operationId: "signInAgentServerOperator",
12269
12864
  response: { 200: {
@@ -12272,10 +12867,12 @@ function buildAgentServer(input) {
12272
12867
  required: ["state"]
12273
12868
  } }
12274
12869
  } }, async (request) => {
12275
- 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));
12870
+ await (await requireNativeOrigin(requireAuthorizedOrigin, request, oauth)).authorize(void 0, requestOperationSignal(request, options.shutdownSignal));
12277
12871
  return { state: "authorized" };
12278
12872
  });
12873
+ app.get("/v1/native/operator", { schema: { hide: true } }, async (request) => {
12874
+ return { operatorConfigured: (await requireNativeOrigin(requireAuthorizedOrigin, request, oauth)).operatorConfigured() };
12875
+ });
12279
12876
  app.post("/v1/operator/cancel", { schema: {
12280
12877
  operationId: "cancelAgentServerOperatorApproval",
12281
12878
  tags: ["operator"],
@@ -12289,8 +12886,7 @@ function buildAgentServer(input) {
12289
12886
  required: ["state"]
12290
12887
  } }
12291
12888
  } }, async (request) => {
12292
- if (await requireAuthorizedOrigin(request) !== "moltnet-agent-desktop://native" || !oauth) throw new AgentServerHttpError(403, "native_required", "Native administration required");
12293
- oauth.cancel();
12889
+ (await requireNativeOrigin(requireAuthorizedOrigin, request, oauth)).cancel();
12294
12890
  return { state: "cancelled" };
12295
12891
  });
12296
12892
  app.delete("/v1/operator", { schema: {
@@ -12306,8 +12902,7 @@ function buildAgentServer(input) {
12306
12902
  required: ["state"]
12307
12903
  } }
12308
12904
  } }, async (request) => {
12309
- if (await requireAuthorizedOrigin(request) !== "moltnet-agent-desktop://native" || !oauth) throw new AgentServerHttpError(403, "native_required", "Native administration required");
12310
- oauth.removeOperator();
12905
+ (await requireNativeOrigin(requireAuthorizedOrigin, request, oauth)).removeOperator();
12311
12906
  return { state: "removed" };
12312
12907
  });
12313
12908
  registerStatusRoute(app, options, requireAuthorizedOrigin);
@@ -12316,6 +12911,7 @@ function buildAgentServer(input) {
12316
12911
  registerSubscriptionRoutes(app, options, requireAuthorizedOrigin);
12317
12912
  registerRunRoutes(app, options, requireAuthorizedOrigin);
12318
12913
  registerCatalogueRoute(app, options, requireAuthorizedOrigin);
12914
+ registerProjectLocationRoutes(app, options, requireAuthorizedOrigin);
12319
12915
  });
12320
12916
  app.addHook("preClose", async () => {
12321
12917
  options.operatorOAuth?.cancel();
@@ -12342,6 +12938,103 @@ function buildAgentServer(input) {
12342
12938
  });
12343
12939
  return app;
12344
12940
  }
12941
+ async function requireNativeOrigin(authorize, request, ...resource) {
12942
+ if (await authorize(request) !== "moltnet-agent-desktop://native" || resource.length > 0 && resource[0] === void 0) throw new AgentServerHttpError(403, "native_required", "Native administration required");
12943
+ return resource[0];
12944
+ }
12945
+ function registerProjectLocationRoutes(app, options, authorize) {
12946
+ let locations;
12947
+ const getLocations = () => {
12948
+ const root = options.connectionSettings?.root;
12949
+ if (!root) throw new AgentServerHttpError(503, "locations_unavailable", "Project locations need the Desktop connection store");
12950
+ locations ??= new LocalProjectBindings(root, options.defaultApiUrl, [
12951
+ root,
12952
+ options.store.root,
12953
+ options.store.secretsDir
12954
+ ]);
12955
+ return locations;
12956
+ };
12957
+ const requireNativeRequest = async (request) => {
12958
+ await requireNativeOrigin(authorize, request);
12959
+ if (request.validationError) throw new AgentServerHttpError(400, "invalid_location", "Check the project location fields");
12960
+ };
12961
+ app.get("/v1/native/project-locations", { schema: AgentServerRouteSchemas.listProjectLocations }, async (request) => {
12962
+ await requireNativeRequest(request);
12963
+ return { locations: await getLocations().list() };
12964
+ });
12965
+ const saveFields = new Set(Object.keys(AgentServerRouteSchemas.saveProjectLocation.body.properties));
12966
+ app.put("/v1/native/project-locations/:name", {
12967
+ schema: AgentServerRouteSchemas.saveProjectLocation,
12968
+ attachValidation: true,
12969
+ preValidation: async (request) => {
12970
+ await requireNativeOrigin(authorize, request);
12971
+ const body = request.body;
12972
+ if (body && typeof body === "object" && Object.keys(body).some((key) => !saveFields.has(key))) throw new AgentServerHttpError(400, "invalid_location", "Check the project location fields");
12973
+ }
12974
+ }, async (request) => {
12975
+ await requireNativeRequest(request);
12976
+ const bindings = getLocations();
12977
+ const { name } = request.params;
12978
+ const { identity, ...location } = requireBody(request);
12979
+ const alias = identity.trim();
12980
+ requireActivation(options.store, alias);
12981
+ const { config } = await loadAgentActivation(options.store, alias);
12982
+ if (locationEndpoint(config.endpoints.api) !== bindings.apiUrl) throw new AgentServerHttpError(400, "endpoint_mismatch", "Choose an identity for the current server endpoint");
12983
+ const signal = AbortSignal.any([requestOperationSignal(request, options.shutdownSignal), AbortSignal.timeout(options.projectSaveTimeoutMs ?? 1e4)]);
12984
+ await verifyLocationTarget(options, alias, location, request.log, signal);
12985
+ try {
12986
+ return await bindings.save({
12987
+ ...location,
12988
+ name,
12989
+ apiUrl: bindings.apiUrl
12990
+ }, { signal });
12991
+ } catch (error) {
12992
+ if (signal.aborted) throw checkUnavailable(error, true);
12993
+ throw error;
12994
+ }
12995
+ });
12996
+ app.delete("/v1/native/project-locations/:name", {
12997
+ schema: AgentServerRouteSchemas.removeProjectLocation,
12998
+ attachValidation: true
12999
+ }, async (request) => {
13000
+ await requireNativeRequest(request);
13001
+ await getLocations().remove(request.params.name);
13002
+ return { removed: true };
13003
+ });
13004
+ }
13005
+ /**
13006
+ * A location save checks only its target team: `readTeam` verifies the team
13007
+ * credential and returns its diaries, then the shared check reads the project.
13008
+ */
13009
+ async function verifyLocationTarget(options, alias, target, logger, signal) {
13010
+ const agent = await catalogueAgent(options, alias);
13011
+ if (!agent.teamIds.includes(target.teamId)) throw projectUnavailable();
13012
+ let diaries = [];
13013
+ await verifyProjectTarget({
13014
+ readProject: async (teamId, projectId, readSignal) => {
13015
+ const team = await agent.readTeam(teamId, readSignal);
13016
+ if (team.team.id !== teamId) throw new Error("Team response mismatch");
13017
+ diaries = team.diaries;
13018
+ return agent.readProject(teamId, projectId, readSignal);
13019
+ },
13020
+ readDiary: async (teamId, diaryId) => diaries.find((diary) => diary.id === diaryId && diary.teamId === teamId) ?? null
13021
+ }, target, {
13022
+ signal,
13023
+ logger
13024
+ });
13025
+ }
13026
+ async function catalogueAgent(options, alias) {
13027
+ return options.catalogueAgentFor ? options.catalogueAgentFor(alias) : defaultCatalogueAgent(options, alias);
13028
+ }
13029
+ async function readIdentityCatalogue(options, alias, logger) {
13030
+ requireActivation(options.store, alias);
13031
+ return buildCatalogue({
13032
+ agent: await catalogueAgent(options, alias),
13033
+ machine: machineCapabilities(options),
13034
+ identityDefault: readIdentityDefaultBinding(options.store.identityDir(alias)),
13035
+ logger
13036
+ });
13037
+ }
12345
13038
  function registerCatalogueRoute(app, options, requireAuthorizedOrigin) {
12346
13039
  app.get("/v1/catalogue", {
12347
13040
  schema: AgentServerRouteSchemas.catalogue,
@@ -12350,34 +13043,42 @@ function registerCatalogueRoute(app, options, requireAuthorizedOrigin) {
12350
13043
  await requireAuthorizedOrigin(request);
12351
13044
  const { identity } = request.query ?? {};
12352
13045
  if (!identity || identity.trim().length === 0) throw new AgentServerHttpError(400, "invalid_query", "\"identity\" is required");
12353
- 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
- });
13046
+ return readIdentityCatalogue(options, identity.trim(), request.log);
12360
13047
  });
12361
13048
  }
12362
13049
  /** Resolve and verify each indexed team independently with its exact key. */
12363
13050
  async function defaultCatalogueAgent(options, alias) {
12364
13051
  const { config } = await loadAgentActivation(options.store, alias);
13052
+ const clients = /* @__PURE__ */ new Map();
13053
+ const verifiedClient = (teamId) => {
13054
+ const client = clients.get(teamId);
13055
+ if (!client) throw new Error("Team must be verified before project discovery");
13056
+ return client;
13057
+ };
12365
13058
  return {
12366
13059
  teamIds: Object.keys(config.agent_key_refs ?? {}),
12367
13060
  lastVerified: (teamId) => requireActivation(options.store, alias).credentialHealth?.[teamId],
12368
- readTeam: async (teamId) => {
12369
- const { client, metadata } = requireCredentialSnapshot(await verifyTeamActivation(options.store, alias, options.secretProviders, options.externalSecretProviders, void 0, options.shutdownSignal, teamId));
13061
+ readTeam: async (teamId, signal) => {
13062
+ const activated = await verifyTeamActivation(options.store, alias, options.secretProviders, options.externalSecretProviders, void 0, signal && options.shutdownSignal ? AbortSignal.any([signal, options.shutdownSignal]) : signal ?? options.shutdownSignal, teamId);
13063
+ signal?.throwIfAborted();
13064
+ const { client, metadata } = requireCredentialSnapshot(activated);
12370
13065
  const [team, diaries, profiles] = await Promise.all([
12371
13066
  client.teams.get(teamId),
12372
13067
  client.diaries.list(),
12373
13068
  client.runtimeProfiles.list({ teamId })
12374
13069
  ]);
13070
+ clients.set(teamId, client);
12375
13071
  return {
12376
13072
  team,
12377
13073
  diaries: diaries.items,
12378
13074
  profiles: profiles.items,
12379
13075
  credential: metadata
12380
13076
  };
13077
+ },
13078
+ readProjects: async (teamId) => readCatalogueProjects(verifiedClient(teamId).projects, teamId),
13079
+ readProject: async (teamId, projectId, signal) => {
13080
+ signal?.throwIfAborted();
13081
+ return readCatalogueProject(verifiedClient(teamId).projects, teamId, projectId);
12381
13082
  }
12382
13083
  };
12383
13084
  }
@@ -12400,7 +13101,7 @@ function machineCapabilities(options) {
12400
13101
  function registerStatusRoute(app, options, requireAuthorizedOrigin) {
12401
13102
  const { store, runs } = options;
12402
13103
  app.get("/v1/status", { schema: AgentServerRouteSchemas.status }, async (request) => {
12403
- await requireAuthorizedOrigin(request);
13104
+ const origin = await requireAuthorizedOrigin(request);
12404
13105
  const selected = selectedIdentity(store, options.activeIdentity);
12405
13106
  return {
12406
13107
  version: options.version,
@@ -12410,7 +13111,7 @@ function registerStatusRoute(app, options, requireAuthorizedOrigin) {
12410
13111
  identities: identityViews(store),
12411
13112
  ...selected ? { selectedIdentity: selected } : {},
12412
13113
  providers: options.providers.list(),
12413
- runs: await runViews(runs),
13114
+ runs: await runViews(runs, origin),
12414
13115
  runtimeSettings: options.runtimeSettings ?? DEFAULT_LOCAL_OPERATIONAL_SETTINGS
12415
13116
  };
12416
13117
  });
@@ -12535,11 +13236,30 @@ function registerProviderRoutes(app, options, requireAuthorizedOrigin) {
12535
13236
  return reply.code(204).send(null);
12536
13237
  });
12537
13238
  }
12538
- async function runViews(runs) {
12539
- return (await runs.listAsync(RUN_HISTORY_LIMIT)).map((record) => ({
12540
- ...record,
12541
- active: runs.isActive(record.id)
12542
- }));
13239
+ async function runViews(runs, origin) {
13240
+ return (await runs.listAsync(RUN_HISTORY_LIMIT)).map((record) => runView(record, runs.isActive(record.id), origin));
13241
+ }
13242
+ /**
13243
+ * Local folders are native-only, as project locations are: other origins see
13244
+ * the location name and project, never the path. The snapshot path is
13245
+ * daemon-internal for every origin.
13246
+ */
13247
+ function runView(record, active, origin) {
13248
+ const native = origin === NATIVE_CLIENT_ORIGIN;
13249
+ const { source, workspace, ...rest } = record;
13250
+ const view = {
13251
+ ...rest,
13252
+ ...native && source !== void 0 ? { source } : {},
13253
+ active
13254
+ };
13255
+ if (workspace) {
13256
+ const { configPath: _configPath, source: resolvedSource, ...shared } = workspace;
13257
+ view.workspace = {
13258
+ ...shared,
13259
+ ...native && resolvedSource !== void 0 ? { source: resolvedSource } : {}
13260
+ };
13261
+ }
13262
+ return view;
12543
13263
  }
12544
13264
  function registerSubscriptionRoutes(app, options, requireAuthorizedOrigin) {
12545
13265
  app.get("/v1/subscriptions", { schema: AgentServerRouteSchemas.listSubscriptions }, async (request) => {
@@ -12566,17 +13286,22 @@ function registerSubscriptionRoutes(app, options, requireAuthorizedOrigin) {
12566
13286
  function registerRunRoutes(app, options, requireAuthorizedOrigin) {
12567
13287
  const { runs } = options;
12568
13288
  app.get("/v1/runs", { schema: AgentServerRouteSchemas.listRuns }, async (request) => {
12569
- await requireAuthorizedOrigin(request);
12570
- return runViews(runs);
13289
+ return runViews(runs, await requireAuthorizedOrigin(request));
12571
13290
  });
12572
13291
  app.post("/v1/runs", {
12573
13292
  schema: AgentServerRouteSchemas.startRun,
12574
13293
  attachValidation: true
12575
13294
  }, async (request, reply) => {
12576
- await requireAuthorizedOrigin(request);
13295
+ const origin = await requireAuthorizedOrigin(request);
12577
13296
  const body = requireBody(request);
13297
+ if (NATIVE_RUN_FIELDS.some((key) => body[key] !== void 0 && body[key] !== null)) await requireNativeOrigin(async () => origin, request);
13298
+ if (request.validationError) throw new AgentServerHttpError(400, "invalid_spec", `Check the run fields: ${request.validationError.message}`);
12578
13299
  const diaryId = optionalString(body, "diaryId");
12579
13300
  const record = await runs.start({
13301
+ ...body.projectId === null ? { projectId: null } : body.projectId !== void 0 ? { projectId: requireString(body, "projectId") } : {},
13302
+ ...body.location !== void 0 ? { location: requireString(body, "location") } : {},
13303
+ ...body.source !== void 0 ? { source: requireString(body, "source") } : {},
13304
+ ...body.strategy !== void 0 ? { strategy: requireString(body, "strategy") } : {},
12580
13305
  agent: requireString(body, "agent"),
12581
13306
  teamId: requireString(body, "teamId"),
12582
13307
  ...diaryId ? { diaryId } : {},
@@ -12584,15 +13309,13 @@ function registerRunRoutes(app, options, requireAuthorizedOrigin) {
12584
13309
  taskTypes: stringArray(body, "taskTypes"),
12585
13310
  mode: requireString(body, "mode")
12586
13311
  }, requestOperationSignal(request, options.shutdownSignal));
12587
- return reply.code(201).send({
12588
- ...record,
12589
- active: runs.isActive(record.id)
12590
- });
13312
+ return reply.code(201).send(runView(record, runs.isActive(record.id), origin));
12591
13313
  });
12592
13314
  app.delete("/v1/runs/:runId", { schema: AgentServerRouteSchemas.stopRun }, async (request) => {
12593
- await requireAuthorizedOrigin(request);
13315
+ const origin = await requireAuthorizedOrigin(request);
12594
13316
  const { runId } = request.params;
12595
- return runs.stop(runId);
13317
+ const record = runs.stop(runId);
13318
+ return runView(record, runs.isActive(record.id), origin);
12596
13319
  });
12597
13320
  registerRunLogRoute(app, options, requireAuthorizedOrigin);
12598
13321
  }
@@ -12619,9 +13342,10 @@ function registerRunLogRoute(app, options, requireAuthorizedOrigin) {
12619
13342
  } }
12620
13343
  } }
12621
13344
  } }, async (request) => {
12622
- await requireAuthorizedOrigin(request);
13345
+ const origin = await requireAuthorizedOrigin(request);
12623
13346
  const { runId } = request.params;
12624
13347
  const record = runs.status(runId);
13348
+ const redact = localPathRedactor(record, origin, options);
12625
13349
  const handle = await open(store.resolveRunLogPath(record.id), constants.O_RDONLY | constants.O_NOFOLLOW);
12626
13350
  try {
12627
13351
  const state = {
@@ -12629,20 +13353,22 @@ function registerRunLogRoute(app, options, requireAuthorizedOrigin) {
12629
13353
  fragment: ""
12630
13354
  };
12631
13355
  const { lines, omitted } = await readAgentServerLogDelta(handle, state);
13356
+ const native = origin === NATIVE_CLIENT_ORIGIN;
12632
13357
  return { lines: [
12633
13358
  ...omitted ? ["[older log output omitted]"] : [],
12634
- ...lines,
12635
- ...state.fragment ? [state.fragment] : []
12636
- ] };
13359
+ ...omitted && !native ? lines.slice(1) : lines,
13360
+ ...state.fragment && (native || !runs.isActive(record.id)) ? [state.fragment] : []
13361
+ ].map(redact) };
12637
13362
  } finally {
12638
13363
  await handle.close();
12639
13364
  }
12640
13365
  });
12641
13366
  let openStreams = 0;
12642
13367
  app.get("/v1/runs/:runId/logs", { schema: AgentServerRouteSchemas.streamRunLogs }, async (request, reply) => {
12643
- await requireAuthorizedOrigin(request);
13368
+ const origin = await requireAuthorizedOrigin(request);
12644
13369
  const { runId } = request.params;
12645
13370
  const record = runs.status(runId);
13371
+ const redact = localPathRedactor(record, origin, options);
12646
13372
  store.resolveRunLogPath(record.id);
12647
13373
  if (openStreams >= MAX_LOG_STREAMS) throw new AgentServerHttpError(429, "rate_limited", "Too many concurrent log streams");
12648
13374
  openStreams += 1;
@@ -12683,12 +13409,12 @@ function registerRunLogRoute(app, options, requireAuthorizedOrigin) {
12683
13409
  });
12684
13410
  };
12685
13411
  const push = async () => {
12686
- if (request.headers.origin !== "moltnet-agent-desktop://native") await requireAuthorizedOrigin(request);
12687
13412
  const handle = await open(store.resolveRunLogPath(record.id), constants.O_RDONLY | constants.O_NOFOLLOW);
12688
13413
  try {
12689
13414
  const { lines, omitted } = await readAgentServerLogDelta(handle, readState);
12690
13415
  if (omitted) await writeData("[older log output omitted]");
12691
- for (const line of lines) await writeData(line);
13416
+ const complete = omitted && origin !== "moltnet-agent-desktop://native" ? lines.slice(1) : lines;
13417
+ for (const line of complete) await writeData(redact(line));
12692
13418
  } finally {
12693
13419
  await handle.close();
12694
13420
  }
@@ -12723,6 +13449,43 @@ function registerRunLogRoute(app, options, requireAuthorizedOrigin) {
12723
13449
  return reply;
12724
13450
  });
12725
13451
  }
13452
+ /**
13453
+ * Worker logs name local folders (the chosen source, state and HOME under the
13454
+ * store). Non-native origins get them replaced, as `runView` does for records.
13455
+ * The user's home directory is included so any other path under it cannot
13456
+ * reveal the OS account name.
13457
+ */
13458
+ /**
13459
+ * A service account's home can be `/` or `/root`; replacing it would rewrite
13460
+ * every slash in the log, so only a home with two or more segments counts.
13461
+ */
13462
+ function redactableHome() {
13463
+ const home = homedir();
13464
+ return home.split(/[\\/]/u).filter(Boolean).length >= 2 ? home : void 0;
13465
+ }
13466
+ function localPathRedactor(record, origin, options) {
13467
+ if (origin === "moltnet-agent-desktop://native") return (line) => line;
13468
+ const paths = /* @__PURE__ */ new Set();
13469
+ for (const path of [
13470
+ record.source,
13471
+ record.workspace?.source,
13472
+ options.store.root,
13473
+ options.connectionSettings?.root,
13474
+ redactableHome()
13475
+ ]) {
13476
+ if (!path) continue;
13477
+ const forms = [path];
13478
+ try {
13479
+ forms.push(realpathSync.native(path));
13480
+ } catch {}
13481
+ for (const form of forms) {
13482
+ paths.add(form);
13483
+ paths.add(JSON.stringify(form).slice(1, -1));
13484
+ }
13485
+ }
13486
+ const ordered = [...paths].sort((a, b) => b.length - a.length);
13487
+ return (line) => ordered.reduce((text, path) => text.split(path).join("<local path>"), line);
13488
+ }
12726
13489
  function corsHeadersFor(request, options) {
12727
13490
  const origin = request.headers.origin;
12728
13491
  if (isConfiguredOrigin(origin, options)) return {
@@ -12735,6 +13498,23 @@ function isConfiguredOrigin(origin, options) {
12735
13498
  return typeof origin === "string" && (origin === "moltnet-agent-desktop://native" || !options.nativeOnly && (options.allowedOrigins.includes(origin) || origin === options.selfOrigin));
12736
13499
  }
12737
13500
  function normalizeAgentServerError(error) {
13501
+ if (error instanceof ProjectConfigError) {
13502
+ if (error.kind === "version") return {
13503
+ statusCode: 409,
13504
+ code: "config_version",
13505
+ message: "The project locations file was written by a newer MoltNet. Update this app."
13506
+ };
13507
+ if (error.kind === "io") return {
13508
+ statusCode: 500,
13509
+ code: "config_unavailable",
13510
+ message: "The project locations file could not be read or written. Check its ownership and permissions."
13511
+ };
13512
+ return {
13513
+ statusCode: 400,
13514
+ code: "invalid_location",
13515
+ message: error.message
13516
+ };
13517
+ }
12738
13518
  if (error instanceof TeamCredentialError) return {
12739
13519
  statusCode: 400,
12740
13520
  code: error.blocker.code,
@@ -12819,7 +13599,7 @@ function nativeSocketValidationOptions(input) {
12819
13599
  ...input.cliAllowedOrigins ? { allowedOrigins: input.cliAllowedOrigins } : {}
12820
13600
  };
12821
13601
  }
12822
- async function runAgentServer(argv) {
13602
+ async function runAgentServer(argv, ports = {}) {
12823
13603
  if (isHelpFlag(argv)) {
12824
13604
  console.log(AGENT_SERVER_HELP);
12825
13605
  return 0;
@@ -12897,9 +13677,12 @@ async function runAgentServer(argv) {
12897
13677
  logger
12898
13678
  });
12899
13679
  const runtimeRegistry = new RuntimeRegistry(store.root);
13680
+ const supplied = ports.configure?.(store);
12900
13681
  const runs = new RunManager({
13682
+ ...supplied?.runOptions,
12901
13683
  store,
12902
13684
  storeRoot: settingsRoot,
13685
+ projectRoot: settingsRoot,
12903
13686
  secretProviders,
12904
13687
  externalSecretProviders,
12905
13688
  baseEnv: processEnvSnapshot(),
@@ -12909,16 +13692,17 @@ async function runAgentServer(argv) {
12909
13692
  });
12910
13693
  if (nativeSocket) await validateNativeSocket(nativeSocket);
12911
13694
  const selfOrigin = nativeSocket ? void 0 : `http://127.0.0.1:${port}`;
13695
+ const operatorOAuth = new OperatorOAuth({
13696
+ issuer: connection.issuer,
13697
+ authorizationUrl: new URL("/oauth2/auth", connection.publicUrl).href,
13698
+ tokenUrl: new URL("/oauth2/token", connection.publicUrl).href,
13699
+ jwksUrl: new URL("/.well-known/jwks.json", connection.publicUrl).href,
13700
+ nativeClientId: connection.nativeClientId,
13701
+ callbackPort: OPERATOR_OAUTH.callbackPort
13702
+ }, root);
12912
13703
  const app = buildAgentServer({
12913
- 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),
13704
+ ...supplied?.catalogueAgentFor ? { catalogueAgentFor: supplied.catalogueAgentFor } : {},
13705
+ operatorOAuth,
12922
13706
  nativeOnly: Boolean(nativeSocket),
12923
13707
  connectionSettings,
12924
13708
  operatorApiUrl: connection.apiUrl,
@@ -13403,7 +14187,7 @@ async function writeCache(cache) {
13403
14187
  }
13404
14188
  //#endregion
13405
14189
  //#region src/version.ts
13406
- var DAEMON_VERSION = "0.63.1";
14190
+ var DAEMON_VERSION = "0.64.0";
13407
14191
  //#endregion
13408
14192
  //#region src/cli.ts
13409
14193
  async function runAgentDaemonCli(options) {