@cmmd-center/forge 0.11.0 → 0.11.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/bin.mjs CHANGED
@@ -5,9 +5,9 @@ import { $ as encodeEffect, $a as make$39, $c as fromUndefinedOr, $i as spaced,
5
5
  import { $ as toAsyncIterable, A as make$45, At as make$44, Bt as publish, C as fromEffect, Ct as make$43, E as fromQueue, Et as end, F as merge$4, Ft as shutdown$1, G as runFold, H as run$2, Ht as subscribe$1, It as sliding, Jt as getCurrent, K as runForEach, Kt as makeUnsafe$7, Lt as take$1, M as mapAccum, Mt as offerAll, N as mapEffect$1, Nt as offerUnsafe, O as fromSubscription, Ot as failCause$4, Q as tick, R as onExit$1, Rt as takeAll, S as fromChannel, T as fromPubSub, Tt as bounded, U as runCollect, Ut as unbounded$1, V as provideContext$1, Vt as shutdown$2, W as runDrain, Y as succeed$4, Yt as interrupt$4, Z as takeWhile, Zt as runIn, _t as mapInput, at as make$46, b as flatMap$3, bt as fromTransform, c as callback$1, d as debounce$1, f as decodeText$1, g as ensuring$1, h as encodeText, it as get$4, j as map$7, jt as offer$1, k as isStream, l as catchCause$1, m as empty$5, mt as forEach$1, ot as get$5, pt as drain, q as runForEachArray, qt as await_, r as FileSystem, rt as unwrap$1, st as make$47, t as Path$2, u as concat$2, v as failCause$5, w as fromIterable$1, wt as makeUnsafe$6, x as fromAsyncIterable, y as filter$2, yt as fromSubscriptionArray, zt as unbounded$2 } from "./Path-C6Zb2WHp.mjs";
6
6
  import { _ as ChildProcessSpawner, a as Stdio, g as make$48, o as make$49, r as isQuitError } from "./NodeTerminal-BEGtoMSS.mjs";
7
7
  import { n as layer$7 } from "./NodeServices-CBNXOYLG.mjs";
8
- import { $ as file, A as SocketErrorReason, At as merge$5, Bt as set$2, Ct as HttpClientErrorSchema, Dt as empty$7, Ft as getAndSet, G as TracerPropagationEnabled, Ht as updateAndGet, I as HttpServerError, It as getUnsafe$2, J as transformResponse, K as filterStatusOk$1, Lt as make$50, Ot as fromInput$1, Pt as get$6, Q as empty$6, Rt as makeUnsafe$8, St as setBody, Tt as jsonUnsafe$1, U as layer$8, Vt as update$4, W as HttpClient, Z as symbol$1, _ as schemaBodyUrlParams, _t as acceptJson, a as withLogAddress, at as setCookie, bt as get$7, ct as stream, d as logger$1, dt as filterStatusOk, et as isHttpServerResponse, f as withLoggerDisabled, ft as matchStatus, g as schemaBodyJson, gt as schemaBodyJson$1, i as serve$1, it as schemaJson, kt as fromRecordUnsafe, lt as text$2, m as ParsedSearchParams, nt as raw, ot as setCookies, p as HttpServerRequest, q as retryTransient, rt as redirect, st as setHeader, t as HttpServer, tt as jsonUnsafe, u as cors, ut as uint8Array, v as schemaHeaders, vt as bearerToken, wt as isHttpClientError, xt as post, y as toURL, yt as bodyJson, z as RouteNotFound, zt as modify$3 } from "./HttpServer-DNGCTTdk.mjs";
8
+ import { $ as file, A as SocketErrorReason, At as merge$5, Bt as set$3, Ct as HttpClientErrorSchema, Dt as empty$7, Ft as getAndSet, G as TracerPropagationEnabled, Ht as updateAndGet, I as HttpServerError, It as getUnsafe$2, J as transformResponse, K as filterStatusOk$1, Lt as make$50, Ot as fromInput$1, Pt as get$6, Q as empty$6, Rt as makeUnsafe$8, St as setBody, Tt as jsonUnsafe$1, U as layer$8, Vt as update$4, W as HttpClient, Z as symbol$1, _ as schemaBodyUrlParams, _t as acceptJson, a as withLogAddress, at as setCookie, bt as get$7, ct as stream, d as logger$1, dt as filterStatusOk, et as isHttpServerResponse, f as withLoggerDisabled, ft as matchStatus, g as schemaBodyJson, gt as schemaBodyJson$1, i as serve$1, it as schemaJson, kt as fromRecordUnsafe, lt as text$2, m as ParsedSearchParams, nt as raw, ot as setCookies, p as HttpServerRequest, q as retryTransient, rt as redirect, st as setHeader, t as HttpServer, tt as jsonUnsafe, u as cors, ut as uint8Array, v as schemaHeaders, vt as bearerToken, wt as isHttpClientError, xt as post, y as toURL, yt as bodyJson, z as RouteNotFound, zt as modify$3 } from "./HttpServer-DNGCTTdk.mjs";
9
9
  import { _ as withDefault$3, a as all$1, c as logLevel, d as option$1, f as port, g as url$2, i as TrueValues, l as map$8, m as string$7, n as FalseValues, o as boolean$5, p as schema$2, r as Record$1, s as int$2, t as Boolean$2, u as number$2 } from "./Config-AqUR_lhX.mjs";
10
- import { a as make$51, i as keys, n as getOption, o as makeWith$1, r as invalidate, s as set$3, t as get$8 } from "./Cache-p0X77SKw.mjs";
10
+ import { a as make$51, i as keys, n as getOption, o as makeWith$1, r as invalidate, s as set$4, t as get$8 } from "./Cache-p0X77SKw.mjs";
11
11
  import { S as layer$9, _ as defaultEscape, a as DeadlockError, b as makeCompiler$1, c as SqlError, d as UnknownError, g as custom, h as make$52, i as ConstraintError, l as SqlSyntaxError, m as SqlClient, n as AuthorizationError, o as LockTimeoutError, r as ConnectionError, s as SerializationError, t as AuthenticationError, u as StatementTimeoutError, v as defaultTransforms, y as fragment } from "./SqlError-cyJT2IRY.mjs";
12
12
  import { r as wrapper_default, t as Mime_default } from "./Mime-Dq9IoNiV.mjs";
13
13
  import { a as diag, c as SpanKind$1, d as TraceFlags, f as ROOT_CONTEXT, i as propagation, l as isSpanContextValid, m as DiagLogLevel, n as init_esm$1, o as context$1, p as createContextKey, r as trace, s as SpanStatusCode, t as esm_exports$1 } from "./esm-wLIF-u3N.mjs";
@@ -15750,7 +15750,7 @@ const get$1 = (self) => modify$1(self, (current) => [current, current]);
15750
15750
  * })
15751
15751
  * ```
15752
15752
  */
15753
- const set$1 = /* @__PURE__ */ dual(2, (self, value) => update$1(self, () => value));
15753
+ const set$2 = /* @__PURE__ */ dual(2, (self, value) => update$1(self, () => value));
15754
15754
  //#endregion
15755
15755
  //#region ../../node_modules/.bun/effect@4.0.0-beta.51/node_modules/effect/dist/TxChunk.js
15756
15756
  /**
@@ -15939,7 +15939,7 @@ const get = (self) => get$1(self.ref);
15939
15939
  * @since 4.0.0
15940
15940
  * @category combinators
15941
15941
  */
15942
- const set = /* @__PURE__ */ dual(2, (self, chunk) => set$1(self.ref, chunk));
15942
+ const set$1 = /* @__PURE__ */ dual(2, (self, chunk) => set$2(self.ref, chunk));
15943
15943
  /**
15944
15944
  * Appends an element to the end of the `TxChunk`.
15945
15945
  *
@@ -16187,7 +16187,7 @@ const take = (self) => gen(function* () {
16187
16187
  const head$3 = head(yield* get(self.items));
16188
16188
  if (isNone(head$3)) return yield* txRetry;
16189
16189
  yield* drop(self.items, 1);
16190
- if (state._tag === "Closing" && (yield* isEmpty(self))) yield* set$1(self.stateRef, {
16190
+ if (state._tag === "Closing" && (yield* isEmpty(self))) yield* set$2(self.stateRef, {
16191
16191
  _tag: "Done",
16192
16192
  cause: state.cause
16193
16193
  });
@@ -16285,11 +16285,11 @@ const interrupt = (self) => withFiber$1((fiber) => failCause(self, interrupt$2(f
16285
16285
  */
16286
16286
  const failCause = /* @__PURE__ */ dual(2, (self, cause) => gen(function* () {
16287
16287
  if ((yield* get$1(self.stateRef))._tag !== "Open") return false;
16288
- if (yield* isEmpty(self)) yield* set$1(self.stateRef, {
16288
+ if (yield* isEmpty(self)) yield* set$2(self.stateRef, {
16289
16289
  _tag: "Done",
16290
16290
  cause
16291
16291
  });
16292
- else yield* set$1(self.stateRef, {
16292
+ else yield* set$2(self.stateRef, {
16293
16293
  _tag: "Closing",
16294
16294
  cause
16295
16295
  });
@@ -16331,7 +16331,7 @@ const clear = (self) => gen(function* () {
16331
16331
  return yield* failCause$1(state.cause);
16332
16332
  }
16333
16333
  const chunk = yield* get(self.items);
16334
- yield* set(self.items, empty$2());
16334
+ yield* set$1(self.items, empty$2());
16335
16335
  return toArray(chunk);
16336
16336
  }).pipe(tx$1);
16337
16337
  /**
@@ -26406,6 +26406,29 @@ function encodeCmmdRuntimeThreadLifecycleMetadata(lifecycle) {
26406
26406
  } };
26407
26407
  }
26408
26408
  //#endregion
26409
+ //#region ../../packages/contracts/src/cmmdRuntimeThreadWorkspace.ts
26410
+ function encodeCmmdRuntimeThreadWorkspaceMetadata(workspace) {
26411
+ return { forgeWorkspace: {
26412
+ schemaVersion: 1,
26413
+ worktreePath: workspace.worktreePath
26414
+ } };
26415
+ }
26416
+ //#endregion
26417
+ //#region ../../packages/contracts/src/cmmdRuntimeToolActivity.ts
26418
+ function isRecord$22(value) {
26419
+ return value !== null && typeof value === "object" && !Array.isArray(value);
26420
+ }
26421
+ function encodeCmmdRuntimeToolActivityPayload(kind, payload) {
26422
+ return {
26423
+ ...isRecord$22(payload) ? payload : { sourcePayload: payload },
26424
+ forgeToolActivity: {
26425
+ schemaVersion: 1,
26426
+ source: "native",
26427
+ kind
26428
+ }
26429
+ };
26430
+ }
26431
+ //#endregion
26409
26432
  //#region ../../packages/contracts/src/cmmdProjectPatch.ts
26410
26433
  const CMMD_PROJECT_PATCH_FIELDS = {
26411
26434
  title: "requiredText",
@@ -65495,7 +65518,7 @@ function normalizeNumberish(value) {
65495
65518
  }
65496
65519
  //#endregion
65497
65520
  //#region package.json
65498
- var version$1 = "0.11.0";
65521
+ var version$1 = "0.11.1";
65499
65522
  //#endregion
65500
65523
  //#region src/sentry.ts
65501
65524
  const SERVER_APP_NAME = "forge-server";
@@ -69266,6 +69289,17 @@ const browserApiCorsLayer = middleware(cors({
69266
69289
  credentials: true,
69267
69290
  maxAge: 600
69268
69291
  }), { global: true });
69292
+ /**
69293
+ * An /api path that reaches the static catch-all is not implemented by this
69294
+ * server: every real API route is registered ahead of it. Serving the web app's
69295
+ * index.html for those made API clients decode an HTML document as JSON, which
69296
+ * is how the phone's shell snapshot fetch failed with a JSON parse error on
69297
+ * every connect (FOR-36611). Callers can act on a 404; they cannot act on a
69298
+ * document that claims to be their payload.
69299
+ */
69300
+ function isApiRequestPath(pathname) {
69301
+ return pathname === "/api" || pathname.startsWith("/api/");
69302
+ }
69269
69303
  function resolveDevRedirectUrl(devUrl, requestUrl) {
69270
69304
  const redirectUrl = new URL(devUrl.toString());
69271
69305
  redirectUrl.pathname = requestUrl.pathname;
@@ -69387,6 +69421,10 @@ const projectFaviconRouteLayer = add$1("GET", "/api/project-favicon", gen(functi
69387
69421
  const staticAndDevRouteLayer = add$1("GET", "*", gen(function* () {
69388
69422
  const url = toURL(yield* HttpServerRequest);
69389
69423
  if (isNone(url)) return text$2("Bad Request", { status: 400 });
69424
+ if (isApiRequestPath(url.value.pathname)) return jsonUnsafe({
69425
+ error: "not_found",
69426
+ message: `This server does not serve ${url.value.pathname}.`
69427
+ }, { status: 404 });
69390
69428
  const config = yield* ServerConfig;
69391
69429
  if (config.devUrl && isLoopbackHostname(url.value.hostname)) return redirect(resolveDevRedirectUrl(config.devUrl, url.value), { status: 302 });
69392
69430
  const staticDir = config.staticDir ?? (config.devUrl ? yield* resolveStaticDir() : void 0);
@@ -82637,7 +82675,7 @@ var ServerSettingsService = class ServerSettingsService extends Service$1()("for
82637
82675
  start: void_$1,
82638
82676
  ready: void_$1,
82639
82677
  getSettings: get$6(currentSettingsRef),
82640
- updateSettings: (patch) => get$6(currentSettingsRef).pipe(map$3((currentSettings) => deepMerge$1(currentSettings, patch)), tap((nextSettings) => set$2(currentSettingsRef, nextSettings))),
82678
+ updateSettings: (patch) => get$6(currentSettingsRef).pipe(map$3((currentSettings) => deepMerge$1(currentSettings, patch)), tap((nextSettings) => set$3(currentSettingsRef, nextSettings))),
82641
82679
  streamChanges: empty$5
82642
82680
  };
82643
82681
  }));
@@ -82777,7 +82815,7 @@ const ServerSettingsLive = effect(ServerSettingsService, gen(function* () {
82777
82815
  cause
82778
82816
  })));
82779
82817
  yield* writeSettingsAtomically(next);
82780
- yield* set$3(settingsCache, cacheKey, next);
82818
+ yield* set$4(settingsCache, cacheKey, next);
82781
82819
  yield* emitChange(next);
82782
82820
  return resolveTextGenerationProvider(next);
82783
82821
  })),
@@ -91707,7 +91745,7 @@ const KeybindingsLive = effect(Keybindings, gen(function* () {
91707
91745
  return {
91708
91746
  start: gen(function* () {
91709
91747
  if (yield* get$6(startedRef)) return yield* _await(startedDeferred);
91710
- yield* set$2(startedRef, true);
91748
+ yield* set$3(startedRef, true);
91711
91749
  const startupExit = yield* exit(gen(function* () {
91712
91750
  yield* startWatcher;
91713
91751
  yield* syncDefaultKeybindingsOnStartup;
@@ -91736,7 +91774,7 @@ const KeybindingsLive = effect(Keybindings, gen(function* () {
91736
91774
  });
91737
91775
  yield* writeConfigAtomically(cappedConfig);
91738
91776
  const nextResolved = mergeWithDefaultKeybindings(compileResolvedKeybindingsConfig(cappedConfig));
91739
- yield* set$3(resolvedConfigCache, resolvedConfigCacheKey, {
91777
+ yield* set$4(resolvedConfigCache, resolvedConfigCacheKey, {
91740
91778
  keybindings: nextResolved,
91741
91779
  issues: []
91742
91780
  });
@@ -91750,7 +91788,7 @@ const KeybindingsLive = effect(Keybindings, gen(function* () {
91750
91788
  const nextConfig = (yield* loadWritableCustomKeybindingsConfig()).filter((entry) => entry.command !== command);
91751
91789
  yield* writeConfigAtomically(nextConfig);
91752
91790
  const nextResolved = mergeWithDefaultKeybindings(compileResolvedKeybindingsConfig(nextConfig));
91753
- yield* set$3(resolvedConfigCache, resolvedConfigCacheKey, {
91791
+ yield* set$4(resolvedConfigCache, resolvedConfigCacheKey, {
91754
91792
  keybindings: nextResolved,
91755
91793
  issues: []
91756
91794
  });
@@ -95620,10 +95658,13 @@ function serializeRuntimeInventory(snapshot, providerInventory) {
95620
95658
  workerRole: thread.workerRole,
95621
95659
  anchorKind: thread.anchorKind,
95622
95660
  anchorCmmdId: thread.anchorCmmdId,
95623
- metadata: encodeCmmdRuntimeThreadLifecycleMetadata({
95624
- settledAt: thread.settledAt ?? null,
95625
- settledOverride: thread.settledOverride ?? null
95626
- })
95661
+ metadata: {
95662
+ ...encodeCmmdRuntimeThreadLifecycleMetadata({
95663
+ settledAt: thread.settledAt ?? null,
95664
+ settledOverride: thread.settledOverride ?? null
95665
+ }),
95666
+ ...encodeCmmdRuntimeThreadWorkspaceMetadata({ worktreePath: thread.worktreePath })
95667
+ }
95627
95668
  })),
95628
95669
  metadata: {
95629
95670
  source: "forge-server-projection",
@@ -95690,7 +95731,7 @@ function serializeNativeToolActivity(activity) {
95690
95731
  kind: "tool_call",
95691
95732
  summary,
95692
95733
  status: activityStatus(activity),
95693
- ...activity.payload === void 0 ? {} : { payload: activity.payload }
95734
+ payload: encodeCmmdRuntimeToolActivityPayload(activity.kind, activity.payload)
95694
95735
  }
95695
95736
  };
95696
95737
  }
@@ -97852,8 +97893,8 @@ const runUsageScanWarmup = (options) => gen(function* () {
97852
97893
  return yield* forever$1(gen(function* () {
97853
97894
  if (yield* get$6(scanning)) yield* logInfo("usage scan warm-up skipped; the previous scan is still running");
97854
97895
  else {
97855
- yield* set$2(scanning, true);
97856
- yield* forkChild(scanOnce(options, now).pipe(ensuring(set$2(scanning, false))));
97896
+ yield* set$3(scanning, true);
97897
+ yield* forkChild(scanOnce(options, now).pipe(ensuring(set$3(scanning, false))));
97857
97898
  }
97858
97899
  yield* sleep(intervalMs);
97859
97900
  }));
@@ -97871,11 +97912,11 @@ const makeCommandGate = gen(function* () {
97871
97912
  return {
97872
97913
  awaitCommandReady: _await(commandReady),
97873
97914
  signalCommandReady: gen(function* () {
97874
- yield* set$2(commandReadinessState, "ready");
97915
+ yield* set$3(commandReadinessState, "ready");
97875
97916
  yield* succeed$3(commandReady, void 0).pipe(orDie);
97876
97917
  }),
97877
97918
  failCommandReady: (error) => gen(function* () {
97878
- yield* set$2(commandReadinessState, error);
97919
+ yield* set$3(commandReadinessState, error);
97879
97920
  yield* fail$3(commandReady, error).pipe(orDie);
97880
97921
  }),
97881
97922
  enqueueCommand: (effect) => gen(function* () {
@@ -98256,413 +98297,23 @@ function browseDirectory(input) {
98256
98297
  });
98257
98298
  }
98258
98299
  //#endregion
98259
- //#region src/workspace/Services/WorkspaceEntries.ts
98300
+ //#region src/pathWithinRoot.ts
98260
98301
  /**
98261
- * WorkspaceEntries - Effect service contract for cached workspace entry search.
98302
+ * True when `candidate` is `root` itself or lies below it, judged on the path
98303
+ * strings alone. The path from `root` to `candidate` is taken with
98304
+ * `path.relative` and normalised to forward slashes, and the candidate is
98305
+ * outside when that path climbs out (`..` or `../...`) or is absolute, which is
98306
+ * what `relative` returns across Windows drives. A child whose name only starts
98307
+ * with two dots, such as `..cache`, is inside. Symlinks are not followed, so a
98308
+ * caller that cares where a link points has to compare resolved paths.
98262
98309
  *
98263
- * Owns indexed workspace entry search plus cache invalidation for workspace
98264
- * roots when the underlying filesystem changes.
98265
- *
98266
- * @module WorkspaceEntries
98267
- */
98268
- var WorkspaceEntriesError = class extends TaggedErrorClass()("WorkspaceEntriesError", {
98269
- cwd: String$1,
98270
- operation: String$1,
98271
- detail: String$1,
98272
- cause: optional$4(Defect)
98273
- }) {};
98274
- /**
98275
- * WorkspaceEntries - Service tag for cached workspace entry search.
98276
- */
98277
- var WorkspaceEntries = class extends Service$1()("forge/workspace/Services/WorkspaceEntries") {};
98278
- //#endregion
98279
- //#region src/workspace/Services/WorkspaceBulkTransfer.ts
98280
- var WorkspaceBulkTransfer = class extends Service$1()("forge/workspace/Services/WorkspaceBulkTransfer") {};
98281
- //#endregion
98282
- //#region src/workspace/Layers/WorkspaceBulkTransfer.ts
98283
- const TRANSFER_TTL_MS = 1440 * 60 * 1e3;
98284
- const TRANSFER_LOCK_STRIPE_COUNT = 64;
98285
- function transferError(input) {
98286
- return new BulkTransferError({
98287
- code: input.code,
98288
- message: input.message,
98289
- transferId: input.transferId ?? null,
98290
- retryable: input.retryable ?? false,
98291
- ...input.cause === void 0 ? {} : { cause: input.cause }
98292
- });
98293
- }
98294
- function mapFailure(operation, transferId) {
98295
- return (cause) => is$1(BulkTransferError)(cause) ? cause : transferError({
98296
- code: "io_error",
98297
- message: `Workspace bulk transfer failed during ${operation}.`,
98298
- ...transferId === void 0 ? {} : { transferId },
98299
- retryable: true,
98300
- cause
98301
- });
98302
- }
98303
- /**
98304
- * Exported so the unary file read in `projectFile/readProjectFile.ts` uses this
98305
- * containment check rather than a second copy of it. Two copies of a security
98306
- * rule in one repo is how they drift, and the drift is silent.
98310
+ * Only a type import, so a module that has to stay free of Effect services can
98311
+ * use it without importing a Layer.
98307
98312
  */
98308
98313
  function isPathWithinRoot(path, root, candidate) {
98309
98314
  const relative = path.relative(root, candidate).replaceAll("\\", "/");
98310
98315
  return relative === "" || !relative.startsWith("../") && relative !== ".." && !path.isAbsolute(relative);
98311
98316
  }
98312
- function transferLockStripe(transferId) {
98313
- let hash = 2166136261;
98314
- for (let index = 0; index < transferId.length; index += 1) hash = Math.imul(hash ^ transferId.charCodeAt(index), 16777619);
98315
- return (hash >>> 0) % TRANSFER_LOCK_STRIPE_COUNT;
98316
- }
98317
- const makeWorkspaceBulkTransfer = (options = {}) => gen(function* () {
98318
- const fileSystem = yield* FileSystem;
98319
- const path = yield* Path$2;
98320
- const sql = yield* SqlClient;
98321
- const workspacePaths = yield* WorkspacePaths;
98322
- const workspaceEntries = yield* WorkspaceEntries;
98323
- const fault = options.fault ?? (() => void_$1);
98324
- const transferLocks = yield* forEach(Array.from({ length: TRANSFER_LOCK_STRIPE_COUNT }), () => make$43(1));
98325
- const withTransferLock = (transferId, effect) => transferLocks[transferLockStripe(transferId)].withPermit(effect);
98326
- const findTransfer = (transferId) => sql`
98327
- SELECT transfer_id AS "transferId", workspace_root AS "workspaceRoot",
98328
- relative_path AS "relativePath", target_path AS "targetPath", temp_path AS "tempPath",
98329
- total_bytes AS "totalBytes", whole_sha256 AS "wholeSha256",
98330
- next_offset AS "nextOffset", state, expires_at AS "expiresAt"
98331
- FROM workspace_bulk_transfers WHERE transfer_id = ${transferId} LIMIT 1
98332
- `.pipe(map$3((rows) => rows[0] ?? null));
98333
- const requireTransfer = (transferId) => findTransfer(transferId).pipe(flatMap((row) => row === null ? fail(transferError({
98334
- code: "transfer_not_found",
98335
- message: "Bulk transfer does not exist.",
98336
- transferId
98337
- })) : succeed(row)));
98338
- const requireOpenTransfer = (transferId) => requireTransfer(transferId).pipe(flatMap((row) => {
98339
- if (row.state === "cancelled") return fail(transferError({
98340
- code: "transfer_cancelled",
98341
- message: "Bulk transfer was cancelled.",
98342
- transferId
98343
- }));
98344
- if (row.state === "expired" || Date.parse(row.expiresAt) <= Date.now()) return fail(transferError({
98345
- code: "transfer_expired",
98346
- message: "Bulk transfer expired.",
98347
- transferId
98348
- }));
98349
- return succeed(row);
98350
- }));
98351
- const resolveTarget = fn$1("WorkspaceBulkTransfer.resolveTarget")(function* (input) {
98352
- const workspaceRoot = yield* workspacePaths.normalizeWorkspaceRoot(input.cwd);
98353
- const resolved = yield* workspacePaths.resolveRelativePathWithinRoot({
98354
- workspaceRoot,
98355
- relativePath: input.relativePath
98356
- });
98357
- const canonicalRoot = yield* fileSystem.realPath(workspaceRoot);
98358
- const parentSegments = resolved.relativePath.split("/").slice(0, -1);
98359
- let canonicalParent = canonicalRoot;
98360
- for (const segment of parentSegments) {
98361
- const candidate = path.join(canonicalParent, segment);
98362
- const canonicalCandidate = yield* fileSystem.realPath(candidate).pipe(catch_(() => input.createParent ? fileSystem.makeDirectory(candidate).pipe(flatMap(() => fileSystem.realPath(candidate))) : fileSystem.realPath(candidate)));
98363
- if (!isPathWithinRoot(path, canonicalRoot, canonicalCandidate)) return yield* transferError({
98364
- code: "symlink_escape",
98365
- message: "Workspace file path resolves outside the project root."
98366
- });
98367
- canonicalParent = canonicalCandidate;
98368
- }
98369
- const targetPath = path.join(canonicalParent, path.basename(resolved.relativePath));
98370
- const targetRealPath = yield* fileSystem.realPath(targetPath).pipe(catch_(() => succeed(null)));
98371
- if (targetRealPath !== null && !isPathWithinRoot(path, canonicalRoot, targetRealPath)) return yield* transferError({
98372
- code: "symlink_escape",
98373
- message: "Workspace file target resolves outside the project root."
98374
- });
98375
- return {
98376
- workspaceRoot: canonicalRoot,
98377
- relativePath: resolved.relativePath,
98378
- targetPath,
98379
- canonicalParent
98380
- };
98381
- });
98382
- const readFile = (input) => unwrap$1(gen(function* () {
98383
- const target = yield* resolveTarget({
98384
- ...input,
98385
- createParent: false
98386
- });
98387
- const info = yield* fileSystem.stat(target.targetPath);
98388
- const maxBytes = input.maxBytes ?? 16777216;
98389
- if (Number(info.size) > maxBytes) return yield* transferError({
98390
- code: "size_limit_exceeded",
98391
- message: `Workspace file exceeds the ${maxBytes}-byte read limit.`
98392
- });
98393
- const bytes = yield* fileSystem.readFile(target.targetPath);
98394
- const transfer = yield* promise(() => chunkBulkTransfer(bytes));
98395
- const transferId = transfer.chunks[0]?.transferId ?? crypto.randomUUID().replaceAll("-", "_");
98396
- return fromIterable$1([
98397
- {
98398
- kind: "started",
98399
- transferId,
98400
- totalBytes: transfer.totalBytes,
98401
- sha256: transfer.sha256
98402
- },
98403
- ...transfer.chunks.map((chunk) => ({
98404
- ...chunk,
98405
- transferId
98406
- })),
98407
- {
98408
- kind: "completed",
98409
- transferId,
98410
- totalBytes: transfer.totalBytes,
98411
- sha256: transfer.sha256
98412
- }
98413
- ]);
98414
- }).pipe(mapError(mapFailure("read file"))));
98415
- const resumeWrite = fn$1("WorkspaceBulkTransfer.resumeWrite")(function* (input, existing) {
98416
- const target = yield* resolveTarget({
98417
- ...input,
98418
- createParent: true
98419
- });
98420
- if (!(existing.workspaceRoot === target.workspaceRoot && existing.relativePath === target.relativePath && existing.totalBytes === input.totalBytes && existing.wholeSha256 === input.sha256)) return yield* transferError({
98421
- code: "transfer_identity_conflict",
98422
- message: "Bulk transfer ID is already bound to different content or a different path.",
98423
- transferId: input.transferId
98424
- });
98425
- if (existing.state === "cancelled") return yield* transferError({
98426
- code: "transfer_cancelled",
98427
- message: "Bulk transfer was cancelled.",
98428
- transferId: input.transferId
98429
- });
98430
- if (existing.state === "expired" || Date.parse(existing.expiresAt) <= Date.now()) return yield* transferError({
98431
- code: "transfer_expired",
98432
- message: "Bulk transfer expired.",
98433
- transferId: input.transferId
98434
- });
98435
- return {
98436
- transferId: input.transferId,
98437
- nextOffset: existing.nextOffset,
98438
- state: existing.state === "completed" ? "completed" : "open"
98439
- };
98440
- });
98441
- const createWrite = fn$1("WorkspaceBulkTransfer.createWrite")(function* (input) {
98442
- const target = yield* resolveTarget({
98443
- ...input,
98444
- createParent: true
98445
- }).pipe(mapError((cause) => is$1(WorkspacePathOutsideRootError)(cause) ? transferError({
98446
- code: "path_outside_workspace",
98447
- message: "Workspace file path must stay within the project root.",
98448
- transferId: input.transferId,
98449
- cause
98450
- }) : cause));
98451
- const tempPath = path.join(target.canonicalParent, `.forge-transfer-${input.transferId}.part`);
98452
- yield* scoped(fileSystem.open(tempPath, {
98453
- flag: "wx",
98454
- mode: 384
98455
- }).pipe(flatMap((file) => file.sync), catch_(() => fileSystem.stat(tempPath).pipe(flatMap((stat) => Number(stat.size) === 0 ? void_$1 : fail(transferError({
98456
- code: "transfer_identity_conflict",
98457
- message: "Bulk transfer temporary path already contains data.",
98458
- transferId: input.transferId
98459
- })))))));
98460
- const now = /* @__PURE__ */ new Date();
98461
- yield* sql`
98462
- INSERT INTO workspace_bulk_transfers (
98463
- transfer_id, workspace_root, relative_path, target_path, temp_path, total_bytes,
98464
- whole_sha256, next_offset, state, expires_at, created_at, updated_at
98465
- ) VALUES (
98466
- ${input.transferId}, ${target.workspaceRoot}, ${target.relativePath}, ${target.targetPath},
98467
- ${tempPath}, ${input.totalBytes}, ${input.sha256}, 0, 'open',
98468
- ${new Date(now.getTime() + TRANSFER_TTL_MS).toISOString()}, ${now.toISOString()},
98469
- ${now.toISOString()}
98470
- )
98471
- `;
98472
- return {
98473
- transferId: input.transferId,
98474
- nextOffset: 0,
98475
- state: "open"
98476
- };
98477
- });
98478
- const beginWriteEffect = fn$1("WorkspaceBulkTransfer.beginWrite")(function* (input) {
98479
- const existing = yield* findTransfer(input.transferId);
98480
- return yield* existing === null ? createWrite(input) : resumeWrite(input, existing);
98481
- });
98482
- const beginWrite = (input) => withTransferLock(input.transferId, beginWriteEffect(input)).pipe(mapError(mapFailure("begin write", input.transferId)));
98483
- const acceptRetriedChunk = fn$1("WorkspaceBulkTransfer.acceptRetriedChunk")(function* (input, row) {
98484
- const accepted = (yield* sql`
98485
- SELECT chunk_offset AS "chunkOffset", chunk_bytes AS "chunkBytes",
98486
- chunk_sha256 AS "chunkSha256"
98487
- FROM workspace_bulk_transfer_chunks
98488
- WHERE transfer_id = ${input.transferId} AND chunk_offset = ${input.offset}
98489
- LIMIT 1
98490
- `)[0];
98491
- if (!(accepted !== void 0 && accepted.chunkBytes === input.bytes && accepted.chunkSha256 === input.sha256)) return yield* transferError({
98492
- code: "integrity_mismatch",
98493
- message: "Retried bulk transfer chunk differs from the accepted chunk.",
98494
- transferId: input.transferId
98495
- });
98496
- return {
98497
- transferId: input.transferId,
98498
- nextOffset: row.nextOffset,
98499
- duplicate: true
98500
- };
98501
- });
98502
- const validateChunkRange = fn$1("WorkspaceBulkTransfer.validateChunkRange")(function* (input, row, byteLength) {
98503
- if (input.offset !== row.nextOffset) return yield* transferError({
98504
- code: "offset_mismatch",
98505
- message: `Expected chunk offset ${row.nextOffset}, received ${input.offset}.`,
98506
- transferId: input.transferId,
98507
- retryable: true
98508
- });
98509
- const nextOffset = input.offset + byteLength;
98510
- if (nextOffset > row.totalBytes) return yield* transferError({
98511
- code: "size_limit_exceeded",
98512
- message: "Bulk transfer chunk exceeds the declared total byte count.",
98513
- transferId: input.transferId
98514
- });
98515
- return nextOffset;
98516
- });
98517
- const reconcileChunkBytes = fn$1("WorkspaceBulkTransfer.reconcileChunkBytes")(function* (input, row, bytes, nextOffset) {
98518
- const tempInfo = yield* fileSystem.stat(row.tempPath);
98519
- if (Number(tempInfo.size) > row.nextOffset) {
98520
- if (Number(tempInfo.size) !== nextOffset) return yield* transferError({
98521
- code: "integrity_mismatch",
98522
- message: "Bulk transfer temporary file cannot be reconciled with the next chunk.",
98523
- transferId: input.transferId
98524
- });
98525
- const recovered = (yield* fileSystem.readFile(row.tempPath)).slice(input.offset, nextOffset);
98526
- if ((yield* promise(() => sha256Hex(recovered))) !== input.sha256) return yield* transferError({
98527
- code: "integrity_mismatch",
98528
- message: "Bulk transfer temporary bytes differ from the retried chunk.",
98529
- transferId: input.transferId
98530
- });
98531
- return;
98532
- }
98533
- yield* fault("before-byte-write");
98534
- yield* scoped(fileSystem.open(row.tempPath, { flag: "r+" }).pipe(flatMap((file) => file.seek(input.offset, "start").pipe(flatMap(() => file.writeAll(bytes)), flatMap(() => file.sync)))));
98535
- yield* fault("after-byte-write-before-metadata");
98536
- });
98537
- const persistAcceptedChunk = fn$1("WorkspaceBulkTransfer.persistAcceptedChunk")(function* (input, nextOffset) {
98538
- const updatedAt = (/* @__PURE__ */ new Date()).toISOString();
98539
- yield* sql.withTransaction(sql`
98540
- INSERT INTO workspace_bulk_transfer_chunks (
98541
- transfer_id, chunk_offset, chunk_bytes, chunk_sha256
98542
- ) VALUES (${input.transferId}, ${input.offset}, ${input.bytes}, ${input.sha256})
98543
- ON CONFLICT DO NOTHING
98544
- `.pipe(flatMap(() => sql`
98545
- UPDATE workspace_bulk_transfers
98546
- SET next_offset = ${nextOffset}, updated_at = ${updatedAt}
98547
- WHERE transfer_id = ${input.transferId} AND next_offset = ${input.offset} AND state = 'open'
98548
- `)));
98549
- yield* fault("after-metadata-advance");
98550
- });
98551
- const writeChunkEffect = fn$1("WorkspaceBulkTransfer.writeChunk")(function* (input) {
98552
- const bytes = yield* promise(() => verifyBulkTransferChunk({
98553
- ...input,
98554
- kind: "chunk"
98555
- }));
98556
- const row = yield* requireOpenTransfer(input.transferId);
98557
- if (row.state === "completed") return yield* transferError({
98558
- code: "offset_mismatch",
98559
- message: "Bulk transfer is already complete.",
98560
- transferId: input.transferId
98561
- });
98562
- if (input.offset < row.nextOffset) return yield* acceptRetriedChunk(input, row);
98563
- const nextOffset = yield* validateChunkRange(input, row, bytes.byteLength);
98564
- yield* reconcileChunkBytes(input, row, bytes, nextOffset);
98565
- yield* persistAcceptedChunk(input, nextOffset);
98566
- return {
98567
- transferId: input.transferId,
98568
- nextOffset,
98569
- duplicate: false
98570
- };
98571
- });
98572
- const writeChunk = (input) => withTransferLock(input.transferId, writeChunkEffect(input)).pipe(mapError(mapFailure("write chunk", input.transferId)));
98573
- const commitWriteEffect = fn$1("WorkspaceBulkTransfer.commitWrite")(function* (input) {
98574
- const row = yield* requireOpenTransfer(input.transferId);
98575
- if (row.state === "completed") return {
98576
- transferId: row.transferId,
98577
- relativePath: row.relativePath,
98578
- totalBytes: row.totalBytes,
98579
- sha256: row.wholeSha256
98580
- };
98581
- if (row.nextOffset !== row.totalBytes) return yield* transferError({
98582
- code: "offset_mismatch",
98583
- message: `Bulk transfer is incomplete at byte ${row.nextOffset} of ${row.totalBytes}.`,
98584
- transferId: row.transferId,
98585
- retryable: true
98586
- });
98587
- if ((yield* resolveTarget({
98588
- cwd: row.workspaceRoot,
98589
- relativePath: row.relativePath,
98590
- createParent: false
98591
- })).targetPath !== row.targetPath) return yield* transferError({
98592
- code: "symlink_escape",
98593
- message: "Workspace file destination changed during transfer.",
98594
- transferId: row.transferId
98595
- });
98596
- const tempInfo = yield* fileSystem.stat(row.tempPath).pipe(catch_(() => succeed(null)));
98597
- const candidatePath = tempInfo === null ? row.targetPath : row.tempPath;
98598
- const candidate = yield* fileSystem.readFile(candidatePath);
98599
- if (candidate.byteLength !== row.totalBytes || (yield* promise(() => sha256Hex(candidate))) !== row.wholeSha256) return yield* transferError({
98600
- code: "integrity_mismatch",
98601
- message: "Bulk transfer payload does not match its declared size and digest.",
98602
- transferId: row.transferId
98603
- });
98604
- if (tempInfo !== null) {
98605
- yield* fileSystem.rename(row.tempPath, row.targetPath);
98606
- yield* fault("after-atomic-rename");
98607
- }
98608
- yield* sql`
98609
- UPDATE workspace_bulk_transfers SET state = 'completed', updated_at = ${(/* @__PURE__ */ new Date()).toISOString()}
98610
- WHERE transfer_id = ${row.transferId}
98611
- `;
98612
- yield* workspaceEntries.invalidate(row.workspaceRoot);
98613
- return {
98614
- transferId: row.transferId,
98615
- relativePath: row.relativePath,
98616
- totalBytes: row.totalBytes,
98617
- sha256: row.wholeSha256
98618
- };
98619
- });
98620
- const commitWrite = (input) => withTransferLock(input.transferId, commitWriteEffect(input)).pipe(mapError(mapFailure("commit write", input.transferId)));
98621
- const cancelWriteEffect = fn$1("WorkspaceBulkTransfer.cancelWrite")(function* (input) {
98622
- const row = yield* requireTransfer(input.transferId);
98623
- if (row.state !== "completed") {
98624
- yield* fileSystem.remove(row.tempPath, { force: true });
98625
- yield* sql`
98626
- UPDATE workspace_bulk_transfers SET state = 'cancelled', updated_at = ${(/* @__PURE__ */ new Date()).toISOString()}
98627
- WHERE transfer_id = ${input.transferId}
98628
- `;
98629
- }
98630
- return {
98631
- transferId: input.transferId,
98632
- state: row.state === "completed" ? "completed" : "cancelled"
98633
- };
98634
- });
98635
- const cancelWrite = (input) => withTransferLock(input.transferId, cancelWriteEffect(input)).pipe(mapError(mapFailure("cancel write", input.transferId)));
98636
- const cleanupExpiredEffect = fn$1("WorkspaceBulkTransfer.cleanupExpired")(function* () {
98637
- const now = (/* @__PURE__ */ new Date()).toISOString();
98638
- const rows = yield* sql`
98639
- SELECT transfer_id AS "transferId", temp_path AS "tempPath"
98640
- FROM workspace_bulk_transfers WHERE state = 'open' AND expires_at <= ${now}
98641
- `;
98642
- let expiredCount = 0;
98643
- for (const row of rows) if (yield* withTransferLock(row.transferId, gen(function* () {
98644
- const current = yield* findTransfer(row.transferId);
98645
- if (current === null || current.state !== "open" || Date.parse(current.expiresAt) > Date.parse(now)) return false;
98646
- yield* fileSystem.remove(current.tempPath, { force: true });
98647
- yield* sql`
98648
- UPDATE workspace_bulk_transfers SET state = 'expired', updated_at = ${now}
98649
- WHERE transfer_id = ${row.transferId} AND state = 'open' AND expires_at <= ${now}
98650
- `;
98651
- return true;
98652
- }))) expiredCount += 1;
98653
- return expiredCount;
98654
- });
98655
- const cleanupExpired = () => cleanupExpiredEffect().pipe(mapError(mapFailure("expiry cleanup")));
98656
- return {
98657
- readFile,
98658
- beginWrite,
98659
- writeChunk,
98660
- commitWrite,
98661
- cancelWrite,
98662
- cleanupExpired
98663
- };
98664
- });
98665
- const WorkspaceBulkTransferLive = effect(WorkspaceBulkTransfer, makeWorkspaceBulkTransfer());
98666
98317
  //#endregion
98667
98318
  //#region src/projectFile/readProjectFile.ts
98668
98319
  /**
@@ -99464,6 +99115,26 @@ const DevTunnelLive = effect(DevTunnel, gen(function* () {
99464
99115
  };
99465
99116
  }));
99466
99117
  //#endregion
99118
+ //#region src/workspace/Services/WorkspaceEntries.ts
99119
+ /**
99120
+ * WorkspaceEntries - Effect service contract for cached workspace entry search.
99121
+ *
99122
+ * Owns indexed workspace entry search plus cache invalidation for workspace
99123
+ * roots when the underlying filesystem changes.
99124
+ *
99125
+ * @module WorkspaceEntries
99126
+ */
99127
+ var WorkspaceEntriesError = class extends TaggedErrorClass()("WorkspaceEntriesError", {
99128
+ cwd: String$1,
99129
+ operation: String$1,
99130
+ detail: String$1,
99131
+ cause: optional$4(Defect)
99132
+ }) {};
99133
+ /**
99134
+ * WorkspaceEntries - Service tag for cached workspace entry search.
99135
+ */
99136
+ var WorkspaceEntries = class extends Service$1()("forge/workspace/Services/WorkspaceEntries") {};
99137
+ //#endregion
99467
99138
  //#region src/workspace/Services/WorkspaceFileSystem.ts
99468
99139
  /**
99469
99140
  * WorkspaceFileSystem - Effect service contract for workspace file mutations.
@@ -99485,6 +99156,9 @@ var WorkspaceFileSystemError = class extends TaggedErrorClass()("WorkspaceFileSy
99485
99156
  */
99486
99157
  var WorkspaceFileSystem = class extends Service$1()("forge/workspace/Services/WorkspaceFileSystem") {};
99487
99158
  //#endregion
99159
+ //#region src/workspace/Services/WorkspaceBulkTransfer.ts
99160
+ var WorkspaceBulkTransfer = class extends Service$1()("forge/workspace/Services/WorkspaceBulkTransfer") {};
99161
+ //#endregion
99488
99162
  //#region src/workspace/Services/WorkspaceReadinessState.ts
99489
99163
  var WorkspaceReadinessState = class extends Service$1()("forge/workspace/Services/WorkspaceReadinessState") {};
99490
99164
  //#endregion
@@ -210937,56 +210611,169 @@ function hasConfiguredApexCredential(profiles) {
210937
210611
  }));
210938
210612
  }
210939
210613
  //#endregion
210940
- //#region src/provider/claudeCredentialContainment.ts
210614
+ //#region src/provider/claudeCredentialDirectory.ts
210941
210615
  /**
210942
- * Containment for provider credentials inherited by the Claude Agent SDK child.
210616
+ * Per-account credential directories for Claude turns.
210943
210617
  *
210944
- * Pure and dependency-free on purpose: ClaudeAdapter pulls in Effect platform
210945
- * services, the browser manager, and the server config, so a unit test that
210946
- * imports it cannot run in a worktree. This rule is cost-critical and must stay
210947
- * directly testable.
210618
+ * Claude Code keeps conversation transcripts under
210619
+ * `${CLAUDE_CONFIG_DIR}/projects/<cwd-slug>/<sessionId>.jsonl` and its login
210620
+ * under `.credentials.json`. `CLAUDE_SECURESTORAGE_CONFIG_DIR` moves only the
210621
+ * credential file, so each Provider Account can hold its own login while every
210622
+ * session file stays in one place and `--resume` keeps finding its target. See
210623
+ * `docs/adr/claude-accounts-split-credentials-not-sessions.md`.
210948
210624
  *
210949
- * Audit finding S1 (2026-07-25). The Claude Agent SDK reads ANTHROPIC_API_KEY /
210950
- * ANTHROPIC_AUTH_TOKEN from its own environment, and those TAKE PRECEDENCE over
210951
- * the user's claude.ai OAuth login — that precedence is exactly what the
210952
- * adapter relies on to redirect billing at the proxy.
210625
+ * That variable is not documented. Under
210626
+ * `docs/adr/unpublished-provider-surface-needs-a-fallback.md` the dependency is
210627
+ * only allowed because absence degrades to a named behaviour that already
210628
+ * works: a thread with no account runs on the Environment's own login. This
210629
+ * module sets the variable only when the resolved directory holds a credential
210630
+ * the CLI can use, because a missing file, or one whose access token a refusal
210631
+ * has blanked, makes the CLI report logged out and a live turn would fail.
210953
210632
  *
210954
- * So on a plain Claude turn (no endpoint profile, no proxy credential), which
210955
- * is supposed to run on the personal subscription, an ANTHROPIC_* key inherited
210956
- * from the server's environment would send the turn straight to
210957
- * api.anthropic.com on a metered account — never reaching
210958
- * /api/provider-egress/*, leaving the fail-closed billing gate inert.
210633
+ * Nothing writes these directories yet. #3049 adds the writer and keys each
210634
+ * directory on the account identity CMMD writes on the delivery receipt it
210635
+ * signs (`docs/adr/provider-account-identity-is-issued-by-cmmd.md`). Until then
210636
+ * every runtime takes the `no-account` skip.
210959
210637
  *
210960
- * Forge Cloud sprites were only incidentally safe (no Anthropic key present on
210961
- * the box). This makes containment an enforced property rather than an
210962
- * assumption about what the deployment environment happens to contain.
210638
+ * Free of Effect services on purpose, for the same reason as
210639
+ * `claudeCredentialContainment.ts`: ClaudeAdapter drags in Effect platform
210640
+ * services, the browser manager and the server config, so a unit test that
210641
+ * imports it cannot run in a worktree. This rule decides which account pays for
210642
+ * a turn, so it has to stay directly testable.
210963
210643
  */
210964
- /** Credentials the Claude Agent SDK will prefer over the user's OAuth login. */
210965
- const INHERITABLE_ANTHROPIC_CREDENTIAL_KEYS = ["ANTHROPIC_API_KEY", "ANTHROPIC_AUTH_TOKEN"];
210644
+ /** Repoints `.credentials.json` without moving the transcripts. */
210645
+ const CLAUDE_SECURESTORAGE_CONFIG_DIR_ENV = "CLAUDE_SECURESTORAGE_CONFIG_DIR";
210646
+ /** The one file Claude Code writes into the credential directory. */
210647
+ const CLAUDE_CREDENTIAL_FILE_NAME = ".credentials.json";
210966
210648
  /**
210967
- * Routing override the SDK would obey instead of the login's default endpoint.
210968
- * A CMMD cloud runtime exports it to point at the workspace provider-egress
210969
- * proxy; on a PERSONAL-Claude-in-VM runtime that must be dropped so the turn
210970
- * spends the on-box ~/.claude subscription (card 1359 Phase 2). Stripped ONLY
210971
- * when the runtime signals in-VM delivery — otherwise existing behavior (the
210972
- * inherited base URL survives) is preserved exactly.
210649
+ * Runtime env var naming the Provider Account this runtime's Claude turns
210650
+ * belong to. Nothing in Forge or CMMD sets it, so no runtime names an account
210651
+ * and each keeps its one login.
210652
+ *
210653
+ * It is a bridge that will not get a writer. #3073 settled the key in
210654
+ * `docs/adr/provider-account-identity-is-issued-by-cmmd.md`: the directory name
210655
+ * comes from the identity CMMD issues and writes on the delivery receipt it
210656
+ * signs, and this variable goes away. #3049 makes that change. The credential
210657
+ * generation was ruled out as a key: it rotates on every reconnect, and CMMD
210658
+ * uses it as the primary account's id, so keying on it would strand every
210659
+ * thread pinned to that account on a directory that stops receiving deliveries.
210660
+ *
210661
+ * Tests reach the `credentialDirectory` parameter on
210662
+ * `ClaudeAdapter.buildClaudeQueryEnv` directly.
210973
210663
  */
210974
- const ANTHROPIC_ENDPOINT_OVERRIDE_KEY = "ANTHROPIC_BASE_URL";
210664
+ const CLAUDE_PROVIDER_ACCOUNT_IDENTITY_ENV = "FORGE_CLAUDE_PROVIDER_ACCOUNT_IDENTITY";
210665
+ /** Directory under the server's base directory that holds one directory per account. */
210666
+ const CREDENTIAL_ROOT_DIRECTORY_NAME = "claude-credentials";
210667
+ const DEFAULT_CLAUDE_CONFIG_DIRECTORY_NAME = ".claude";
210668
+ const MAX_ACCOUNT_IDENTITY_LENGTH = 256;
210669
+ const DIRECTORY_NAME_HASH_LENGTH = 32;
210975
210670
  /**
210976
- * Remove inherited Anthropic credentials so the child falls back to the user's
210977
- * personal claude.ai login. Call this ONLY on the path where no base URL
210978
- * resolved; when the adapter has a profile or proxy credential it sets these
210979
- * variables deliberately, and that assignment must win.
210671
+ * Directory name for an account. A hash, so no email or display name reaches a
210672
+ * filesystem path and no path-illegal character can appear in one either.
210673
+ */
210674
+ function claudeCredentialDirectoryName(accountIdentity) {
210675
+ return `account-${createHash("sha256").update(accountIdentity, "utf8").digest("hex").slice(0, DIRECTORY_NAME_HASH_LENGTH)}`;
210676
+ }
210677
+ /** The account this runtime's Claude turns belong to, or null while the variable is unset. */
210678
+ function resolveClaudeProviderAccountIdentity(env) {
210679
+ const identity = env[CLAUDE_PROVIDER_ACCOUNT_IDENTITY_ENV]?.trim();
210680
+ if (!identity || identity.length > MAX_ACCOUNT_IDENTITY_LENGTH) return null;
210681
+ return identity;
210682
+ }
210683
+ /**
210684
+ * Where per-account credential directories live: `claude-credentials` under the
210685
+ * server's base directory, `ServerConfig.baseDir`, which is `FORGE_HOME` or
210686
+ * `~/.forge`. A delivery writer gets the same `ServerConfig`, so reader and
210687
+ * writer cannot disagree about the root. Always outside `CLAUDE_CONFIG_DIR`, so
210688
+ * one of these can never be mistaken for the main credential store, and a stray
210689
+ * read of the config directory cannot walk into another account's login.
210690
+ */
210691
+ function claudeCredentialDirectoryRoot(env, baseDir) {
210692
+ if (!isAbsolute(baseDir)) return null;
210693
+ const root = resolve(baseDir, CREDENTIAL_ROOT_DIRECTORY_NAME);
210694
+ const configDirectory = claudeConfigDirectory(env);
210695
+ if (configDirectory && isPathWithinRoot({
210696
+ relative,
210697
+ isAbsolute
210698
+ }, configDirectory, root)) return null;
210699
+ return root;
210700
+ }
210701
+ /** Absolute credential directory for one account, or null when either half is missing. */
210702
+ function resolveClaudeCredentialDirectory(input) {
210703
+ if (!input.accountIdentity) return null;
210704
+ const root = claudeCredentialDirectoryRoot(input.env, input.baseDir);
210705
+ if (!root) return null;
210706
+ return join(root, claudeCredentialDirectoryName(input.accountIdentity));
210707
+ }
210708
+ /**
210709
+ * Decide whether this turn may point at a per-account credential directory.
210980
210710
  *
210981
- * `stripEndpointOverride` additionally drops ANTHROPIC_BASE_URL — pass it only on
210982
- * a personal-Claude-in-VM runtime (gated), never by default, so existing turns
210983
- * that rely on an inherited base URL are untouched.
210711
+ * The cheap checks run first and the capability probe runs last, so a runtime
210712
+ * with no usable credential never spawns the CLI. The probe's error channel is
210713
+ * empty, so nothing here catches.
210714
+ *
210715
+ * A skip is not a fallback onto the default account. With no account the turn
210716
+ * runs on the Environment's own login, unchanged. A thread pinned to an account
210717
+ * whose credential is missing is refused by name under
210718
+ * `docs/adr/provider-account-identity-is-issued-by-cmmd.md`, and #3049 turns
210719
+ * `no-usable-credential`, and `capability-not-supported` for a pinned account,
210720
+ * into those refusals. Until #3049 nothing names an account, so `no-account` is
210721
+ * the only skip a runtime sees.
210722
+ */
210723
+ function decideClaudeCredentialDirectory(input) {
210724
+ return gen(function* () {
210725
+ if (!input.accountIdentity) return skip("no-account");
210726
+ if (input.env["CLAUDE_SECURESTORAGE_CONFIG_DIR"]?.trim()) return skip("operator-override");
210727
+ const directory = resolveClaudeCredentialDirectory(input);
210728
+ if (!directory) return skip("no-root");
210729
+ if (!holdsUsableCredential(directory)) return skip("no-usable-credential");
210730
+ return (yield* input.probeSupport) === "supported" ? set(directory) : skip("capability-not-supported");
210731
+ });
210732
+ }
210733
+ /**
210734
+ * True when `.credentials.json` parses and carries a non-empty access token.
210735
+ *
210736
+ * File presence is not readiness. A refused credential is rewritten in place
210737
+ * with `accessToken: ""`, so a file that exists can still leave the CLI logged
210738
+ * out. Only the token's length is checked. The value is never logged, returned
210739
+ * or kept.
210984
210740
  */
210985
- function withoutInheritedAnthropicCredentials(env, options) {
210986
- const contained = { ...env };
210987
- for (const key of INHERITABLE_ANTHROPIC_CREDENTIAL_KEYS) delete contained[key];
210988
- if (options?.stripEndpointOverride) delete contained[ANTHROPIC_ENDPOINT_OVERRIDE_KEY];
210989
- return contained;
210741
+ function holdsUsableCredential(directory) {
210742
+ const file = join(directory, CLAUDE_CREDENTIAL_FILE_NAME);
210743
+ try {
210744
+ if (!statSync(file).isFile()) return false;
210745
+ return hasAccessToken(JSON.parse(readFileSync(file, "utf8")));
210746
+ } catch {
210747
+ return false;
210748
+ }
210749
+ }
210750
+ function hasAccessToken(credential) {
210751
+ if (typeof credential !== "object" || credential === null) return false;
210752
+ const oauth = credential.claudeAiOauth;
210753
+ if (typeof oauth !== "object" || oauth === null) return false;
210754
+ const token = oauth.accessToken;
210755
+ return typeof token === "string" && token.length > 0;
210756
+ }
210757
+ function set(directory) {
210758
+ return {
210759
+ outcome: "set",
210760
+ directory
210761
+ };
210762
+ }
210763
+ function skip(reason) {
210764
+ return {
210765
+ outcome: "skipped",
210766
+ reason
210767
+ };
210768
+ }
210769
+ function claudeConfigDirectory(env) {
210770
+ const configured = env.CLAUDE_CONFIG_DIR?.trim();
210771
+ if (configured) return configured;
210772
+ try {
210773
+ return join(homedir(), DEFAULT_CLAUDE_CONFIG_DIRECTORY_NAME);
210774
+ } catch {
210775
+ return null;
210776
+ }
210990
210777
  }
210991
210778
  //#endregion
210992
210779
  //#region src/provider/personalClaudeInVm.ts
@@ -211056,160 +210843,76 @@ function personalClaudeGenerationHash(env = process.env) {
211056
210843
  }
211057
210844
  }
211058
210845
  //#endregion
211059
- //#region src/provider/claudeCredentialDirectory.ts
210846
+ //#region src/provider/claudeCredentialContainment.ts
211060
210847
  /**
211061
- * Per-account credential directories for Claude turns.
211062
- *
211063
- * Claude Code keeps conversation transcripts under
211064
- * `${CLAUDE_CONFIG_DIR}/projects/<cwd-slug>/<sessionId>.jsonl` and its login
211065
- * under `.credentials.json`. `CLAUDE_SECURESTORAGE_CONFIG_DIR` moves only the
211066
- * credential file, so each Provider Account can hold its own login while every
211067
- * session file stays in one place and `--resume` keeps finding its target. See
211068
- * `docs/adr/claude-accounts-split-credentials-not-sessions.md`.
210848
+ * Containment for provider credentials inherited by the Claude Agent SDK child.
211069
210849
  *
211070
- * That variable is not documented. Under
211071
- * `docs/adr/unpublished-provider-surface-needs-a-fallback.md` the dependency is
211072
- * only allowed because absence degrades to a named behaviour that already
211073
- * works: one account per Environment, exactly what runs today. This module is
211074
- * the gate that keeps the degrade honest. It sets the variable only when a
211075
- * readable `.credentials.json` is already sitting in the resolved directory,
211076
- * because a missing or empty directory makes the CLI report logged out and a
211077
- * live turn would fail. Nothing writes these directories yet, so every runtime
211078
- * today takes the skip path.
210850
+ * Free of Effect services on purpose: ClaudeAdapter pulls in Effect platform
210851
+ * services, the browser manager, and the server config, so a unit test that
210852
+ * imports it cannot run in a worktree. This rule is cost-critical and must stay
210853
+ * directly testable.
211079
210854
  *
211080
- * Pure and dependency-free on purpose, for the same reason as
211081
- * `claudeCredentialContainment.ts`: ClaudeAdapter drags in Effect platform
211082
- * services, the browser manager and the server config, so a unit test that
211083
- * imports it cannot run in a worktree. This rule decides which account pays for
211084
- * a turn, so it has to stay directly testable.
211085
- */
211086
- /** Repoints `.credentials.json` without moving the transcripts. */
211087
- const CLAUDE_SECURESTORAGE_CONFIG_DIR_ENV = "CLAUDE_SECURESTORAGE_CONFIG_DIR";
211088
- /** The one file Claude Code writes into the credential directory. */
211089
- const CLAUDE_CREDENTIAL_FILE_NAME = ".credentials.json";
211090
- /**
211091
- * Runtime env var naming the Provider Account this runtime's Claude turns
211092
- * belong to.
210855
+ * Audit finding S1 (2026-07-25). The Claude Agent SDK reads ANTHROPIC_API_KEY /
210856
+ * ANTHROPIC_AUTH_TOKEN from its own environment, and those TAKE PRECEDENCE over
210857
+ * the user's claude.ai OAuth login — that precedence is exactly what the
210858
+ * adapter relies on to redirect billing at the proxy.
211093
210859
  *
211094
- * NOTHING SETS THIS. It has no writer anywhere in Forge or CMMD, so as shipped
211095
- * the per-account lane can never turn on and every runtime keeps today's single
211096
- * account. The writer waits on issue 3073, which decides which identity keys
211097
- * the directory. That has to be settled first: the credential generation
211098
- * rotates on every re-delivery, so keying on it would give a new directory
211099
- * each time and defeat the point.
210860
+ * So on a plain Claude turn (no endpoint profile, no proxy credential), which
210861
+ * is supposed to run on the personal subscription, an ANTHROPIC_* key inherited
210862
+ * from the server's environment would send the turn straight to
210863
+ * api.anthropic.com on a metered account — never reaching
210864
+ * /api/provider-egress/*, leaving the fail-closed billing gate inert.
211100
210865
  *
211101
- * It is also not the seam. The seam is the `credentialDirectory` parameter on
211102
- * `ClaudeAdapter.buildClaudeQueryEnv`, which is what tests and callers drive.
211103
- * This variable is one bridge into that parameter, and the delivery step is
211104
- * free to replace it with something better rather than adopt it.
210866
+ * Forge Cloud sprites were only incidentally safe (no Anthropic key present on
210867
+ * the box). This makes containment an enforced property rather than an
210868
+ * assumption about what the deployment environment happens to contain.
211105
210869
  */
211106
- const CLAUDE_PROVIDER_ACCOUNT_IDENTITY_ENV = "FORGE_CLAUDE_PROVIDER_ACCOUNT_IDENTITY";
211107
- /** Operator escape hatch for the directory root, mostly for local debugging. */
211108
- const CLAUDE_CREDENTIAL_ROOT_ENV = "FORGE_CLAUDE_CREDENTIAL_ROOT";
211109
- const CREDENTIAL_ROOT_DIRECTORY_NAME = "claude-credentials";
211110
- const FALLBACK_ROOT_DIRECTORY_NAME = ".forge-claude-credentials";
211111
- const DEFAULT_CLAUDE_CONFIG_DIRECTORY_NAME = ".claude";
211112
- const MAX_ACCOUNT_IDENTITY_LENGTH = 256;
211113
- const DIRECTORY_NAME_HASH_LENGTH = 32;
210870
+ /** Credentials the Claude Agent SDK will prefer over the user's OAuth login. */
210871
+ const INHERITABLE_ANTHROPIC_CREDENTIAL_KEYS = ["ANTHROPIC_API_KEY", "ANTHROPIC_AUTH_TOKEN"];
211114
210872
  /**
211115
- * Directory name for an account. A hash, so no email or display name reaches a
211116
- * filesystem path and no path-illegal character can appear in one either.
210873
+ * Routing override the SDK would obey instead of the login's default endpoint.
210874
+ * A CMMD cloud runtime exports it to point at the workspace provider-egress
210875
+ * proxy; on a PERSONAL-Claude-in-VM runtime that must be dropped so the turn
210876
+ * spends the on-box ~/.claude subscription (card 1359 Phase 2). Stripped ONLY
210877
+ * when the runtime signals in-VM delivery — otherwise existing behavior (the
210878
+ * inherited base URL survives) is preserved exactly.
211117
210879
  */
211118
- function claudeCredentialDirectoryName(accountIdentity) {
211119
- return `account-${createHash("sha256").update(accountIdentity, "utf8").digest("hex").slice(0, DIRECTORY_NAME_HASH_LENGTH)}`;
211120
- }
211121
- /** The account this runtime's Claude turns belong to, or null on every runtime today. */
211122
- function resolveClaudeProviderAccountIdentity(env) {
211123
- const identity = env[CLAUDE_PROVIDER_ACCOUNT_IDENTITY_ENV]?.trim();
211124
- if (!identity || identity.length > MAX_ACCOUNT_IDENTITY_LENGTH) return null;
211125
- return identity;
211126
- }
210880
+ const ANTHROPIC_ENDPOINT_OVERRIDE_KEY = "ANTHROPIC_BASE_URL";
211127
210881
  /**
211128
- * Where per-account credential directories live. Always outside
211129
- * `CLAUDE_CONFIG_DIR`, so one of them can never be mistaken for the main
211130
- * credential store, and so a stray read of the config directory cannot walk
211131
- * into another account's login.
210882
+ * Remove inherited Anthropic credentials so the child falls back to the user's
210883
+ * personal claude.ai login. Call this ONLY on the path where no base URL
210884
+ * resolved; when the adapter has a profile or proxy credential it sets these
210885
+ * variables deliberately, and that assignment must win.
210886
+ *
210887
+ * `stripEndpointOverride` additionally drops ANTHROPIC_BASE_URL — pass it only on
210888
+ * a personal-Claude-in-VM runtime (gated), never by default, so existing turns
210889
+ * that rely on an inherited base URL are untouched.
211132
210890
  */
211133
- function claudeCredentialDirectoryRoot(env) {
211134
- const root = credentialRootCandidate(env);
211135
- if (!root || !isAbsolute(root)) return null;
211136
- return isInside(root, claudeConfigDirectory(env)) ? null : resolve(root);
211137
- }
211138
- /** Absolute credential directory for one account, or null when either half is missing. */
211139
- function resolveClaudeCredentialDirectory(input) {
211140
- if (!input.accountIdentity) return null;
211141
- const root = claudeCredentialDirectoryRoot(input.env);
211142
- if (!root) return null;
211143
- return join(root, claudeCredentialDirectoryName(input.accountIdentity));
210891
+ function withoutInheritedAnthropicCredentials(env, options) {
210892
+ const contained = { ...env };
210893
+ for (const key of INHERITABLE_ANTHROPIC_CREDENTIAL_KEYS) delete contained[key];
210894
+ if (options?.stripEndpointOverride) delete contained[ANTHROPIC_ENDPOINT_OVERRIDE_KEY];
210895
+ return contained;
211144
210896
  }
211145
210897
  /**
211146
- * Decide whether this turn may point at a per-account credential directory.
210898
+ * The environment a Claude child gets when it runs on the personal
210899
+ * subscription. Server-only secrets are scrubbed, inherited Anthropic keys are
210900
+ * removed so they cannot outrank the login, and the credential directory is set
210901
+ * only when one was decided. The turn's personal-subscription branch and the
210902
+ * capability probe both build their environment here, so the probe asks about
210903
+ * the setup the turn will run with.
211147
210904
  *
211148
- * The cheap checks run first and the capability probe runs last, so a runtime
211149
- * with no delivered credential never spawns the CLI. Any failure is a skip, so
211150
- * the worst outcome is today's behaviour.
211151
- */
211152
- async function decideClaudeCredentialDirectory(input) {
211153
- if (!input.accountIdentity) return skip("no-account");
211154
- if (input.env["CLAUDE_SECURESTORAGE_CONFIG_DIR"]?.trim()) return skip("operator-override");
211155
- const directory = resolveClaudeCredentialDirectory(input);
211156
- if (!directory) return skip("no-root");
211157
- if (!(input.credentialFilePresent ?? hasReadableCredentialFile)(directory)) return skip("no-credential-file");
211158
- return await probeSupportSafely(input.probeSupport) === "supported" ? {
211159
- outcome: "set",
211160
- directory
211161
- } : skip("capability-not-supported");
211162
- }
211163
- /** True when `.credentials.json` is in the directory and readable right now. */
211164
- function hasReadableCredentialFile(directory) {
211165
- const file = join(directory, CLAUDE_CREDENTIAL_FILE_NAME);
211166
- try {
211167
- if (!statSync(file).isFile()) return false;
211168
- accessSync(file, constants.R_OK);
211169
- return true;
211170
- } catch {
211171
- return false;
211172
- }
211173
- }
211174
- function skip(reason) {
210905
+ * The endpoint override goes only when `personalClaudeInVmEnabled` says this
210906
+ * runtime is on the personal Claude lane. That check also reads the activation
210907
+ * marker CMMD writes on disk (#3081), so a Sprite switched on in place, which
210908
+ * has the marker and no env var until it restarts, drops the override too.
210909
+ */
210910
+ function claudeSubscriptionChildEnv(processEnv, credentialDirectory) {
211175
210911
  return {
211176
- outcome: "skipped",
211177
- reason
210912
+ ...withoutInheritedAnthropicCredentials(scrubForgeProviderChildEnvSecrets(processEnv), { stripEndpointOverride: personalClaudeInVmEnabled(processEnv) }),
210913
+ ...credentialDirectory ? { [CLAUDE_SECURESTORAGE_CONFIG_DIR_ENV]: credentialDirectory } : {}
211178
210914
  };
211179
210915
  }
211180
- /** `inconclusive` is treated as `unsupported`, and so is a probe that throws. */
211181
- async function probeSupportSafely(probeSupport) {
211182
- try {
211183
- return await probeSupport();
211184
- } catch {
211185
- return "inconclusive";
211186
- }
211187
- }
211188
- function credentialRootCandidate(env) {
211189
- const explicit = env[CLAUDE_CREDENTIAL_ROOT_ENV]?.trim();
211190
- if (explicit) return explicit;
211191
- const forgeHome = env.FORGE_HOME?.trim();
211192
- if (forgeHome) return join(forgeHome, CREDENTIAL_ROOT_DIRECTORY_NAME);
211193
- try {
211194
- return join(homedir(), FALLBACK_ROOT_DIRECTORY_NAME);
211195
- } catch {
211196
- return null;
211197
- }
211198
- }
211199
- function claudeConfigDirectory(env) {
211200
- const configured = env.CLAUDE_CONFIG_DIR?.trim();
211201
- if (configured) return configured;
211202
- try {
211203
- return join(homedir(), DEFAULT_CLAUDE_CONFIG_DIRECTORY_NAME);
211204
- } catch {
211205
- return null;
211206
- }
211207
- }
211208
- function isInside(candidate, parent) {
211209
- if (!parent) return false;
211210
- const step = relative(resolve(parent), resolve(candidate));
211211
- return step === "" || !step.startsWith("..") && !isAbsolute(step);
211212
- }
211213
210916
  //#endregion
211214
210917
  //#region src/provider/claudeCredentialDirectoryProbe.ts
211215
210918
  /**
@@ -211222,6 +210925,11 @@ function isInside(candidate, parent) {
211222
210925
  * A runtime with no credential at all is logged out either way and cannot
211223
210926
  * discriminate, which is `inconclusive`, and callers read that as unsupported.
211224
210927
  *
210928
+ * An answer only counts for the binary and environment the turn will use. The
210929
+ * probe spawns the `binaryPath` the turn hands the SDK, and builds its
210930
+ * environment with `claudeSubscriptionChildEnv`, the same builder as the turn's
210931
+ * personal-subscription branch.
210932
+ *
211225
210933
  * `claude auth status` is a pure read. It sends no prompt and leaves
211226
210934
  * `.credentials.json` byte-identical, so probing spends no subscription usage.
211227
210935
  * It prints JSON on stdout and exits 1 when logged out, so the exit code is not
@@ -211237,38 +210945,62 @@ const VERSION_ARGS = ["--version"];
211237
210945
  * hung or unkillable process costs a few seconds and an `inconclusive`.
211238
210946
  */
211239
210947
  const PROBE_TIMEOUT_MS = 5e3;
210948
+ const PROBE_OUTPUT_LIMIT_BYTES = 1024 * 1024;
211240
210949
  const PROBE_DIRECTORY_PREFIX = "forge-claude-capability-";
211241
210950
  const VERSION_PATTERN = /\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?/;
211242
210951
  function makeClaudeCredentialDirectoryProbe(deps) {
211243
210952
  const answers = /* @__PURE__ */ new Map();
211244
- return { support: async () => {
210953
+ const answer = async () => {
211245
210954
  const version = await readVersionSafely(deps);
211246
210955
  if (version === null) return runProbe(deps);
211247
210956
  const cached = answers.get(version);
211248
210957
  if (cached) return cached;
211249
210958
  const pending = runProbe(deps);
211250
210959
  answers.set(version, pending);
210960
+ const forget = () => {
210961
+ if (answers.get(version) === pending) answers.delete(version);
210962
+ };
210963
+ pending.then((result) => {
210964
+ if (result === "inconclusive") forget();
210965
+ }, forget);
211251
210966
  return pending;
211252
- } };
210967
+ };
210968
+ return { support: tryPromise(answer).pipe(orElseSucceed(() => "inconclusive")) };
211253
210969
  }
211254
210970
  /**
211255
- * One probe per binary path for the life of the process. The probe spawns the
211256
- * CLI twice, and every Claude session start would otherwise pay for it.
210971
+ * One probe per binary path for the life of the process. A conclusive answer is
210972
+ * cached per binary version, so the two `auth status` spawns run once per path
210973
+ * and version. `claude --version` still spawns on every call, which is how an
210974
+ * in-place CLI upgrade gets a fresh answer.
211257
210975
  */
211258
210976
  const probesByBinaryPath = /* @__PURE__ */ new Map();
210977
+ const INCONCLUSIVE_PROBE = { support: succeed("inconclusive") };
210978
+ /**
210979
+ * The probe for the binary a turn with this `binaryPath` setting runs.
210980
+ *
210981
+ * `buildClaudeQueryOptions` hands the setting to the SDK as
210982
+ * `pathToClaudeCodeExecutable`, and the SDK spawns it as given, so the default
210983
+ * `claude` goes through PATH for the probe exactly as for the turn. The SDK
210984
+ * picks a bundled build of its own only when the path is empty, and the
210985
+ * settings schema turns an empty value into `claude`. Should one arrive empty
210986
+ * anyway, the answer is `inconclusive` rather than an answer about another
210987
+ * binary.
210988
+ */
211259
210989
  function claudeCredentialDirectoryProbeFor(binaryPath) {
211260
- const binary = binaryPath?.trim() || "claude";
211261
- const existing = probesByBinaryPath.get(binary);
210990
+ if (!binaryPath) return INCONCLUSIVE_PROBE;
210991
+ const existing = probesByBinaryPath.get(binaryPath);
211262
210992
  if (existing) return existing;
211263
- const probe = makeClaudeCredentialDirectoryProbe(claudeCredentialDirectoryProbeDeps(binary));
211264
- probesByBinaryPath.set(binary, probe);
210993
+ const probe = makeClaudeCredentialDirectoryProbe(claudeCredentialDirectoryProbeDeps(binaryPath));
210994
+ probesByBinaryPath.set(binaryPath, probe);
211265
210995
  return probe;
211266
210996
  }
211267
- function claudeCredentialDirectoryProbeDeps(binaryPath) {
210997
+ const spawnClaude = (file, args, options) => execFileAsync$3(file, [...args], { ...options });
210998
+ function claudeCredentialDirectoryProbeDeps(binaryPath, options = {}) {
210999
+ const run = (args, credentialDirectory) => runClaudeCommand$1(options.execFile ?? spawnClaude, binaryPath, args, claudeSubscriptionChildEnv(options.baseEnv ?? process.env, credentialDirectory));
211268
211000
  return {
211269
- runAuthStatus: ({ credentialDirectory }) => runClaudeCommand$1(binaryPath, AUTH_STATUS_ARGS, credentialDirectory),
211001
+ runAuthStatus: ({ credentialDirectory }) => run(AUTH_STATUS_ARGS, credentialDirectory),
211270
211002
  readBinaryVersion: async () => {
211271
- const { stdout } = await runClaudeCommand$1(binaryPath, VERSION_ARGS, null);
211003
+ const { stdout } = await run(VERSION_ARGS, null);
211272
211004
  return VERSION_PATTERN.exec(stdout)?.[0] ?? null;
211273
211005
  },
211274
211006
  makeProbeDirectory: () => mkdtemp(join(tmpdir(), PROBE_DIRECTORY_PREFIX)),
@@ -211278,6 +211010,7 @@ function claudeCredentialDirectoryProbeDeps(binaryPath) {
211278
211010
  })
211279
211011
  };
211280
211012
  }
211013
+ /** Rejects when the probe could not tell. `support` turns that into `inconclusive`. */
211281
211014
  async function runProbe(deps) {
211282
211015
  let probeDirectory = null;
211283
211016
  try {
@@ -211287,8 +211020,6 @@ async function runProbe(deps) {
211287
211020
  const redirected = parseLoggedIn(await runBounded(deps, () => deps.runAuthStatus({ credentialDirectory: directory })));
211288
211021
  if (redirected === false) return "supported";
211289
211022
  return redirected === true ? "unsupported" : "inconclusive";
211290
- } catch {
211291
- return "inconclusive";
211292
211023
  } finally {
211293
211024
  if (probeDirectory !== null) await deps.removeProbeDirectory(probeDirectory).catch(() => void 0);
211294
211025
  }
@@ -211310,15 +211041,15 @@ async function runBounded(deps, operation) {
211310
211041
  let timer;
211311
211042
  try {
211312
211043
  return await Promise.race([operation(), new Promise((_resolve, reject) => {
211313
- timer = setTimeout(() => reject(/* @__PURE__ */ new Error("claude auth status timed out")), timeoutMs);
211044
+ timer = setTimeout(() => reject(/* @__PURE__ */ new Error("claude probe call timed out")), timeoutMs);
211314
211045
  })]);
211315
211046
  } finally {
211316
211047
  if (timer !== void 0) clearTimeout(timer);
211317
211048
  }
211318
211049
  }
211319
211050
  /** The `loggedIn` field, or null when the output does not carry one. */
211320
- function parseLoggedIn(run) {
211321
- const parsed = parseJsonObject$1(run.stdout);
211051
+ function parseLoggedIn(output) {
211052
+ const parsed = parseJsonObject$1(output.stdout);
211322
211053
  return typeof parsed?.loggedIn === "boolean" ? parsed.loggedIn : null;
211323
211054
  }
211324
211055
  function parseJsonObject$1(output) {
@@ -211333,27 +211064,18 @@ function parseJsonObject$1(output) {
211333
211064
  return null;
211334
211065
  }
211335
211066
  }
211336
- async function runClaudeCommand$1(binaryPath, args, credentialDirectory) {
211067
+ async function runClaudeCommand$1(execFileImpl, binaryPath, args, env) {
211337
211068
  try {
211338
- const { stdout } = await execFileAsync$3(binaryPath, [...args], {
211339
- env: {
211340
- ...process.env,
211341
- ...credentialDirectory ? { [CLAUDE_SECURESTORAGE_CONFIG_DIR_ENV]: credentialDirectory } : {}
211342
- },
211343
- maxBuffer: 1024 * 1024,
211069
+ const { stdout } = await execFileImpl(binaryPath, args, {
211070
+ env,
211071
+ maxBuffer: PROBE_OUTPUT_LIMIT_BYTES,
211344
211072
  timeout: PROBE_TIMEOUT_MS
211345
211073
  });
211346
- return {
211347
- stdout,
211348
- exitCode: 0
211349
- };
211074
+ return { stdout };
211350
211075
  } catch (cause) {
211351
211076
  const failure = cause;
211352
211077
  if (typeof failure.stdout !== "string") throw cause;
211353
- return {
211354
- stdout: failure.stdout,
211355
- exitCode: typeof failure.code === "number" ? failure.code : null
211356
- };
211078
+ return { stdout: failure.stdout };
211357
211079
  }
211358
211080
  }
211359
211081
  //#endregion
@@ -211368,12 +211090,12 @@ const makeManagedServerProvider = fn$1("makeManagedServerProvider")(function* (i
211368
211090
  const forceRefresh = options?.forceRefresh === true;
211369
211091
  const previousSettings = yield* get$6(settingsRef);
211370
211092
  if (!forceRefresh && !input.haveSettingsChanged(previousSettings, nextSettings)) {
211371
- yield* set$2(settingsRef, nextSettings);
211093
+ yield* set$3(settingsRef, nextSettings);
211372
211094
  return yield* get$6(snapshotRef);
211373
211095
  }
211374
211096
  const nextSnapshot = yield* input.checkProvider;
211375
- yield* set$2(settingsRef, nextSettings);
211376
- yield* set$2(snapshotRef, nextSnapshot);
211097
+ yield* set$3(settingsRef, nextSettings);
211098
+ yield* set$3(snapshotRef, nextSnapshot);
211377
211099
  yield* publish(changesPubSub, nextSnapshot);
211378
211100
  return nextSnapshot;
211379
211101
  });
@@ -211393,7 +211115,7 @@ const makeManagedServerProvider = fn$1("makeManagedServerProvider")(function* (i
211393
211115
  failureCount: failureCount + 1,
211394
211116
  nextDelayMs: Math.min(baseMs * Math.pow(2, failureCount + 1), 3e5)
211395
211117
  });
211396
- } else yield* set$2(refreshFailureCount, 0);
211118
+ } else yield* set$3(refreshFailureCount, 0);
211397
211119
  })).pipe(forkScoped);
211398
211120
  return {
211399
211121
  getSnapshot: input.getSettings.pipe(flatMap(applySnapshot), tapError(logError), orDie),
@@ -212867,10 +212589,12 @@ function claudeForgeAttributionPrompt() {
212867
212589
  * `credentialDirectory` points this turn at one Provider Account's own
212868
212590
  * `.credentials.json` without moving the transcripts, which is what lets an
212869
212591
  * account switch keep `--resume` working
212870
- * (`docs/adr/claude-accounts-split-credentials-not-sessions.md`). It is null on
212871
- * every runtime today: `decideClaudeCredentialDirectory` only hands back a
212872
- * directory that already holds a readable credential file, and nothing writes
212873
- * one yet, so the environment stays byte-for-byte what it was.
212592
+ * (`docs/adr/claude-accounts-split-credentials-not-sessions.md`). It stays null
212593
+ * until #3049 gives the directories a writer, keyed on the account identity on
212594
+ * the delivery receipt CMMD signs. `decideClaudeCredentialDirectory` only hands
212595
+ * back a directory that already holds a usable credential, and while none
212596
+ * exists the environment is byte-for-byte what it would be without this
212597
+ * parameter.
212874
212598
  *
212875
212599
  * It applies to the personal-subscription branch below and to nothing else. On
212876
212600
  * the profile / proxy branch the credential file is not what authenticates the
@@ -212880,10 +212604,7 @@ function claudeForgeAttributionPrompt() {
212880
212604
  function buildClaudeQueryEnv(baseProcessEnv, endpointProfile, claudeSettings, providerProxyCredential, credentialDirectory) {
212881
212605
  const baseEnv = scrubForgeProviderChildEnvSecrets(baseProcessEnv);
212882
212606
  const effectiveBaseUrl = resolveClaudeQueryBaseUrl(endpointProfile, claudeSettings, providerProxyCredential);
212883
- if (!effectiveBaseUrl) return {
212884
- ...withoutInheritedAnthropicCredentials(baseEnv, { stripEndpointOverride: personalClaudeInVmEnabled(baseProcessEnv) }),
212885
- ...credentialDirectory ? { [CLAUDE_SECURESTORAGE_CONFIG_DIR_ENV]: credentialDirectory } : {}
212886
- };
212607
+ if (!effectiveBaseUrl) return claudeSubscriptionChildEnv(baseProcessEnv, credentialDirectory);
212887
212608
  const effectiveApiKey = resolveClaudeQueryApiKey(endpointProfile, claudeSettings, providerProxyCredential?.token ?? null);
212888
212609
  return {
212889
212610
  ...baseEnv,
@@ -215151,11 +214872,17 @@ const makeClaudeAdapter = fn$1("makeClaudeAdapter")(function* (options) {
215151
214872
  })));
215152
214873
  const claudeSettings = serverSettings.providers.claudeAgent;
215153
214874
  const claudeBinaryPath = claudeSettings.binaryPath;
215154
- const claudeCredentialDirectory = yield* tryPromise(() => decideClaudeCredentialDirectory({
214875
+ const credentialDirectoryDecision = yield* decideClaudeCredentialDirectory({
215155
214876
  env: process.env,
214877
+ baseDir: serverConfig.baseDir,
215156
214878
  accountIdentity: resolveClaudeProviderAccountIdentity(process.env),
215157
- probeSupport: () => claudeCredentialDirectoryProbeFor(claudeBinaryPath).support()
215158
- })).pipe(map$3((decision) => decision.outcome === "set" ? decision.directory : null), orElseSucceed(() => null));
214879
+ probeSupport: claudeCredentialDirectoryProbeFor(claudeBinaryPath).support
214880
+ });
214881
+ if (credentialDirectoryDecision.outcome === "skipped") {
214882
+ const skipLine = `Claude credential directory skipped: ${credentialDirectoryDecision.reason}`;
214883
+ yield* credentialDirectoryDecision.reason === "no-account" ? logDebug(skipLine) : logWarning$1(skipLine);
214884
+ }
214885
+ const claudeCredentialDirectory = credentialDirectoryDecision.outcome === "set" ? credentialDirectoryDecision.directory : null;
215159
214886
  const modelConfig = resolveClaudeSessionModelConfig(input.modelSelection, claudeSettings.endpointProfiles);
215160
214887
  const providerProxyCredential = yield* issueClaudeProviderProxyCredential(modelConfig.endpointProfile, modelConfig.modelSlug ?? modelConfig.effectiveApiModelId ?? "unknown", threadId);
215161
214888
  const permissionMode = permissionModeForRuntimeMode(input.runtimeMode);
@@ -215217,7 +214944,7 @@ const makeClaudeAdapter = fn$1("makeClaudeAdapter")(function* (options) {
215217
214944
  effectiveApiModelId: modelConfig.effectiveApiModelId,
215218
214945
  effectiveEffort: modelConfig.effectiveEffort
215219
214946
  });
215220
- yield* set$2(contextRef, context$4);
214947
+ yield* set$3(contextRef, context$4);
215221
214948
  sessions.set(threadId, context$4);
215222
214949
  yield* emitClaudeSessionStartupEvents({
215223
214950
  threadId,
@@ -220098,7 +219825,7 @@ const makeAcpSessionRuntime = (options) => gen(function* () {
220098
219825
  }
220099
219826
  });
220100
219827
  });
220101
- const updateConfigOptions = (response) => set$2(configOptionsRef, sessionConfigOptionsFromSetup(response));
219828
+ const updateConfigOptions = (response) => set$3(configOptionsRef, sessionConfigOptionsFromSetup(response));
220102
219829
  const updateCurrentModeId = (modeId) => update$4(modeStateRef, (current) => current ? {
220103
219830
  ...current,
220104
219831
  currentModeId: modeId
@@ -220157,8 +219884,8 @@ const makeAcpSessionRuntime = (options) => gen(function* () {
220157
219884
  sessionId = created.sessionId;
220158
219885
  sessionSetupResult = created;
220159
219886
  }
220160
- yield* set$2(modeStateRef, parseSessionModeState(sessionSetupResult));
220161
- yield* set$2(configOptionsRef, sessionConfigOptionsFromSetup(sessionSetupResult));
219887
+ yield* set$3(modeStateRef, parseSessionModeState(sessionSetupResult));
219888
+ yield* set$3(configOptionsRef, sessionConfigOptionsFromSetup(sessionSetupResult));
220162
219889
  return {
220163
219890
  sessionId,
220164
219891
  initializeResult,
@@ -220352,10 +220079,10 @@ const makeStartCoordinator = (startStateRef, startOnce) => gen(function* () {
220352
220079
  switch (state._tag) {
220353
220080
  case "Started": return [succeed(state.result), state];
220354
220081
  case "Starting": return [_await(state.deferred), state];
220355
- case "NotStarted": return [startOnce.pipe(tap((result) => set$2(startStateRef, {
220082
+ case "NotStarted": return [startOnce.pipe(tap((result) => set$3(startStateRef, {
220356
220083
  _tag: "Started",
220357
220084
  result
220358
- }).pipe(andThen(succeed$3(deferred, result)))), onError$1((cause) => failCause$3(deferred, cause).pipe(andThen(set$2(startStateRef, { _tag: "NotStarted" }))))), {
220085
+ }).pipe(andThen(succeed$3(deferred, result)))), onError$1((cause) => failCause$3(deferred, cause).pipe(andThen(set$3(startStateRef, { _tag: "NotStarted" }))))), {
220359
220086
  _tag: "Starting",
220360
220087
  deferred
220361
220088
  }];
@@ -284032,7 +283759,7 @@ const GitManagerLive = effect(GitManager, fn$1("makeGitManager")(function* () {
284032
283759
  let preResolvedCommitSuggestion = void 0;
284033
283760
  const modelSelection = yield* serverSettingsService.getSettings.pipe(map$3((settings) => settings.textGenerationModelSelection), mapError((cause) => gitManagerError("runStackedAction", "Failed to get server settings.", cause)));
284034
283761
  if (input.featureBranch) {
284035
- yield* set$2(currentPhase, some("branch"));
283762
+ yield* set$3(currentPhase, some("branch"));
284036
283763
  yield* progress.emit({
284037
283764
  kind: "phase_started",
284038
283765
  phase: "branch",
@@ -284045,12 +283772,12 @@ const GitManagerLive = effect(GitManager, fn$1("makeGitManager")(function* () {
284045
283772
  } else branchStep = { status: "skipped_not_requested" };
284046
283773
  const currentBranch = branchStep.name ?? initialStatus.branch;
284047
283774
  const commitAction = isCommitAction(input.action) ? input.action : null;
284048
- const commit = commitAction ? yield* set$2(currentPhase, some("commit")).pipe(flatMap(() => runCommitStep(modelSelection, input.cwd, commitAction, currentBranch, commitMessageForStep, preResolvedCommitSuggestion, input.filePaths, options?.progressReporter, progress.actionId, input.threadId))) : { status: "skipped_not_requested" };
283775
+ const commit = commitAction ? yield* set$3(currentPhase, some("commit")).pipe(flatMap(() => runCommitStep(modelSelection, input.cwd, commitAction, currentBranch, commitMessageForStep, preResolvedCommitSuggestion, input.filePaths, options?.progressReporter, progress.actionId, input.threadId))) : { status: "skipped_not_requested" };
284049
283776
  const push = wantsPush ? yield* progress.emit({
284050
283777
  kind: "phase_started",
284051
283778
  phase: "push",
284052
283779
  label: "Pushing..."
284053
- }).pipe(tap(() => set$2(currentPhase, some("push"))), flatMap(() => {
283780
+ }).pipe(tap(() => set$3(currentPhase, some("push"))), flatMap(() => {
284054
283781
  const emit = (event) => progress.emit(event);
284055
283782
  const hookProgress = createHookProgressEmitter(emit);
284056
283783
  return gitCore.pushCurrentBranch(input.cwd, currentBranch, { progress: hookProgress.progress }).pipe(tap(() => {
@@ -284068,11 +283795,11 @@ const GitManagerLive = effect(GitManager, fn$1("makeGitManager")(function* () {
284068
283795
  kind: "phase_started",
284069
283796
  phase: "pr",
284070
283797
  label: "Preparing PR merge..."
284071
- }).pipe(tap(() => set$2(currentPhase, some("pr"))), flatMap(() => runMergePrStep(input.cwd, currentBranch, progress.emit))) : wantsPr ? yield* progress.emit({
283798
+ }).pipe(tap(() => set$3(currentPhase, some("pr"))), flatMap(() => runMergePrStep(input.cwd, currentBranch, progress.emit))) : wantsPr ? yield* progress.emit({
284072
283799
  kind: "phase_started",
284073
283800
  phase: "pr",
284074
283801
  label: "Preparing PR..."
284075
- }).pipe(tap(() => set$2(currentPhase, some("pr"))), flatMap(() => runPrStep(modelSelection, input.cwd, currentBranch, progress.emit, input.threadId))) : { status: "skipped_not_requested" };
283802
+ }).pipe(tap(() => set$3(currentPhase, some("pr"))), flatMap(() => runPrStep(modelSelection, input.cwd, currentBranch, progress.emit, input.threadId))) : { status: "skipped_not_requested" };
284076
283803
  const toast = yield* buildCompletionToast(input.cwd, {
284077
283804
  action: input.action,
284078
283805
  branch: branchStep,
@@ -287928,7 +287655,7 @@ const ProviderRuntimeIngestionLive = effect(ProviderRuntimeIngestionService, fn$
287928
287655
  createdAt: ""
287929
287656
  })
287930
287657
  });
287931
- const rememberAssistantMessageId = (threadId, turnId, messageId) => getOption(turnMessageIdsByTurnKey, providerTurnKey(threadId, turnId)).pipe(flatMap((existingIds) => set$3(turnMessageIdsByTurnKey, providerTurnKey(threadId, turnId), match(existingIds, {
287658
+ const rememberAssistantMessageId = (threadId, turnId, messageId) => getOption(turnMessageIdsByTurnKey, providerTurnKey(threadId, turnId)).pipe(flatMap((existingIds) => set$4(turnMessageIdsByTurnKey, providerTurnKey(threadId, turnId), match(existingIds, {
287932
287659
  onNone: () => new Set([messageId]),
287933
287660
  onSome: (ids) => {
287934
287661
  const nextIds = new Set(ids);
@@ -287942,7 +287669,7 @@ const ProviderRuntimeIngestionLive = effect(ProviderRuntimeIngestionService, fn$
287942
287669
  const nextIds = new Set(ids);
287943
287670
  nextIds.delete(messageId);
287944
287671
  if (nextIds.size === 0) return invalidate(turnMessageIdsByTurnKey, providerTurnKey(threadId, turnId));
287945
- return set$3(turnMessageIdsByTurnKey, providerTurnKey(threadId, turnId), nextIds);
287672
+ return set$4(turnMessageIdsByTurnKey, providerTurnKey(threadId, turnId), nextIds);
287946
287673
  }
287947
287674
  })));
287948
287675
  const getAssistantMessageIdsForTurn = (threadId, turnId) => getOption(turnMessageIdsByTurnKey, providerTurnKey(threadId, turnId)).pipe(map$3((existingIds) => getOrElse(existingIds, () => /* @__PURE__ */ new Set())));
@@ -287953,7 +287680,7 @@ const ProviderRuntimeIngestionLive = effect(ProviderRuntimeIngestionService, fn$
287953
287680
  onSome: (text) => `${text}${delta}`
287954
287681
  });
287955
287682
  if (nextText.length <= MAX_BUFFERED_ASSISTANT_CHARS) {
287956
- yield* set$3(bufferedAssistantTextByMessageId, messageId, nextText);
287683
+ yield* set$4(bufferedAssistantTextByMessageId, messageId, nextText);
287957
287684
  return "";
287958
287685
  }
287959
287686
  yield* invalidate(bufferedAssistantTextByMessageId, messageId);
@@ -287963,7 +287690,7 @@ const ProviderRuntimeIngestionLive = effect(ProviderRuntimeIngestionService, fn$
287963
287690
  const clearBufferedAssistantText = (messageId) => invalidate(bufferedAssistantTextByMessageId, messageId);
287964
287691
  const appendBufferedProposedPlan = (planId, delta, createdAt) => getOption(bufferedProposedPlanById, planId).pipe(flatMap((existingEntry) => {
287965
287692
  const existing = getOrUndefined(existingEntry);
287966
- return set$3(bufferedProposedPlanById, planId, {
287693
+ return set$4(bufferedProposedPlanById, planId, {
287967
287694
  text: `${existing?.text ?? ""}${delta}`,
287968
287695
  createdAt: existing?.createdAt && existing.createdAt.length > 0 ? existing.createdAt : createdAt
287969
287696
  });
@@ -288841,7 +288568,7 @@ const ProviderCommandReactorLive = effect(ProviderCommandReactor, gen(function*
288841
288568
  timeToLive: HANDLED_TURN_START_KEY_TTL,
288842
288569
  lookup: () => succeed(true)
288843
288570
  });
288844
- const hasHandledTurnStartRecently = (key) => getOption(handledTurnStartKeys, key).pipe(flatMap((cached) => set$3(handledTurnStartKeys, key, true).pipe(as$3(isSome(cached)))));
288571
+ const hasHandledTurnStartRecently = (key) => getOption(handledTurnStartKeys, key).pipe(flatMap((cached) => set$4(handledTurnStartKeys, key, true).pipe(as$3(isSome(cached)))));
288845
288572
  const threadModelSelections = /* @__PURE__ */ new Map();
288846
288573
  /**
288847
288574
  * The providerAccountId each LIVE provider session was actually started with.
@@ -292684,6 +292411,382 @@ const WorkspaceFileSystemLive = effect(WorkspaceFileSystem, gen(function* () {
292684
292411
  }) };
292685
292412
  }));
292686
292413
  //#endregion
292414
+ //#region src/workspace/Layers/WorkspaceBulkTransfer.ts
292415
+ const TRANSFER_TTL_MS = 1440 * 60 * 1e3;
292416
+ const TRANSFER_LOCK_STRIPE_COUNT = 64;
292417
+ function transferError(input) {
292418
+ return new BulkTransferError({
292419
+ code: input.code,
292420
+ message: input.message,
292421
+ transferId: input.transferId ?? null,
292422
+ retryable: input.retryable ?? false,
292423
+ ...input.cause === void 0 ? {} : { cause: input.cause }
292424
+ });
292425
+ }
292426
+ function mapFailure(operation, transferId) {
292427
+ return (cause) => is$1(BulkTransferError)(cause) ? cause : transferError({
292428
+ code: "io_error",
292429
+ message: `Workspace bulk transfer failed during ${operation}.`,
292430
+ ...transferId === void 0 ? {} : { transferId },
292431
+ retryable: true,
292432
+ cause
292433
+ });
292434
+ }
292435
+ function transferLockStripe(transferId) {
292436
+ let hash = 2166136261;
292437
+ for (let index = 0; index < transferId.length; index += 1) hash = Math.imul(hash ^ transferId.charCodeAt(index), 16777619);
292438
+ return (hash >>> 0) % TRANSFER_LOCK_STRIPE_COUNT;
292439
+ }
292440
+ const makeWorkspaceBulkTransfer = (options = {}) => gen(function* () {
292441
+ const fileSystem = yield* FileSystem;
292442
+ const path = yield* Path$2;
292443
+ const sql = yield* SqlClient;
292444
+ const workspacePaths = yield* WorkspacePaths;
292445
+ const workspaceEntries = yield* WorkspaceEntries;
292446
+ const fault = options.fault ?? (() => void_$1);
292447
+ const transferLocks = yield* forEach(Array.from({ length: TRANSFER_LOCK_STRIPE_COUNT }), () => make$43(1));
292448
+ const withTransferLock = (transferId, effect) => transferLocks[transferLockStripe(transferId)].withPermit(effect);
292449
+ const findTransfer = (transferId) => sql`
292450
+ SELECT transfer_id AS "transferId", workspace_root AS "workspaceRoot",
292451
+ relative_path AS "relativePath", target_path AS "targetPath", temp_path AS "tempPath",
292452
+ total_bytes AS "totalBytes", whole_sha256 AS "wholeSha256",
292453
+ next_offset AS "nextOffset", state, expires_at AS "expiresAt"
292454
+ FROM workspace_bulk_transfers WHERE transfer_id = ${transferId} LIMIT 1
292455
+ `.pipe(map$3((rows) => rows[0] ?? null));
292456
+ const requireTransfer = (transferId) => findTransfer(transferId).pipe(flatMap((row) => row === null ? fail(transferError({
292457
+ code: "transfer_not_found",
292458
+ message: "Bulk transfer does not exist.",
292459
+ transferId
292460
+ })) : succeed(row)));
292461
+ const requireOpenTransfer = (transferId) => requireTransfer(transferId).pipe(flatMap((row) => {
292462
+ if (row.state === "cancelled") return fail(transferError({
292463
+ code: "transfer_cancelled",
292464
+ message: "Bulk transfer was cancelled.",
292465
+ transferId
292466
+ }));
292467
+ if (row.state === "expired" || Date.parse(row.expiresAt) <= Date.now()) return fail(transferError({
292468
+ code: "transfer_expired",
292469
+ message: "Bulk transfer expired.",
292470
+ transferId
292471
+ }));
292472
+ return succeed(row);
292473
+ }));
292474
+ const resolveTarget = fn$1("WorkspaceBulkTransfer.resolveTarget")(function* (input) {
292475
+ const workspaceRoot = yield* workspacePaths.normalizeWorkspaceRoot(input.cwd);
292476
+ const resolved = yield* workspacePaths.resolveRelativePathWithinRoot({
292477
+ workspaceRoot,
292478
+ relativePath: input.relativePath
292479
+ });
292480
+ const canonicalRoot = yield* fileSystem.realPath(workspaceRoot);
292481
+ const parentSegments = resolved.relativePath.split("/").slice(0, -1);
292482
+ let canonicalParent = canonicalRoot;
292483
+ for (const segment of parentSegments) {
292484
+ const candidate = path.join(canonicalParent, segment);
292485
+ const canonicalCandidate = yield* fileSystem.realPath(candidate).pipe(catch_(() => input.createParent ? fileSystem.makeDirectory(candidate).pipe(flatMap(() => fileSystem.realPath(candidate))) : fileSystem.realPath(candidate)));
292486
+ if (!isPathWithinRoot(path, canonicalRoot, canonicalCandidate)) return yield* transferError({
292487
+ code: "symlink_escape",
292488
+ message: "Workspace file path resolves outside the project root."
292489
+ });
292490
+ canonicalParent = canonicalCandidate;
292491
+ }
292492
+ const targetPath = path.join(canonicalParent, path.basename(resolved.relativePath));
292493
+ const targetRealPath = yield* fileSystem.realPath(targetPath).pipe(catch_(() => succeed(null)));
292494
+ if (targetRealPath !== null && !isPathWithinRoot(path, canonicalRoot, targetRealPath)) return yield* transferError({
292495
+ code: "symlink_escape",
292496
+ message: "Workspace file target resolves outside the project root."
292497
+ });
292498
+ return {
292499
+ workspaceRoot: canonicalRoot,
292500
+ relativePath: resolved.relativePath,
292501
+ targetPath,
292502
+ canonicalParent
292503
+ };
292504
+ });
292505
+ const readFile = (input) => unwrap$1(gen(function* () {
292506
+ const target = yield* resolveTarget({
292507
+ ...input,
292508
+ createParent: false
292509
+ });
292510
+ const info = yield* fileSystem.stat(target.targetPath);
292511
+ const maxBytes = input.maxBytes ?? 16777216;
292512
+ if (Number(info.size) > maxBytes) return yield* transferError({
292513
+ code: "size_limit_exceeded",
292514
+ message: `Workspace file exceeds the ${maxBytes}-byte read limit.`
292515
+ });
292516
+ const bytes = yield* fileSystem.readFile(target.targetPath);
292517
+ const transfer = yield* promise(() => chunkBulkTransfer(bytes));
292518
+ const transferId = transfer.chunks[0]?.transferId ?? crypto.randomUUID().replaceAll("-", "_");
292519
+ return fromIterable$1([
292520
+ {
292521
+ kind: "started",
292522
+ transferId,
292523
+ totalBytes: transfer.totalBytes,
292524
+ sha256: transfer.sha256
292525
+ },
292526
+ ...transfer.chunks.map((chunk) => ({
292527
+ ...chunk,
292528
+ transferId
292529
+ })),
292530
+ {
292531
+ kind: "completed",
292532
+ transferId,
292533
+ totalBytes: transfer.totalBytes,
292534
+ sha256: transfer.sha256
292535
+ }
292536
+ ]);
292537
+ }).pipe(mapError(mapFailure("read file"))));
292538
+ const resumeWrite = fn$1("WorkspaceBulkTransfer.resumeWrite")(function* (input, existing) {
292539
+ const target = yield* resolveTarget({
292540
+ ...input,
292541
+ createParent: true
292542
+ });
292543
+ if (!(existing.workspaceRoot === target.workspaceRoot && existing.relativePath === target.relativePath && existing.totalBytes === input.totalBytes && existing.wholeSha256 === input.sha256)) return yield* transferError({
292544
+ code: "transfer_identity_conflict",
292545
+ message: "Bulk transfer ID is already bound to different content or a different path.",
292546
+ transferId: input.transferId
292547
+ });
292548
+ if (existing.state === "cancelled") return yield* transferError({
292549
+ code: "transfer_cancelled",
292550
+ message: "Bulk transfer was cancelled.",
292551
+ transferId: input.transferId
292552
+ });
292553
+ if (existing.state === "expired" || Date.parse(existing.expiresAt) <= Date.now()) return yield* transferError({
292554
+ code: "transfer_expired",
292555
+ message: "Bulk transfer expired.",
292556
+ transferId: input.transferId
292557
+ });
292558
+ return {
292559
+ transferId: input.transferId,
292560
+ nextOffset: existing.nextOffset,
292561
+ state: existing.state === "completed" ? "completed" : "open"
292562
+ };
292563
+ });
292564
+ const createWrite = fn$1("WorkspaceBulkTransfer.createWrite")(function* (input) {
292565
+ const target = yield* resolveTarget({
292566
+ ...input,
292567
+ createParent: true
292568
+ }).pipe(mapError((cause) => is$1(WorkspacePathOutsideRootError)(cause) ? transferError({
292569
+ code: "path_outside_workspace",
292570
+ message: "Workspace file path must stay within the project root.",
292571
+ transferId: input.transferId,
292572
+ cause
292573
+ }) : cause));
292574
+ const tempPath = path.join(target.canonicalParent, `.forge-transfer-${input.transferId}.part`);
292575
+ yield* scoped(fileSystem.open(tempPath, {
292576
+ flag: "wx",
292577
+ mode: 384
292578
+ }).pipe(flatMap((file) => file.sync), catch_(() => fileSystem.stat(tempPath).pipe(flatMap((stat) => Number(stat.size) === 0 ? void_$1 : fail(transferError({
292579
+ code: "transfer_identity_conflict",
292580
+ message: "Bulk transfer temporary path already contains data.",
292581
+ transferId: input.transferId
292582
+ })))))));
292583
+ const now = /* @__PURE__ */ new Date();
292584
+ yield* sql`
292585
+ INSERT INTO workspace_bulk_transfers (
292586
+ transfer_id, workspace_root, relative_path, target_path, temp_path, total_bytes,
292587
+ whole_sha256, next_offset, state, expires_at, created_at, updated_at
292588
+ ) VALUES (
292589
+ ${input.transferId}, ${target.workspaceRoot}, ${target.relativePath}, ${target.targetPath},
292590
+ ${tempPath}, ${input.totalBytes}, ${input.sha256}, 0, 'open',
292591
+ ${new Date(now.getTime() + TRANSFER_TTL_MS).toISOString()}, ${now.toISOString()},
292592
+ ${now.toISOString()}
292593
+ )
292594
+ `;
292595
+ return {
292596
+ transferId: input.transferId,
292597
+ nextOffset: 0,
292598
+ state: "open"
292599
+ };
292600
+ });
292601
+ const beginWriteEffect = fn$1("WorkspaceBulkTransfer.beginWrite")(function* (input) {
292602
+ const existing = yield* findTransfer(input.transferId);
292603
+ return yield* existing === null ? createWrite(input) : resumeWrite(input, existing);
292604
+ });
292605
+ const beginWrite = (input) => withTransferLock(input.transferId, beginWriteEffect(input)).pipe(mapError(mapFailure("begin write", input.transferId)));
292606
+ const acceptRetriedChunk = fn$1("WorkspaceBulkTransfer.acceptRetriedChunk")(function* (input, row) {
292607
+ const accepted = (yield* sql`
292608
+ SELECT chunk_offset AS "chunkOffset", chunk_bytes AS "chunkBytes",
292609
+ chunk_sha256 AS "chunkSha256"
292610
+ FROM workspace_bulk_transfer_chunks
292611
+ WHERE transfer_id = ${input.transferId} AND chunk_offset = ${input.offset}
292612
+ LIMIT 1
292613
+ `)[0];
292614
+ if (!(accepted !== void 0 && accepted.chunkBytes === input.bytes && accepted.chunkSha256 === input.sha256)) return yield* transferError({
292615
+ code: "integrity_mismatch",
292616
+ message: "Retried bulk transfer chunk differs from the accepted chunk.",
292617
+ transferId: input.transferId
292618
+ });
292619
+ return {
292620
+ transferId: input.transferId,
292621
+ nextOffset: row.nextOffset,
292622
+ duplicate: true
292623
+ };
292624
+ });
292625
+ const validateChunkRange = fn$1("WorkspaceBulkTransfer.validateChunkRange")(function* (input, row, byteLength) {
292626
+ if (input.offset !== row.nextOffset) return yield* transferError({
292627
+ code: "offset_mismatch",
292628
+ message: `Expected chunk offset ${row.nextOffset}, received ${input.offset}.`,
292629
+ transferId: input.transferId,
292630
+ retryable: true
292631
+ });
292632
+ const nextOffset = input.offset + byteLength;
292633
+ if (nextOffset > row.totalBytes) return yield* transferError({
292634
+ code: "size_limit_exceeded",
292635
+ message: "Bulk transfer chunk exceeds the declared total byte count.",
292636
+ transferId: input.transferId
292637
+ });
292638
+ return nextOffset;
292639
+ });
292640
+ const reconcileChunkBytes = fn$1("WorkspaceBulkTransfer.reconcileChunkBytes")(function* (input, row, bytes, nextOffset) {
292641
+ const tempInfo = yield* fileSystem.stat(row.tempPath);
292642
+ if (Number(tempInfo.size) > row.nextOffset) {
292643
+ if (Number(tempInfo.size) !== nextOffset) return yield* transferError({
292644
+ code: "integrity_mismatch",
292645
+ message: "Bulk transfer temporary file cannot be reconciled with the next chunk.",
292646
+ transferId: input.transferId
292647
+ });
292648
+ const recovered = (yield* fileSystem.readFile(row.tempPath)).slice(input.offset, nextOffset);
292649
+ if ((yield* promise(() => sha256Hex(recovered))) !== input.sha256) return yield* transferError({
292650
+ code: "integrity_mismatch",
292651
+ message: "Bulk transfer temporary bytes differ from the retried chunk.",
292652
+ transferId: input.transferId
292653
+ });
292654
+ return;
292655
+ }
292656
+ yield* fault("before-byte-write");
292657
+ yield* scoped(fileSystem.open(row.tempPath, { flag: "r+" }).pipe(flatMap((file) => file.seek(input.offset, "start").pipe(flatMap(() => file.writeAll(bytes)), flatMap(() => file.sync)))));
292658
+ yield* fault("after-byte-write-before-metadata");
292659
+ });
292660
+ const persistAcceptedChunk = fn$1("WorkspaceBulkTransfer.persistAcceptedChunk")(function* (input, nextOffset) {
292661
+ const updatedAt = (/* @__PURE__ */ new Date()).toISOString();
292662
+ yield* sql.withTransaction(sql`
292663
+ INSERT INTO workspace_bulk_transfer_chunks (
292664
+ transfer_id, chunk_offset, chunk_bytes, chunk_sha256
292665
+ ) VALUES (${input.transferId}, ${input.offset}, ${input.bytes}, ${input.sha256})
292666
+ ON CONFLICT DO NOTHING
292667
+ `.pipe(flatMap(() => sql`
292668
+ UPDATE workspace_bulk_transfers
292669
+ SET next_offset = ${nextOffset}, updated_at = ${updatedAt}
292670
+ WHERE transfer_id = ${input.transferId} AND next_offset = ${input.offset} AND state = 'open'
292671
+ `)));
292672
+ yield* fault("after-metadata-advance");
292673
+ });
292674
+ const writeChunkEffect = fn$1("WorkspaceBulkTransfer.writeChunk")(function* (input) {
292675
+ const bytes = yield* promise(() => verifyBulkTransferChunk({
292676
+ ...input,
292677
+ kind: "chunk"
292678
+ }));
292679
+ const row = yield* requireOpenTransfer(input.transferId);
292680
+ if (row.state === "completed") return yield* transferError({
292681
+ code: "offset_mismatch",
292682
+ message: "Bulk transfer is already complete.",
292683
+ transferId: input.transferId
292684
+ });
292685
+ if (input.offset < row.nextOffset) return yield* acceptRetriedChunk(input, row);
292686
+ const nextOffset = yield* validateChunkRange(input, row, bytes.byteLength);
292687
+ yield* reconcileChunkBytes(input, row, bytes, nextOffset);
292688
+ yield* persistAcceptedChunk(input, nextOffset);
292689
+ return {
292690
+ transferId: input.transferId,
292691
+ nextOffset,
292692
+ duplicate: false
292693
+ };
292694
+ });
292695
+ const writeChunk = (input) => withTransferLock(input.transferId, writeChunkEffect(input)).pipe(mapError(mapFailure("write chunk", input.transferId)));
292696
+ const commitWriteEffect = fn$1("WorkspaceBulkTransfer.commitWrite")(function* (input) {
292697
+ const row = yield* requireOpenTransfer(input.transferId);
292698
+ if (row.state === "completed") return {
292699
+ transferId: row.transferId,
292700
+ relativePath: row.relativePath,
292701
+ totalBytes: row.totalBytes,
292702
+ sha256: row.wholeSha256
292703
+ };
292704
+ if (row.nextOffset !== row.totalBytes) return yield* transferError({
292705
+ code: "offset_mismatch",
292706
+ message: `Bulk transfer is incomplete at byte ${row.nextOffset} of ${row.totalBytes}.`,
292707
+ transferId: row.transferId,
292708
+ retryable: true
292709
+ });
292710
+ if ((yield* resolveTarget({
292711
+ cwd: row.workspaceRoot,
292712
+ relativePath: row.relativePath,
292713
+ createParent: false
292714
+ })).targetPath !== row.targetPath) return yield* transferError({
292715
+ code: "symlink_escape",
292716
+ message: "Workspace file destination changed during transfer.",
292717
+ transferId: row.transferId
292718
+ });
292719
+ const tempInfo = yield* fileSystem.stat(row.tempPath).pipe(catch_(() => succeed(null)));
292720
+ const candidatePath = tempInfo === null ? row.targetPath : row.tempPath;
292721
+ const candidate = yield* fileSystem.readFile(candidatePath);
292722
+ if (candidate.byteLength !== row.totalBytes || (yield* promise(() => sha256Hex(candidate))) !== row.wholeSha256) return yield* transferError({
292723
+ code: "integrity_mismatch",
292724
+ message: "Bulk transfer payload does not match its declared size and digest.",
292725
+ transferId: row.transferId
292726
+ });
292727
+ if (tempInfo !== null) {
292728
+ yield* fileSystem.rename(row.tempPath, row.targetPath);
292729
+ yield* fault("after-atomic-rename");
292730
+ }
292731
+ yield* sql`
292732
+ UPDATE workspace_bulk_transfers SET state = 'completed', updated_at = ${(/* @__PURE__ */ new Date()).toISOString()}
292733
+ WHERE transfer_id = ${row.transferId}
292734
+ `;
292735
+ yield* workspaceEntries.invalidate(row.workspaceRoot);
292736
+ return {
292737
+ transferId: row.transferId,
292738
+ relativePath: row.relativePath,
292739
+ totalBytes: row.totalBytes,
292740
+ sha256: row.wholeSha256
292741
+ };
292742
+ });
292743
+ const commitWrite = (input) => withTransferLock(input.transferId, commitWriteEffect(input)).pipe(mapError(mapFailure("commit write", input.transferId)));
292744
+ const cancelWriteEffect = fn$1("WorkspaceBulkTransfer.cancelWrite")(function* (input) {
292745
+ const row = yield* requireTransfer(input.transferId);
292746
+ if (row.state !== "completed") {
292747
+ yield* fileSystem.remove(row.tempPath, { force: true });
292748
+ yield* sql`
292749
+ UPDATE workspace_bulk_transfers SET state = 'cancelled', updated_at = ${(/* @__PURE__ */ new Date()).toISOString()}
292750
+ WHERE transfer_id = ${input.transferId}
292751
+ `;
292752
+ }
292753
+ return {
292754
+ transferId: input.transferId,
292755
+ state: row.state === "completed" ? "completed" : "cancelled"
292756
+ };
292757
+ });
292758
+ const cancelWrite = (input) => withTransferLock(input.transferId, cancelWriteEffect(input)).pipe(mapError(mapFailure("cancel write", input.transferId)));
292759
+ const cleanupExpiredEffect = fn$1("WorkspaceBulkTransfer.cleanupExpired")(function* () {
292760
+ const now = (/* @__PURE__ */ new Date()).toISOString();
292761
+ const rows = yield* sql`
292762
+ SELECT transfer_id AS "transferId", temp_path AS "tempPath"
292763
+ FROM workspace_bulk_transfers WHERE state = 'open' AND expires_at <= ${now}
292764
+ `;
292765
+ let expiredCount = 0;
292766
+ for (const row of rows) if (yield* withTransferLock(row.transferId, gen(function* () {
292767
+ const current = yield* findTransfer(row.transferId);
292768
+ if (current === null || current.state !== "open" || Date.parse(current.expiresAt) > Date.parse(now)) return false;
292769
+ yield* fileSystem.remove(current.tempPath, { force: true });
292770
+ yield* sql`
292771
+ UPDATE workspace_bulk_transfers SET state = 'expired', updated_at = ${now}
292772
+ WHERE transfer_id = ${row.transferId} AND state = 'open' AND expires_at <= ${now}
292773
+ `;
292774
+ return true;
292775
+ }))) expiredCount += 1;
292776
+ return expiredCount;
292777
+ });
292778
+ const cleanupExpired = () => cleanupExpiredEffect().pipe(mapError(mapFailure("expiry cleanup")));
292779
+ return {
292780
+ readFile,
292781
+ beginWrite,
292782
+ writeChunk,
292783
+ commitWrite,
292784
+ cancelWrite,
292785
+ cleanupExpired
292786
+ };
292787
+ });
292788
+ const WorkspaceBulkTransferLive = effect(WorkspaceBulkTransfer, makeWorkspaceBulkTransfer());
292789
+ //#endregion
292687
292790
  //#region src/workspace/Layers/WorkspacePaths.ts
292688
292791
  function toPosixRelativePath(input) {
292689
292792
  return input.replaceAll("\\", "/");
@@ -294277,7 +294380,7 @@ function resolveBuildCommitFromEnv(env) {
294277
294380
  * environment descriptor down instead of reporting an honest "unknown".
294278
294381
  */
294279
294382
  function readBakedBuildCommit() {
294280
- return "8cafbb87b6204b98e93c1c69253c1c590af5f8a1";
294383
+ return "b79da36c868d5629c76dcec88a3326634a02c2c7";
294281
294384
  }
294282
294385
  async function resolveServerBuildCommit(input) {
294283
294386
  if (isFullCommitSha(input.baked)) return input.baked;
@@ -294485,7 +294588,7 @@ const ServerEnvironmentLive = effect(ServerEnvironment, fn$1("makeServerEnvironm
294485
294588
  updateDisplayName: (displayName) => gen(function* () {
294486
294589
  const normalized = normalizeDisplayName(displayName);
294487
294590
  yield* writeMetadata({ displayName: normalized }).pipe(orDie);
294488
- yield* set$2(displayNameOverrideRef, normalized);
294591
+ yield* set$3(displayNameOverrideRef, normalized);
294489
294592
  const descriptor = yield* buildDescriptor;
294490
294593
  yield* publish(changesPubSub, descriptor);
294491
294594
  return descriptor;
@@ -311400,7 +311503,7 @@ const PipelineRunCoordinatorLive = effect(PipelineRunCoordinator, gen(function*
311400
311503
  contextHash: preparedHandoff.authority.contextHash,
311401
311504
  contextSnapshot: snapshot.run.contextSnapshot
311402
311505
  }).pipe(mapError((cause) => coordinatorError("registerHandoff", "PIPELINE_CONTEXT_ARTIFACT_FAILED", "The immutable CMMD run context could not be materialized for the provider.", cause)));
311403
- const latestPreparedAuthority = yield* heartbeatPublicationSemaphore.withPermits(1)(set$2(acceptingHeartbeatRef, false).pipe(andThen(get$6(preparedAuthorityRef))));
311506
+ const latestPreparedAuthority = yield* heartbeatPublicationSemaphore.withPermits(1)(set$3(acceptingHeartbeatRef, false).pipe(andThen(get$6(preparedAuthorityRef))));
311404
311507
  const persistedAuthority = yield* persistAuthoritySecret({
311405
311508
  runId: input.runId,
311406
311509
  organizationId: latestPreparedAuthority.authority.organizationId,
@@ -311487,7 +311590,7 @@ const PipelineRunCoordinatorLive = effect(PipelineRunCoordinator, gen(function*
311487
311590
  });
311488
311591
  yield* heartbeatPublicationSemaphore.withPermits(1)(gen(function* () {
311489
311592
  if (!(yield* get$6(acceptingHeartbeatRef))) return;
311490
- yield* set$2(preparedAuthorityRef, {
311593
+ yield* set$3(preparedAuthorityRef, {
311491
311594
  authority: heartbeat.refreshedAuthority,
311492
311595
  preparation
311493
311596
  });
@@ -316261,7 +316364,7 @@ const WorkspaceReadinessStateLive = effect(WorkspaceReadinessState, gen(function
316261
316364
  const slot = yield* getOrCreateSlot(userId);
316262
316365
  const previous = yield* get$6(slot.snapshotRef);
316263
316366
  if (previous !== null && event.ts < previous.ts) return;
316264
- yield* set$2(slot.snapshotRef, event);
316367
+ yield* set$3(slot.snapshotRef, event);
316265
316368
  yield* publish(slot.pubsub, event);
316266
316369
  if (event.phase === "app-ready") yield* schedulePrune(userId, slot);
316267
316370
  });