@cmmd-center/forge 0.10.16 → 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.cjs +1390 -534
- package/dist/bin.mjs +1421 -565
- package/dist/client/assets/{DiffPanel-CniKcSxk.js → DiffPanel-27f3_FPs.js} +3 -3
- package/dist/client/assets/{DiffPanel.logic-UU4xEJJO.js → DiffPanel.logic-BuQtD_J1.js} +2 -2
- package/dist/client/assets/DiffPanelShell-BvD0Fk5A.js +2531 -0
- package/dist/client/assets/{DiffWorkerPoolProvider-B6dAKEIU.js → DiffWorkerPoolProvider-K588YXmt.js} +9 -9
- package/dist/client/assets/{PullRequestCodePanel-DHZ0dDeb.js → PullRequestCodePanel-B4HbAQDj.js} +3 -3
- package/dist/client/assets/{Virtualizer-BaXfuCj_.js → Virtualizer-DMnisVCC.js} +2 -2
- package/dist/client/assets/index-Lfu9WfF1.js +151 -0
- package/dist/client/index.html +4 -4
- package/dist/forge-build.json +1 -1
- package/package.json +1 -1
- package/dist/client/assets/DiffPanelShell-BpK-LVcn.js +0 -2531
- package/dist/client/assets/index-CpaFL0BH.js +0 -151
package/dist/bin.cjs
CHANGED
|
@@ -15773,7 +15773,7 @@ const get$1 = (self) => modify$1(self, (current) => [current, current]);
|
|
|
15773
15773
|
* })
|
|
15774
15774
|
* ```
|
|
15775
15775
|
*/
|
|
15776
|
-
const set$
|
|
15776
|
+
const set$2 = /* @__PURE__ */ require_Schema$1.dual(2, (self, value) => update$1(self, () => value));
|
|
15777
15777
|
//#endregion
|
|
15778
15778
|
//#region ../../node_modules/.bun/effect@4.0.0-beta.51/node_modules/effect/dist/TxChunk.js
|
|
15779
15779
|
/**
|
|
@@ -15962,7 +15962,7 @@ const get = (self) => get$1(self.ref);
|
|
|
15962
15962
|
* @since 4.0.0
|
|
15963
15963
|
* @category combinators
|
|
15964
15964
|
*/
|
|
15965
|
-
const set = /* @__PURE__ */ require_Schema$1.dual(2, (self, chunk) => set$
|
|
15965
|
+
const set$1 = /* @__PURE__ */ require_Schema$1.dual(2, (self, chunk) => set$2(self.ref, chunk));
|
|
15966
15966
|
/**
|
|
15967
15967
|
* Appends an element to the end of the `TxChunk`.
|
|
15968
15968
|
*
|
|
@@ -16211,7 +16211,7 @@ const take = (self) => require_Schema$1.gen(function* () {
|
|
|
16211
16211
|
const head$3 = require_Schema$1.head(chunk);
|
|
16212
16212
|
if (require_Schema$1.isNone(head$3)) return yield* require_Schema$1.txRetry;
|
|
16213
16213
|
yield* drop(self.items, 1);
|
|
16214
|
-
if (state._tag === "Closing" && (yield* isEmpty(self))) yield* set$
|
|
16214
|
+
if (state._tag === "Closing" && (yield* isEmpty(self))) yield* set$2(self.stateRef, {
|
|
16215
16215
|
_tag: "Done",
|
|
16216
16216
|
cause: state.cause
|
|
16217
16217
|
});
|
|
@@ -16309,11 +16309,11 @@ const interrupt = (self) => require_Schema$1.withFiber((fiber) => failCause(self
|
|
|
16309
16309
|
*/
|
|
16310
16310
|
const failCause = /* @__PURE__ */ require_Schema$1.dual(2, (self, cause) => require_Schema$1.gen(function* () {
|
|
16311
16311
|
if ((yield* get$1(self.stateRef))._tag !== "Open") return false;
|
|
16312
|
-
if (yield* isEmpty(self)) yield* set$
|
|
16312
|
+
if (yield* isEmpty(self)) yield* set$2(self.stateRef, {
|
|
16313
16313
|
_tag: "Done",
|
|
16314
16314
|
cause
|
|
16315
16315
|
});
|
|
16316
|
-
else yield* set$
|
|
16316
|
+
else yield* set$2(self.stateRef, {
|
|
16317
16317
|
_tag: "Closing",
|
|
16318
16318
|
cause
|
|
16319
16319
|
});
|
|
@@ -16355,7 +16355,7 @@ const clear = (self) => require_Schema$1.gen(function* () {
|
|
|
16355
16355
|
return yield* require_Schema$1.failCause(state.cause);
|
|
16356
16356
|
}
|
|
16357
16357
|
const chunk = yield* get(self.items);
|
|
16358
|
-
yield* set(self.items, require_Schema$1.empty());
|
|
16358
|
+
yield* set$1(self.items, require_Schema$1.empty());
|
|
16359
16359
|
return require_Schema$1.toArray(chunk);
|
|
16360
16360
|
}).pipe(require_Schema$1.tx);
|
|
16361
16361
|
/**
|
|
@@ -16544,7 +16544,7 @@ function makeNetServiceShape(createServer = node_net.createServer) {
|
|
|
16544
16544
|
}
|
|
16545
16545
|
//#endregion
|
|
16546
16546
|
//#region ../../packages/shared/src/releaseInfo.generated.ts
|
|
16547
|
-
const FORGE_RELEASE_VERSION = "0.
|
|
16547
|
+
const FORGE_RELEASE_VERSION = "0.11.0";
|
|
16548
16548
|
//#endregion
|
|
16549
16549
|
//#region ../../packages/shared/src/hostProcess.ts
|
|
16550
16550
|
/**
|
|
@@ -26678,6 +26678,29 @@ function encodeCmmdRuntimeThreadLifecycleMetadata(lifecycle) {
|
|
|
26678
26678
|
} };
|
|
26679
26679
|
}
|
|
26680
26680
|
//#endregion
|
|
26681
|
+
//#region ../../packages/contracts/src/cmmdRuntimeThreadWorkspace.ts
|
|
26682
|
+
function encodeCmmdRuntimeThreadWorkspaceMetadata(workspace) {
|
|
26683
|
+
return { forgeWorkspace: {
|
|
26684
|
+
schemaVersion: 1,
|
|
26685
|
+
worktreePath: workspace.worktreePath
|
|
26686
|
+
} };
|
|
26687
|
+
}
|
|
26688
|
+
//#endregion
|
|
26689
|
+
//#region ../../packages/contracts/src/cmmdRuntimeToolActivity.ts
|
|
26690
|
+
function isRecord$22(value) {
|
|
26691
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
26692
|
+
}
|
|
26693
|
+
function encodeCmmdRuntimeToolActivityPayload(kind, payload) {
|
|
26694
|
+
return {
|
|
26695
|
+
...isRecord$22(payload) ? payload : { sourcePayload: payload },
|
|
26696
|
+
forgeToolActivity: {
|
|
26697
|
+
schemaVersion: 1,
|
|
26698
|
+
source: "native",
|
|
26699
|
+
kind
|
|
26700
|
+
}
|
|
26701
|
+
};
|
|
26702
|
+
}
|
|
26703
|
+
//#endregion
|
|
26681
26704
|
//#region ../../packages/contracts/src/cmmdProjectPatch.ts
|
|
26682
26705
|
const CMMD_PROJECT_PATCH_FIELDS = {
|
|
26683
26706
|
title: "requiredText",
|
|
@@ -40972,7 +40995,7 @@ function splitPath(filename) {
|
|
|
40972
40995
|
const parts = splitPathRe.exec(truncated);
|
|
40973
40996
|
return parts ? parts.slice(1) : [];
|
|
40974
40997
|
}
|
|
40975
|
-
function dirname$
|
|
40998
|
+
function dirname$7(path) {
|
|
40976
40999
|
const result = splitPath(path);
|
|
40977
41000
|
const root = result[0] || "";
|
|
40978
41001
|
let dir = result[1];
|
|
@@ -58287,7 +58310,7 @@ function getSpotlightConfig(optionsSpotlight) {
|
|
|
58287
58310
|
function normalizeWindowsPath(path) {
|
|
58288
58311
|
return path.replace(/^[A-Z]:/, "").replace(/\\/g, "/");
|
|
58289
58312
|
}
|
|
58290
|
-
function createGetModuleFromFilename(basePath = process.argv[1] ? dirname$
|
|
58313
|
+
function createGetModuleFromFilename(basePath = process.argv[1] ? dirname$7(process.argv[1]) : process.cwd(), isWindows = node_path.sep === "\\") {
|
|
58291
58314
|
const normalizedBase = isWindows ? normalizeWindowsPath(basePath) : basePath;
|
|
58292
58315
|
return (filename) => {
|
|
58293
58316
|
if (!filename) return;
|
|
@@ -65775,7 +65798,7 @@ function normalizeNumberish(value) {
|
|
|
65775
65798
|
}
|
|
65776
65799
|
//#endregion
|
|
65777
65800
|
//#region package.json
|
|
65778
|
-
var version$1 = "0.
|
|
65801
|
+
var version$1 = "0.11.1";
|
|
65779
65802
|
//#endregion
|
|
65780
65803
|
//#region src/sentry.ts
|
|
65781
65804
|
const SERVER_APP_NAME = "forge-server";
|
|
@@ -69563,6 +69586,17 @@ const browserApiCorsLayer = middleware(require_HttpServer.cors({
|
|
|
69563
69586
|
credentials: true,
|
|
69564
69587
|
maxAge: 600
|
|
69565
69588
|
}), { global: true });
|
|
69589
|
+
/**
|
|
69590
|
+
* An /api path that reaches the static catch-all is not implemented by this
|
|
69591
|
+
* server: every real API route is registered ahead of it. Serving the web app's
|
|
69592
|
+
* index.html for those made API clients decode an HTML document as JSON, which
|
|
69593
|
+
* is how the phone's shell snapshot fetch failed with a JSON parse error on
|
|
69594
|
+
* every connect (FOR-36611). Callers can act on a 404; they cannot act on a
|
|
69595
|
+
* document that claims to be their payload.
|
|
69596
|
+
*/
|
|
69597
|
+
function isApiRequestPath(pathname) {
|
|
69598
|
+
return pathname === "/api" || pathname.startsWith("/api/");
|
|
69599
|
+
}
|
|
69566
69600
|
function resolveDevRedirectUrl(devUrl, requestUrl) {
|
|
69567
69601
|
const redirectUrl = new URL(devUrl.toString());
|
|
69568
69602
|
redirectUrl.pathname = requestUrl.pathname;
|
|
@@ -69687,6 +69721,10 @@ const staticAndDevRouteLayer = add$1("GET", "*", require_Schema$1.gen(function*
|
|
|
69687
69721
|
const request = yield* require_HttpServer.HttpServerRequest;
|
|
69688
69722
|
const url = require_HttpServer.toURL(request);
|
|
69689
69723
|
if (require_Schema$1.isNone(url)) return require_HttpServer.text("Bad Request", { status: 400 });
|
|
69724
|
+
if (isApiRequestPath(url.value.pathname)) return require_HttpServer.jsonUnsafe({
|
|
69725
|
+
error: "not_found",
|
|
69726
|
+
message: `This server does not serve ${url.value.pathname}.`
|
|
69727
|
+
}, { status: 404 });
|
|
69690
69728
|
const config = yield* ServerConfig;
|
|
69691
69729
|
if (config.devUrl && isLoopbackHostname(url.value.hostname)) return require_HttpServer.redirect(resolveDevRedirectUrl(config.devUrl, url.value), { status: 302 });
|
|
69692
69730
|
const staticDir = config.staticDir ?? (config.devUrl ? yield* resolveStaticDir() : void 0);
|
|
@@ -92992,7 +93030,7 @@ var AnalyticsService = class AnalyticsService extends require_Schema$1.Service()
|
|
|
92992
93030
|
var RuntimeReplication = class extends require_Schema$1.Service()("forge/runtimeReplication/Services/RuntimeReplication") {};
|
|
92993
93031
|
//#endregion
|
|
92994
93032
|
//#region src/runtimeDrain/forgeGitCommit.ts
|
|
92995
|
-
const execFileAsync$
|
|
93033
|
+
const execFileAsync$7 = (0, node_util.promisify)(node_child_process.execFile);
|
|
92996
93034
|
const FORGE_GIT_COMMIT_PATTERN = /^[0-9a-f]{7,40}$/i;
|
|
92997
93035
|
/**
|
|
92998
93036
|
* Resolve the commit this forge checkout actually booted from, so the
|
|
@@ -93016,7 +93054,7 @@ const FORGE_GIT_COMMIT_PATTERN = /^[0-9a-f]{7,40}$/i;
|
|
|
93016
93054
|
* it yet.
|
|
93017
93055
|
*/
|
|
93018
93056
|
let cached$1 = null;
|
|
93019
|
-
async function resolveForgeGitCommit(execImpl = (command, args) => execFileAsync$
|
|
93057
|
+
async function resolveForgeGitCommit(execImpl = (command, args) => execFileAsync$7(command, [...args], { timeout: 5e3 })) {
|
|
93020
93058
|
if (cached$1 !== null) return cached$1;
|
|
93021
93059
|
try {
|
|
93022
93060
|
const { stdout } = await execImpl("git", ["rev-parse", "HEAD"]);
|
|
@@ -94979,7 +95017,7 @@ function mightCarryUsage(line, provider) {
|
|
|
94979
95017
|
* a line at a time, so the usual cause is a tail the scan opened mid-write,
|
|
94980
95018
|
* and a count that keeps growing is a file the scan can no longer read.
|
|
94981
95019
|
*/
|
|
94982
|
-
function parseJsonObject$
|
|
95020
|
+
function parseJsonObject$2(line, diagnostics) {
|
|
94983
95021
|
let parsed;
|
|
94984
95022
|
try {
|
|
94985
95023
|
parsed = JSON.parse(line);
|
|
@@ -95009,7 +95047,7 @@ function claudeDedupeKey(messageId, requestId) {
|
|
|
95009
95047
|
return `${messageId ?? ""}:${requestId ?? ""}`;
|
|
95010
95048
|
}
|
|
95011
95049
|
function parseClaudeLine(line, diagnostics) {
|
|
95012
|
-
const record = parseJsonObject$
|
|
95050
|
+
const record = parseJsonObject$2(line, diagnostics);
|
|
95013
95051
|
if (record === null) return null;
|
|
95014
95052
|
if (record["type"] !== "assistant") return null;
|
|
95015
95053
|
const messageRecord = objectAt(record, "message");
|
|
@@ -95156,7 +95194,7 @@ function parseCodexTokenCount(record, payloadRecord, state) {
|
|
|
95156
95194
|
};
|
|
95157
95195
|
}
|
|
95158
95196
|
function parseCodexLine(line, state, diagnostics) {
|
|
95159
|
-
const record = parseJsonObject$
|
|
95197
|
+
const record = parseJsonObject$2(line, diagnostics);
|
|
95160
95198
|
if (record === null) return null;
|
|
95161
95199
|
const payloadRecord = objectAt(record, "payload");
|
|
95162
95200
|
if (payloadRecord === null) return null;
|
|
@@ -95993,10 +96031,13 @@ function serializeRuntimeInventory(snapshot, providerInventory) {
|
|
|
95993
96031
|
workerRole: thread.workerRole,
|
|
95994
96032
|
anchorKind: thread.anchorKind,
|
|
95995
96033
|
anchorCmmdId: thread.anchorCmmdId,
|
|
95996
|
-
metadata:
|
|
95997
|
-
|
|
95998
|
-
|
|
95999
|
-
|
|
96034
|
+
metadata: {
|
|
96035
|
+
...encodeCmmdRuntimeThreadLifecycleMetadata({
|
|
96036
|
+
settledAt: thread.settledAt ?? null,
|
|
96037
|
+
settledOverride: thread.settledOverride ?? null
|
|
96038
|
+
}),
|
|
96039
|
+
...encodeCmmdRuntimeThreadWorkspaceMetadata({ worktreePath: thread.worktreePath })
|
|
96040
|
+
}
|
|
96000
96041
|
})),
|
|
96001
96042
|
metadata: {
|
|
96002
96043
|
source: "forge-server-projection",
|
|
@@ -96063,7 +96104,7 @@ function serializeNativeToolActivity(activity) {
|
|
|
96063
96104
|
kind: "tool_call",
|
|
96064
96105
|
summary,
|
|
96065
96106
|
status: activityStatus(activity),
|
|
96066
|
-
|
|
96107
|
+
payload: encodeCmmdRuntimeToolActivityPayload(activity.kind, activity.payload)
|
|
96067
96108
|
}
|
|
96068
96109
|
};
|
|
96069
96110
|
}
|
|
@@ -98633,413 +98674,23 @@ function browseDirectory(input) {
|
|
|
98633
98674
|
});
|
|
98634
98675
|
}
|
|
98635
98676
|
//#endregion
|
|
98636
|
-
//#region src/
|
|
98677
|
+
//#region src/pathWithinRoot.ts
|
|
98637
98678
|
/**
|
|
98638
|
-
*
|
|
98679
|
+
* True when `candidate` is `root` itself or lies below it, judged on the path
|
|
98680
|
+
* strings alone. The path from `root` to `candidate` is taken with
|
|
98681
|
+
* `path.relative` and normalised to forward slashes, and the candidate is
|
|
98682
|
+
* outside when that path climbs out (`..` or `../...`) or is absolute, which is
|
|
98683
|
+
* what `relative` returns across Windows drives. A child whose name only starts
|
|
98684
|
+
* with two dots, such as `..cache`, is inside. Symlinks are not followed, so a
|
|
98685
|
+
* caller that cares where a link points has to compare resolved paths.
|
|
98639
98686
|
*
|
|
98640
|
-
*
|
|
98641
|
-
*
|
|
98642
|
-
*
|
|
98643
|
-
* @module WorkspaceEntries
|
|
98644
|
-
*/
|
|
98645
|
-
var WorkspaceEntriesError = class extends require_Schema$1.TaggedErrorClass()("WorkspaceEntriesError", {
|
|
98646
|
-
cwd: require_Schema$1.String,
|
|
98647
|
-
operation: require_Schema$1.String,
|
|
98648
|
-
detail: require_Schema$1.String,
|
|
98649
|
-
cause: require_Schema$1.optional(require_Schema$1.Defect)
|
|
98650
|
-
}) {};
|
|
98651
|
-
/**
|
|
98652
|
-
* WorkspaceEntries - Service tag for cached workspace entry search.
|
|
98653
|
-
*/
|
|
98654
|
-
var WorkspaceEntries = class extends require_Schema$1.Service()("forge/workspace/Services/WorkspaceEntries") {};
|
|
98655
|
-
//#endregion
|
|
98656
|
-
//#region src/workspace/Services/WorkspaceBulkTransfer.ts
|
|
98657
|
-
var WorkspaceBulkTransfer = class extends require_Schema$1.Service()("forge/workspace/Services/WorkspaceBulkTransfer") {};
|
|
98658
|
-
//#endregion
|
|
98659
|
-
//#region src/workspace/Layers/WorkspaceBulkTransfer.ts
|
|
98660
|
-
const TRANSFER_TTL_MS = 1440 * 60 * 1e3;
|
|
98661
|
-
const TRANSFER_LOCK_STRIPE_COUNT = 64;
|
|
98662
|
-
function transferError(input) {
|
|
98663
|
-
return new BulkTransferError({
|
|
98664
|
-
code: input.code,
|
|
98665
|
-
message: input.message,
|
|
98666
|
-
transferId: input.transferId ?? null,
|
|
98667
|
-
retryable: input.retryable ?? false,
|
|
98668
|
-
...input.cause === void 0 ? {} : { cause: input.cause }
|
|
98669
|
-
});
|
|
98670
|
-
}
|
|
98671
|
-
function mapFailure(operation, transferId) {
|
|
98672
|
-
return (cause) => require_Schema$1.is(BulkTransferError)(cause) ? cause : transferError({
|
|
98673
|
-
code: "io_error",
|
|
98674
|
-
message: `Workspace bulk transfer failed during ${operation}.`,
|
|
98675
|
-
...transferId === void 0 ? {} : { transferId },
|
|
98676
|
-
retryable: true,
|
|
98677
|
-
cause
|
|
98678
|
-
});
|
|
98679
|
-
}
|
|
98680
|
-
/**
|
|
98681
|
-
* Exported so the unary file read in `projectFile/readProjectFile.ts` uses this
|
|
98682
|
-
* containment check rather than a second copy of it. Two copies of a security
|
|
98683
|
-
* rule in one repo is how they drift, and the drift is silent.
|
|
98687
|
+
* Only a type import, so a module that has to stay free of Effect services can
|
|
98688
|
+
* use it without importing a Layer.
|
|
98684
98689
|
*/
|
|
98685
98690
|
function isPathWithinRoot(path, root, candidate) {
|
|
98686
98691
|
const relative = path.relative(root, candidate).replaceAll("\\", "/");
|
|
98687
98692
|
return relative === "" || !relative.startsWith("../") && relative !== ".." && !path.isAbsolute(relative);
|
|
98688
98693
|
}
|
|
98689
|
-
function transferLockStripe(transferId) {
|
|
98690
|
-
let hash = 2166136261;
|
|
98691
|
-
for (let index = 0; index < transferId.length; index += 1) hash = Math.imul(hash ^ transferId.charCodeAt(index), 16777619);
|
|
98692
|
-
return (hash >>> 0) % TRANSFER_LOCK_STRIPE_COUNT;
|
|
98693
|
-
}
|
|
98694
|
-
const makeWorkspaceBulkTransfer = (options = {}) => require_Schema$1.gen(function* () {
|
|
98695
|
-
const fileSystem = yield* require_Path.FileSystem;
|
|
98696
|
-
const path = yield* require_Path.Path;
|
|
98697
|
-
const sql = yield* require_SqlError.SqlClient;
|
|
98698
|
-
const workspacePaths = yield* WorkspacePaths;
|
|
98699
|
-
const workspaceEntries = yield* WorkspaceEntries;
|
|
98700
|
-
const fault = options.fault ?? (() => require_Schema$1.void_);
|
|
98701
|
-
const transferLocks = yield* require_Schema$1.forEach(Array.from({ length: TRANSFER_LOCK_STRIPE_COUNT }), () => require_Path.make$5(1));
|
|
98702
|
-
const withTransferLock = (transferId, effect) => transferLocks[transferLockStripe(transferId)].withPermit(effect);
|
|
98703
|
-
const findTransfer = (transferId) => sql`
|
|
98704
|
-
SELECT transfer_id AS "transferId", workspace_root AS "workspaceRoot",
|
|
98705
|
-
relative_path AS "relativePath", target_path AS "targetPath", temp_path AS "tempPath",
|
|
98706
|
-
total_bytes AS "totalBytes", whole_sha256 AS "wholeSha256",
|
|
98707
|
-
next_offset AS "nextOffset", state, expires_at AS "expiresAt"
|
|
98708
|
-
FROM workspace_bulk_transfers WHERE transfer_id = ${transferId} LIMIT 1
|
|
98709
|
-
`.pipe(require_Schema$1.map((rows) => rows[0] ?? null));
|
|
98710
|
-
const requireTransfer = (transferId) => findTransfer(transferId).pipe(require_Schema$1.flatMap((row) => row === null ? require_Schema$1.fail(transferError({
|
|
98711
|
-
code: "transfer_not_found",
|
|
98712
|
-
message: "Bulk transfer does not exist.",
|
|
98713
|
-
transferId
|
|
98714
|
-
})) : require_Schema$1.succeed(row)));
|
|
98715
|
-
const requireOpenTransfer = (transferId) => requireTransfer(transferId).pipe(require_Schema$1.flatMap((row) => {
|
|
98716
|
-
if (row.state === "cancelled") return require_Schema$1.fail(transferError({
|
|
98717
|
-
code: "transfer_cancelled",
|
|
98718
|
-
message: "Bulk transfer was cancelled.",
|
|
98719
|
-
transferId
|
|
98720
|
-
}));
|
|
98721
|
-
if (row.state === "expired" || Date.parse(row.expiresAt) <= Date.now()) return require_Schema$1.fail(transferError({
|
|
98722
|
-
code: "transfer_expired",
|
|
98723
|
-
message: "Bulk transfer expired.",
|
|
98724
|
-
transferId
|
|
98725
|
-
}));
|
|
98726
|
-
return require_Schema$1.succeed(row);
|
|
98727
|
-
}));
|
|
98728
|
-
const resolveTarget = require_Schema$1.fn("WorkspaceBulkTransfer.resolveTarget")(function* (input) {
|
|
98729
|
-
const workspaceRoot = yield* workspacePaths.normalizeWorkspaceRoot(input.cwd);
|
|
98730
|
-
const resolved = yield* workspacePaths.resolveRelativePathWithinRoot({
|
|
98731
|
-
workspaceRoot,
|
|
98732
|
-
relativePath: input.relativePath
|
|
98733
|
-
});
|
|
98734
|
-
const canonicalRoot = yield* fileSystem.realPath(workspaceRoot);
|
|
98735
|
-
const parentSegments = resolved.relativePath.split("/").slice(0, -1);
|
|
98736
|
-
let canonicalParent = canonicalRoot;
|
|
98737
|
-
for (const segment of parentSegments) {
|
|
98738
|
-
const candidate = path.join(canonicalParent, segment);
|
|
98739
|
-
const canonicalCandidate = yield* fileSystem.realPath(candidate).pipe(require_Schema$1.catch_(() => input.createParent ? fileSystem.makeDirectory(candidate).pipe(require_Schema$1.flatMap(() => fileSystem.realPath(candidate))) : fileSystem.realPath(candidate)));
|
|
98740
|
-
if (!isPathWithinRoot(path, canonicalRoot, canonicalCandidate)) return yield* transferError({
|
|
98741
|
-
code: "symlink_escape",
|
|
98742
|
-
message: "Workspace file path resolves outside the project root."
|
|
98743
|
-
});
|
|
98744
|
-
canonicalParent = canonicalCandidate;
|
|
98745
|
-
}
|
|
98746
|
-
const targetPath = path.join(canonicalParent, path.basename(resolved.relativePath));
|
|
98747
|
-
const targetRealPath = yield* fileSystem.realPath(targetPath).pipe(require_Schema$1.catch_(() => require_Schema$1.succeed(null)));
|
|
98748
|
-
if (targetRealPath !== null && !isPathWithinRoot(path, canonicalRoot, targetRealPath)) return yield* transferError({
|
|
98749
|
-
code: "symlink_escape",
|
|
98750
|
-
message: "Workspace file target resolves outside the project root."
|
|
98751
|
-
});
|
|
98752
|
-
return {
|
|
98753
|
-
workspaceRoot: canonicalRoot,
|
|
98754
|
-
relativePath: resolved.relativePath,
|
|
98755
|
-
targetPath,
|
|
98756
|
-
canonicalParent
|
|
98757
|
-
};
|
|
98758
|
-
});
|
|
98759
|
-
const readFile = (input) => require_Path.unwrap(require_Schema$1.gen(function* () {
|
|
98760
|
-
const target = yield* resolveTarget({
|
|
98761
|
-
...input,
|
|
98762
|
-
createParent: false
|
|
98763
|
-
});
|
|
98764
|
-
const info = yield* fileSystem.stat(target.targetPath);
|
|
98765
|
-
const maxBytes = input.maxBytes ?? 16777216;
|
|
98766
|
-
if (Number(info.size) > maxBytes) return yield* transferError({
|
|
98767
|
-
code: "size_limit_exceeded",
|
|
98768
|
-
message: `Workspace file exceeds the ${maxBytes}-byte read limit.`
|
|
98769
|
-
});
|
|
98770
|
-
const bytes = yield* fileSystem.readFile(target.targetPath);
|
|
98771
|
-
const transfer = yield* require_Schema$1.promise(() => chunkBulkTransfer(bytes));
|
|
98772
|
-
const transferId = transfer.chunks[0]?.transferId ?? crypto.randomUUID().replaceAll("-", "_");
|
|
98773
|
-
return require_Path.fromIterable([
|
|
98774
|
-
{
|
|
98775
|
-
kind: "started",
|
|
98776
|
-
transferId,
|
|
98777
|
-
totalBytes: transfer.totalBytes,
|
|
98778
|
-
sha256: transfer.sha256
|
|
98779
|
-
},
|
|
98780
|
-
...transfer.chunks.map((chunk) => ({
|
|
98781
|
-
...chunk,
|
|
98782
|
-
transferId
|
|
98783
|
-
})),
|
|
98784
|
-
{
|
|
98785
|
-
kind: "completed",
|
|
98786
|
-
transferId,
|
|
98787
|
-
totalBytes: transfer.totalBytes,
|
|
98788
|
-
sha256: transfer.sha256
|
|
98789
|
-
}
|
|
98790
|
-
]);
|
|
98791
|
-
}).pipe(require_Schema$1.mapError(mapFailure("read file"))));
|
|
98792
|
-
const resumeWrite = require_Schema$1.fn("WorkspaceBulkTransfer.resumeWrite")(function* (input, existing) {
|
|
98793
|
-
const target = yield* resolveTarget({
|
|
98794
|
-
...input,
|
|
98795
|
-
createParent: true
|
|
98796
|
-
});
|
|
98797
|
-
if (!(existing.workspaceRoot === target.workspaceRoot && existing.relativePath === target.relativePath && existing.totalBytes === input.totalBytes && existing.wholeSha256 === input.sha256)) return yield* transferError({
|
|
98798
|
-
code: "transfer_identity_conflict",
|
|
98799
|
-
message: "Bulk transfer ID is already bound to different content or a different path.",
|
|
98800
|
-
transferId: input.transferId
|
|
98801
|
-
});
|
|
98802
|
-
if (existing.state === "cancelled") return yield* transferError({
|
|
98803
|
-
code: "transfer_cancelled",
|
|
98804
|
-
message: "Bulk transfer was cancelled.",
|
|
98805
|
-
transferId: input.transferId
|
|
98806
|
-
});
|
|
98807
|
-
if (existing.state === "expired" || Date.parse(existing.expiresAt) <= Date.now()) return yield* transferError({
|
|
98808
|
-
code: "transfer_expired",
|
|
98809
|
-
message: "Bulk transfer expired.",
|
|
98810
|
-
transferId: input.transferId
|
|
98811
|
-
});
|
|
98812
|
-
return {
|
|
98813
|
-
transferId: input.transferId,
|
|
98814
|
-
nextOffset: existing.nextOffset,
|
|
98815
|
-
state: existing.state === "completed" ? "completed" : "open"
|
|
98816
|
-
};
|
|
98817
|
-
});
|
|
98818
|
-
const createWrite = require_Schema$1.fn("WorkspaceBulkTransfer.createWrite")(function* (input) {
|
|
98819
|
-
const target = yield* resolveTarget({
|
|
98820
|
-
...input,
|
|
98821
|
-
createParent: true
|
|
98822
|
-
}).pipe(require_Schema$1.mapError((cause) => require_Schema$1.is(WorkspacePathOutsideRootError)(cause) ? transferError({
|
|
98823
|
-
code: "path_outside_workspace",
|
|
98824
|
-
message: "Workspace file path must stay within the project root.",
|
|
98825
|
-
transferId: input.transferId,
|
|
98826
|
-
cause
|
|
98827
|
-
}) : cause));
|
|
98828
|
-
const tempPath = path.join(target.canonicalParent, `.forge-transfer-${input.transferId}.part`);
|
|
98829
|
-
yield* require_Schema$1.scoped(fileSystem.open(tempPath, {
|
|
98830
|
-
flag: "wx",
|
|
98831
|
-
mode: 384
|
|
98832
|
-
}).pipe(require_Schema$1.flatMap((file) => file.sync), require_Schema$1.catch_(() => fileSystem.stat(tempPath).pipe(require_Schema$1.flatMap((stat) => Number(stat.size) === 0 ? require_Schema$1.void_ : require_Schema$1.fail(transferError({
|
|
98833
|
-
code: "transfer_identity_conflict",
|
|
98834
|
-
message: "Bulk transfer temporary path already contains data.",
|
|
98835
|
-
transferId: input.transferId
|
|
98836
|
-
})))))));
|
|
98837
|
-
const now = /* @__PURE__ */ new Date();
|
|
98838
|
-
yield* sql`
|
|
98839
|
-
INSERT INTO workspace_bulk_transfers (
|
|
98840
|
-
transfer_id, workspace_root, relative_path, target_path, temp_path, total_bytes,
|
|
98841
|
-
whole_sha256, next_offset, state, expires_at, created_at, updated_at
|
|
98842
|
-
) VALUES (
|
|
98843
|
-
${input.transferId}, ${target.workspaceRoot}, ${target.relativePath}, ${target.targetPath},
|
|
98844
|
-
${tempPath}, ${input.totalBytes}, ${input.sha256}, 0, 'open',
|
|
98845
|
-
${new Date(now.getTime() + TRANSFER_TTL_MS).toISOString()}, ${now.toISOString()},
|
|
98846
|
-
${now.toISOString()}
|
|
98847
|
-
)
|
|
98848
|
-
`;
|
|
98849
|
-
return {
|
|
98850
|
-
transferId: input.transferId,
|
|
98851
|
-
nextOffset: 0,
|
|
98852
|
-
state: "open"
|
|
98853
|
-
};
|
|
98854
|
-
});
|
|
98855
|
-
const beginWriteEffect = require_Schema$1.fn("WorkspaceBulkTransfer.beginWrite")(function* (input) {
|
|
98856
|
-
const existing = yield* findTransfer(input.transferId);
|
|
98857
|
-
return yield* existing === null ? createWrite(input) : resumeWrite(input, existing);
|
|
98858
|
-
});
|
|
98859
|
-
const beginWrite = (input) => withTransferLock(input.transferId, beginWriteEffect(input)).pipe(require_Schema$1.mapError(mapFailure("begin write", input.transferId)));
|
|
98860
|
-
const acceptRetriedChunk = require_Schema$1.fn("WorkspaceBulkTransfer.acceptRetriedChunk")(function* (input, row) {
|
|
98861
|
-
const accepted = (yield* sql`
|
|
98862
|
-
SELECT chunk_offset AS "chunkOffset", chunk_bytes AS "chunkBytes",
|
|
98863
|
-
chunk_sha256 AS "chunkSha256"
|
|
98864
|
-
FROM workspace_bulk_transfer_chunks
|
|
98865
|
-
WHERE transfer_id = ${input.transferId} AND chunk_offset = ${input.offset}
|
|
98866
|
-
LIMIT 1
|
|
98867
|
-
`)[0];
|
|
98868
|
-
if (!(accepted !== void 0 && accepted.chunkBytes === input.bytes && accepted.chunkSha256 === input.sha256)) return yield* transferError({
|
|
98869
|
-
code: "integrity_mismatch",
|
|
98870
|
-
message: "Retried bulk transfer chunk differs from the accepted chunk.",
|
|
98871
|
-
transferId: input.transferId
|
|
98872
|
-
});
|
|
98873
|
-
return {
|
|
98874
|
-
transferId: input.transferId,
|
|
98875
|
-
nextOffset: row.nextOffset,
|
|
98876
|
-
duplicate: true
|
|
98877
|
-
};
|
|
98878
|
-
});
|
|
98879
|
-
const validateChunkRange = require_Schema$1.fn("WorkspaceBulkTransfer.validateChunkRange")(function* (input, row, byteLength) {
|
|
98880
|
-
if (input.offset !== row.nextOffset) return yield* transferError({
|
|
98881
|
-
code: "offset_mismatch",
|
|
98882
|
-
message: `Expected chunk offset ${row.nextOffset}, received ${input.offset}.`,
|
|
98883
|
-
transferId: input.transferId,
|
|
98884
|
-
retryable: true
|
|
98885
|
-
});
|
|
98886
|
-
const nextOffset = input.offset + byteLength;
|
|
98887
|
-
if (nextOffset > row.totalBytes) return yield* transferError({
|
|
98888
|
-
code: "size_limit_exceeded",
|
|
98889
|
-
message: "Bulk transfer chunk exceeds the declared total byte count.",
|
|
98890
|
-
transferId: input.transferId
|
|
98891
|
-
});
|
|
98892
|
-
return nextOffset;
|
|
98893
|
-
});
|
|
98894
|
-
const reconcileChunkBytes = require_Schema$1.fn("WorkspaceBulkTransfer.reconcileChunkBytes")(function* (input, row, bytes, nextOffset) {
|
|
98895
|
-
const tempInfo = yield* fileSystem.stat(row.tempPath);
|
|
98896
|
-
if (Number(tempInfo.size) > row.nextOffset) {
|
|
98897
|
-
if (Number(tempInfo.size) !== nextOffset) return yield* transferError({
|
|
98898
|
-
code: "integrity_mismatch",
|
|
98899
|
-
message: "Bulk transfer temporary file cannot be reconciled with the next chunk.",
|
|
98900
|
-
transferId: input.transferId
|
|
98901
|
-
});
|
|
98902
|
-
const recovered = (yield* fileSystem.readFile(row.tempPath)).slice(input.offset, nextOffset);
|
|
98903
|
-
if ((yield* require_Schema$1.promise(() => sha256Hex(recovered))) !== input.sha256) return yield* transferError({
|
|
98904
|
-
code: "integrity_mismatch",
|
|
98905
|
-
message: "Bulk transfer temporary bytes differ from the retried chunk.",
|
|
98906
|
-
transferId: input.transferId
|
|
98907
|
-
});
|
|
98908
|
-
return;
|
|
98909
|
-
}
|
|
98910
|
-
yield* fault("before-byte-write");
|
|
98911
|
-
yield* require_Schema$1.scoped(fileSystem.open(row.tempPath, { flag: "r+" }).pipe(require_Schema$1.flatMap((file) => file.seek(input.offset, "start").pipe(require_Schema$1.flatMap(() => file.writeAll(bytes)), require_Schema$1.flatMap(() => file.sync)))));
|
|
98912
|
-
yield* fault("after-byte-write-before-metadata");
|
|
98913
|
-
});
|
|
98914
|
-
const persistAcceptedChunk = require_Schema$1.fn("WorkspaceBulkTransfer.persistAcceptedChunk")(function* (input, nextOffset) {
|
|
98915
|
-
const updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
98916
|
-
yield* sql.withTransaction(sql`
|
|
98917
|
-
INSERT INTO workspace_bulk_transfer_chunks (
|
|
98918
|
-
transfer_id, chunk_offset, chunk_bytes, chunk_sha256
|
|
98919
|
-
) VALUES (${input.transferId}, ${input.offset}, ${input.bytes}, ${input.sha256})
|
|
98920
|
-
ON CONFLICT DO NOTHING
|
|
98921
|
-
`.pipe(require_Schema$1.flatMap(() => sql`
|
|
98922
|
-
UPDATE workspace_bulk_transfers
|
|
98923
|
-
SET next_offset = ${nextOffset}, updated_at = ${updatedAt}
|
|
98924
|
-
WHERE transfer_id = ${input.transferId} AND next_offset = ${input.offset} AND state = 'open'
|
|
98925
|
-
`)));
|
|
98926
|
-
yield* fault("after-metadata-advance");
|
|
98927
|
-
});
|
|
98928
|
-
const writeChunkEffect = require_Schema$1.fn("WorkspaceBulkTransfer.writeChunk")(function* (input) {
|
|
98929
|
-
const bytes = yield* require_Schema$1.promise(() => verifyBulkTransferChunk({
|
|
98930
|
-
...input,
|
|
98931
|
-
kind: "chunk"
|
|
98932
|
-
}));
|
|
98933
|
-
const row = yield* requireOpenTransfer(input.transferId);
|
|
98934
|
-
if (row.state === "completed") return yield* transferError({
|
|
98935
|
-
code: "offset_mismatch",
|
|
98936
|
-
message: "Bulk transfer is already complete.",
|
|
98937
|
-
transferId: input.transferId
|
|
98938
|
-
});
|
|
98939
|
-
if (input.offset < row.nextOffset) return yield* acceptRetriedChunk(input, row);
|
|
98940
|
-
const nextOffset = yield* validateChunkRange(input, row, bytes.byteLength);
|
|
98941
|
-
yield* reconcileChunkBytes(input, row, bytes, nextOffset);
|
|
98942
|
-
yield* persistAcceptedChunk(input, nextOffset);
|
|
98943
|
-
return {
|
|
98944
|
-
transferId: input.transferId,
|
|
98945
|
-
nextOffset,
|
|
98946
|
-
duplicate: false
|
|
98947
|
-
};
|
|
98948
|
-
});
|
|
98949
|
-
const writeChunk = (input) => withTransferLock(input.transferId, writeChunkEffect(input)).pipe(require_Schema$1.mapError(mapFailure("write chunk", input.transferId)));
|
|
98950
|
-
const commitWriteEffect = require_Schema$1.fn("WorkspaceBulkTransfer.commitWrite")(function* (input) {
|
|
98951
|
-
const row = yield* requireOpenTransfer(input.transferId);
|
|
98952
|
-
if (row.state === "completed") return {
|
|
98953
|
-
transferId: row.transferId,
|
|
98954
|
-
relativePath: row.relativePath,
|
|
98955
|
-
totalBytes: row.totalBytes,
|
|
98956
|
-
sha256: row.wholeSha256
|
|
98957
|
-
};
|
|
98958
|
-
if (row.nextOffset !== row.totalBytes) return yield* transferError({
|
|
98959
|
-
code: "offset_mismatch",
|
|
98960
|
-
message: `Bulk transfer is incomplete at byte ${row.nextOffset} of ${row.totalBytes}.`,
|
|
98961
|
-
transferId: row.transferId,
|
|
98962
|
-
retryable: true
|
|
98963
|
-
});
|
|
98964
|
-
if ((yield* resolveTarget({
|
|
98965
|
-
cwd: row.workspaceRoot,
|
|
98966
|
-
relativePath: row.relativePath,
|
|
98967
|
-
createParent: false
|
|
98968
|
-
})).targetPath !== row.targetPath) return yield* transferError({
|
|
98969
|
-
code: "symlink_escape",
|
|
98970
|
-
message: "Workspace file destination changed during transfer.",
|
|
98971
|
-
transferId: row.transferId
|
|
98972
|
-
});
|
|
98973
|
-
const tempInfo = yield* fileSystem.stat(row.tempPath).pipe(require_Schema$1.catch_(() => require_Schema$1.succeed(null)));
|
|
98974
|
-
const candidatePath = tempInfo === null ? row.targetPath : row.tempPath;
|
|
98975
|
-
const candidate = yield* fileSystem.readFile(candidatePath);
|
|
98976
|
-
if (candidate.byteLength !== row.totalBytes || (yield* require_Schema$1.promise(() => sha256Hex(candidate))) !== row.wholeSha256) return yield* transferError({
|
|
98977
|
-
code: "integrity_mismatch",
|
|
98978
|
-
message: "Bulk transfer payload does not match its declared size and digest.",
|
|
98979
|
-
transferId: row.transferId
|
|
98980
|
-
});
|
|
98981
|
-
if (tempInfo !== null) {
|
|
98982
|
-
yield* fileSystem.rename(row.tempPath, row.targetPath);
|
|
98983
|
-
yield* fault("after-atomic-rename");
|
|
98984
|
-
}
|
|
98985
|
-
yield* sql`
|
|
98986
|
-
UPDATE workspace_bulk_transfers SET state = 'completed', updated_at = ${(/* @__PURE__ */ new Date()).toISOString()}
|
|
98987
|
-
WHERE transfer_id = ${row.transferId}
|
|
98988
|
-
`;
|
|
98989
|
-
yield* workspaceEntries.invalidate(row.workspaceRoot);
|
|
98990
|
-
return {
|
|
98991
|
-
transferId: row.transferId,
|
|
98992
|
-
relativePath: row.relativePath,
|
|
98993
|
-
totalBytes: row.totalBytes,
|
|
98994
|
-
sha256: row.wholeSha256
|
|
98995
|
-
};
|
|
98996
|
-
});
|
|
98997
|
-
const commitWrite = (input) => withTransferLock(input.transferId, commitWriteEffect(input)).pipe(require_Schema$1.mapError(mapFailure("commit write", input.transferId)));
|
|
98998
|
-
const cancelWriteEffect = require_Schema$1.fn("WorkspaceBulkTransfer.cancelWrite")(function* (input) {
|
|
98999
|
-
const row = yield* requireTransfer(input.transferId);
|
|
99000
|
-
if (row.state !== "completed") {
|
|
99001
|
-
yield* fileSystem.remove(row.tempPath, { force: true });
|
|
99002
|
-
yield* sql`
|
|
99003
|
-
UPDATE workspace_bulk_transfers SET state = 'cancelled', updated_at = ${(/* @__PURE__ */ new Date()).toISOString()}
|
|
99004
|
-
WHERE transfer_id = ${input.transferId}
|
|
99005
|
-
`;
|
|
99006
|
-
}
|
|
99007
|
-
return {
|
|
99008
|
-
transferId: input.transferId,
|
|
99009
|
-
state: row.state === "completed" ? "completed" : "cancelled"
|
|
99010
|
-
};
|
|
99011
|
-
});
|
|
99012
|
-
const cancelWrite = (input) => withTransferLock(input.transferId, cancelWriteEffect(input)).pipe(require_Schema$1.mapError(mapFailure("cancel write", input.transferId)));
|
|
99013
|
-
const cleanupExpiredEffect = require_Schema$1.fn("WorkspaceBulkTransfer.cleanupExpired")(function* () {
|
|
99014
|
-
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
99015
|
-
const rows = yield* sql`
|
|
99016
|
-
SELECT transfer_id AS "transferId", temp_path AS "tempPath"
|
|
99017
|
-
FROM workspace_bulk_transfers WHERE state = 'open' AND expires_at <= ${now}
|
|
99018
|
-
`;
|
|
99019
|
-
let expiredCount = 0;
|
|
99020
|
-
for (const row of rows) if (yield* withTransferLock(row.transferId, require_Schema$1.gen(function* () {
|
|
99021
|
-
const current = yield* findTransfer(row.transferId);
|
|
99022
|
-
if (current === null || current.state !== "open" || Date.parse(current.expiresAt) > Date.parse(now)) return false;
|
|
99023
|
-
yield* fileSystem.remove(current.tempPath, { force: true });
|
|
99024
|
-
yield* sql`
|
|
99025
|
-
UPDATE workspace_bulk_transfers SET state = 'expired', updated_at = ${now}
|
|
99026
|
-
WHERE transfer_id = ${row.transferId} AND state = 'open' AND expires_at <= ${now}
|
|
99027
|
-
`;
|
|
99028
|
-
return true;
|
|
99029
|
-
}))) expiredCount += 1;
|
|
99030
|
-
return expiredCount;
|
|
99031
|
-
});
|
|
99032
|
-
const cleanupExpired = () => cleanupExpiredEffect().pipe(require_Schema$1.mapError(mapFailure("expiry cleanup")));
|
|
99033
|
-
return {
|
|
99034
|
-
readFile,
|
|
99035
|
-
beginWrite,
|
|
99036
|
-
writeChunk,
|
|
99037
|
-
commitWrite,
|
|
99038
|
-
cancelWrite,
|
|
99039
|
-
cleanupExpired
|
|
99040
|
-
};
|
|
99041
|
-
});
|
|
99042
|
-
const WorkspaceBulkTransferLive = require_Schema$1.effect(WorkspaceBulkTransfer, makeWorkspaceBulkTransfer());
|
|
99043
98694
|
//#endregion
|
|
99044
98695
|
//#region src/projectFile/readProjectFile.ts
|
|
99045
98696
|
/**
|
|
@@ -99537,7 +99188,7 @@ function buildDiscoveryResult(probes) {
|
|
|
99537
99188
|
}
|
|
99538
99189
|
//#endregion
|
|
99539
99190
|
//#region src/sourceControl/probe.ts
|
|
99540
|
-
const execFileAsync$
|
|
99191
|
+
const execFileAsync$6 = (0, node_util.promisify)(node_child_process.execFile);
|
|
99541
99192
|
/**
|
|
99542
99193
|
* How long a machine's toolchain answer stays good for.
|
|
99543
99194
|
*
|
|
@@ -99548,7 +99199,7 @@ const execFileAsync$5 = (0, node_util.promisify)(node_child_process.execFile);
|
|
|
99548
99199
|
*/
|
|
99549
99200
|
const DISCOVERY_TTL = "5 minutes";
|
|
99550
99201
|
/** Probes time out because `gh auth status` reaches the network on a cold keyring. */
|
|
99551
|
-
const PROBE_TIMEOUT_MS = 5e3;
|
|
99202
|
+
const PROBE_TIMEOUT_MS$1 = 5e3;
|
|
99552
99203
|
/**
|
|
99553
99204
|
* Runs one probe. A tool that is absent, fails, or hangs is reported the same
|
|
99554
99205
|
* way — `null`, meaning "no answer" — because the discovery result only
|
|
@@ -99560,7 +99211,7 @@ const PROBE_TIMEOUT_MS = 5e3;
|
|
|
99560
99211
|
*/
|
|
99561
99212
|
const probe = (command, args) => require_Schema$1.promise(async () => {
|
|
99562
99213
|
try {
|
|
99563
|
-
const { stdout, stderr } = await execFileAsync$
|
|
99214
|
+
const { stdout, stderr } = await execFileAsync$6(command, [...args], { timeout: PROBE_TIMEOUT_MS$1 });
|
|
99564
99215
|
const output = `${stdout}\n${stderr}`.trim();
|
|
99565
99216
|
return { output: output.length === 0 ? null : output };
|
|
99566
99217
|
} catch (cause) {
|
|
@@ -99843,6 +99494,26 @@ const DevTunnelLive = require_Schema$1.effect(DevTunnel, require_Schema$1.gen(fu
|
|
|
99843
99494
|
};
|
|
99844
99495
|
}));
|
|
99845
99496
|
//#endregion
|
|
99497
|
+
//#region src/workspace/Services/WorkspaceEntries.ts
|
|
99498
|
+
/**
|
|
99499
|
+
* WorkspaceEntries - Effect service contract for cached workspace entry search.
|
|
99500
|
+
*
|
|
99501
|
+
* Owns indexed workspace entry search plus cache invalidation for workspace
|
|
99502
|
+
* roots when the underlying filesystem changes.
|
|
99503
|
+
*
|
|
99504
|
+
* @module WorkspaceEntries
|
|
99505
|
+
*/
|
|
99506
|
+
var WorkspaceEntriesError = class extends require_Schema$1.TaggedErrorClass()("WorkspaceEntriesError", {
|
|
99507
|
+
cwd: require_Schema$1.String,
|
|
99508
|
+
operation: require_Schema$1.String,
|
|
99509
|
+
detail: require_Schema$1.String,
|
|
99510
|
+
cause: require_Schema$1.optional(require_Schema$1.Defect)
|
|
99511
|
+
}) {};
|
|
99512
|
+
/**
|
|
99513
|
+
* WorkspaceEntries - Service tag for cached workspace entry search.
|
|
99514
|
+
*/
|
|
99515
|
+
var WorkspaceEntries = class extends require_Schema$1.Service()("forge/workspace/Services/WorkspaceEntries") {};
|
|
99516
|
+
//#endregion
|
|
99846
99517
|
//#region src/workspace/Services/WorkspaceFileSystem.ts
|
|
99847
99518
|
/**
|
|
99848
99519
|
* WorkspaceFileSystem - Effect service contract for workspace file mutations.
|
|
@@ -99864,6 +99535,9 @@ var WorkspaceFileSystemError = class extends require_Schema$1.TaggedErrorClass()
|
|
|
99864
99535
|
*/
|
|
99865
99536
|
var WorkspaceFileSystem = class extends require_Schema$1.Service()("forge/workspace/Services/WorkspaceFileSystem") {};
|
|
99866
99537
|
//#endregion
|
|
99538
|
+
//#region src/workspace/Services/WorkspaceBulkTransfer.ts
|
|
99539
|
+
var WorkspaceBulkTransfer = class extends require_Schema$1.Service()("forge/workspace/Services/WorkspaceBulkTransfer") {};
|
|
99540
|
+
//#endregion
|
|
99867
99541
|
//#region src/workspace/Services/WorkspaceReadinessState.ts
|
|
99868
99542
|
var WorkspaceReadinessState = class extends require_Schema$1.Service()("forge/workspace/Services/WorkspaceReadinessState") {};
|
|
99869
99543
|
//#endregion
|
|
@@ -101099,6 +100773,7 @@ const CLAUDE_IDENTITY_KEYS = new Set([
|
|
|
101099
100773
|
"provider",
|
|
101100
100774
|
"credentialGenerationId"
|
|
101101
100775
|
]);
|
|
100776
|
+
const CLAUDE_IDENTITY_OPTIONAL_KEYS = new Set(["providerAccountIdentity"]);
|
|
101102
100777
|
function isRecord$13(value) {
|
|
101103
100778
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
101104
100779
|
}
|
|
@@ -101123,6 +100798,10 @@ function daysInMonth$1(year, month) {
|
|
|
101123
100798
|
function hasExactKeys$2(value, keys) {
|
|
101124
100799
|
return Object.keys(value).every((key) => keys.has(key)) && Object.keys(value).length === keys.size;
|
|
101125
100800
|
}
|
|
100801
|
+
function hasAllowedKeys(value, required, optional) {
|
|
100802
|
+
const keys = Object.keys(value);
|
|
100803
|
+
return keys.every((key) => required.has(key) || optional.has(key)) && [...required].every((key) => keys.includes(key));
|
|
100804
|
+
}
|
|
101126
100805
|
function parseDeliveryIdentity(value, provider) {
|
|
101127
100806
|
if (!isRecord$13(value)) return null;
|
|
101128
100807
|
if (provider === "openai") return parseCodexDeliveryIdentity(value);
|
|
@@ -101137,12 +100816,25 @@ function parseCodexDeliveryIdentity(value) {
|
|
|
101137
100816
|
providerAccountIdentity: value.providerAccountIdentity
|
|
101138
100817
|
};
|
|
101139
100818
|
}
|
|
100819
|
+
/**
|
|
100820
|
+
* Absent means the writer does not name the Provider Account yet, and the
|
|
100821
|
+
* identity keeps the shape it has always had. Present but malformed means the
|
|
100822
|
+
* identity is not the one CMMD signed, so it is refused the same way a
|
|
100823
|
+
* malformed Codex account identity is.
|
|
100824
|
+
*/
|
|
100825
|
+
function claudeAccountFields(value) {
|
|
100826
|
+
if (value === void 0) return {};
|
|
100827
|
+
if (typeof value !== "string" || !PROVIDER_ACCOUNT_IDENTITY$2.test(value)) return null;
|
|
100828
|
+
return { providerAccountIdentity: value };
|
|
100829
|
+
}
|
|
101140
100830
|
function parseClaudeDeliveryIdentity(value) {
|
|
101141
|
-
|
|
100831
|
+
const account = claudeAccountFields(value.providerAccountIdentity);
|
|
100832
|
+
if (!account || !hasAllowedKeys(value, CLAUDE_IDENTITY_KEYS, CLAUDE_IDENTITY_OPTIONAL_KEYS) || value.kind !== "credential_generation" || value.provider !== "claude" || typeof value.credentialGenerationId !== "string" || !UUID.test(value.credentialGenerationId)) return null;
|
|
101142
100833
|
return {
|
|
101143
100834
|
kind: "credential_generation",
|
|
101144
100835
|
provider: "claude",
|
|
101145
|
-
credentialGenerationId: value.credentialGenerationId
|
|
100836
|
+
credentialGenerationId: value.credentialGenerationId,
|
|
100837
|
+
...account
|
|
101146
100838
|
};
|
|
101147
100839
|
}
|
|
101148
100840
|
function parseProviderRuntimeDeliveryReceipt(value, expected) {
|
|
@@ -101181,10 +100873,49 @@ function readProviderDeliveryBinding(value) {
|
|
|
101181
100873
|
identity
|
|
101182
100874
|
};
|
|
101183
100875
|
}
|
|
100876
|
+
/** The Provider Account a delivery identity names, or null when it names none. */
|
|
100877
|
+
function providerDeliveryAccount(identity) {
|
|
100878
|
+
return identity.kind === "provider_account" ? identity.providerAccountIdentity : identity.providerAccountIdentity ?? null;
|
|
100879
|
+
}
|
|
100880
|
+
function sameDeliveredCredential(left, right) {
|
|
100881
|
+
if (left.kind === "provider_account" && right.kind === "provider_account") return left.providerAccountIdentity === right.providerAccountIdentity;
|
|
100882
|
+
return left.kind === "credential_generation" && right.kind === "credential_generation" && left.credentialGenerationId === right.credentialGenerationId;
|
|
100883
|
+
}
|
|
100884
|
+
/**
|
|
100885
|
+
* Names how one thread's delivery binding moved, which is the question the
|
|
100886
|
+
* delivery layer could not answer while Claude's identity carried no account.
|
|
100887
|
+
*
|
|
100888
|
+
* `account_switch` needs both sides to name an account, the accounts to differ,
|
|
100889
|
+
* the delivered credential to change with them, and the runtime, machine and
|
|
100890
|
+
* epoch to hold. A switch changes the account and the generation together, so
|
|
100891
|
+
* two accounts on one generation would be two accounts sharing a credential,
|
|
100892
|
+
* and a switch that also moved the epoch changed more than the identity. Both
|
|
100893
|
+
* are `unknown`.
|
|
100894
|
+
*
|
|
100895
|
+
* The same delivered credential is `unchanged` whether or not either side names
|
|
100896
|
+
* an account, so a binding recorded before CMMD named the account still resumes
|
|
100897
|
+
* against the credential it ran on. A changed credential where either side
|
|
100898
|
+
* names no account is `unknown` rather than guessed at, which keeps an
|
|
100899
|
+
* account-less binding compared on exactly the fields it was compared on before.
|
|
100900
|
+
*
|
|
100901
|
+
* `providerDeliveryBindingsEqual` asks only whether the answer is `unchanged`.
|
|
100902
|
+
* `permitsProviderAccountTransition` in `provider/providerAccountTransition.ts`
|
|
100903
|
+
* asks whether it is `account_switch`, and is the Codex resume gate's rule
|
|
100904
|
+
* today. #3055 wires the Claude resume gate onto it.
|
|
100905
|
+
*/
|
|
100906
|
+
function classifyProviderDeliveryBindingChange(prior, current) {
|
|
100907
|
+
if (prior.runtimeId !== current.runtimeId || prior.machineId !== current.machineId || prior.identity.kind !== current.identity.kind || prior.identity.provider !== current.identity.provider) return "unknown";
|
|
100908
|
+
const sameEpoch = prior.credentialEpoch === current.credentialEpoch;
|
|
100909
|
+
const sameCredential = sameDeliveredCredential(prior.identity, current.identity);
|
|
100910
|
+
const priorAccount = providerDeliveryAccount(prior.identity);
|
|
100911
|
+
const currentAccount = providerDeliveryAccount(current.identity);
|
|
100912
|
+
if (priorAccount !== null && currentAccount !== null && priorAccount !== currentAccount) return sameEpoch && !sameCredential ? "account_switch" : "unknown";
|
|
100913
|
+
if (sameEpoch && sameCredential) return "unchanged";
|
|
100914
|
+
if (priorAccount === null || currentAccount === null) return "unknown";
|
|
100915
|
+
return "credential_refresh";
|
|
100916
|
+
}
|
|
101184
100917
|
function providerDeliveryBindingsEqual(left, right) {
|
|
101185
|
-
|
|
101186
|
-
if (left.identity.kind === "provider_account" && right.identity.kind === "provider_account") return left.identity.providerAccountIdentity === right.identity.providerAccountIdentity;
|
|
101187
|
-
return left.identity.kind === "credential_generation" && right.identity.kind === "credential_generation" && left.identity.credentialGenerationId === right.identity.credentialGenerationId;
|
|
100918
|
+
return classifyProviderDeliveryBindingChange(left, right) === "unchanged";
|
|
101188
100919
|
}
|
|
101189
100920
|
//#endregion
|
|
101190
100921
|
//#region src/provider/claudeReadinessReason.ts
|
|
@@ -101463,8 +101194,128 @@ const runClaudeLogout = (runtime) => new Promise((resolve, reject) => {
|
|
|
101463
101194
|
});
|
|
101464
101195
|
});
|
|
101465
101196
|
//#endregion
|
|
101197
|
+
//#region src/usage/providerRateLimits.ts
|
|
101198
|
+
/**
|
|
101199
|
+
* `new Date(ms).toISOString()` throws past +-8.64e15 ms, so a `resetsAt` past
|
|
101200
|
+
* the same bound in seconds is refused rather than allowed to throw inside a
|
|
101201
|
+
* decoder whose whole job is to fail closed.
|
|
101202
|
+
*/
|
|
101203
|
+
const MAX_RESETS_AT_SECONDS = 864e10;
|
|
101204
|
+
/**
|
|
101205
|
+
* One reset time in unix seconds as an ISO string, or null.
|
|
101206
|
+
*
|
|
101207
|
+
* Both mappers read reset times through this, so a malformed `resetsAt`
|
|
101208
|
+
* yields the null that means "this observation said nothing" instead of a
|
|
101209
|
+
* RangeError thrown out of a decoder the ingestion layer runs on every Claude
|
|
101210
|
+
* rate-limit event.
|
|
101211
|
+
*/
|
|
101212
|
+
function resetsAtIso(resetsAt) {
|
|
101213
|
+
if (!Number.isFinite(resetsAt) || resetsAt <= 0 || resetsAt > MAX_RESETS_AT_SECONDS) return null;
|
|
101214
|
+
return (/* @__PURE__ */ new Date(resetsAt * 1e3)).toISOString();
|
|
101215
|
+
}
|
|
101216
|
+
/**
|
|
101217
|
+
* The SDK only ever reports ONE window per event. Only `five_hour` and
|
|
101218
|
+
* `seven_day` map to the two windows the Usage page shows; the
|
|
101219
|
+
* per-model/overage variants (seven_day_opus, overage, ...) are ignored here
|
|
101220
|
+
* on purpose rather than silently overwriting the plain 7-day figure.
|
|
101221
|
+
*/
|
|
101222
|
+
function mapClaudeRateLimitInfo(info) {
|
|
101223
|
+
if (typeof info.utilization !== "number" || typeof info.resetsAt !== "number" || !info.status) return null;
|
|
101224
|
+
const kind = info.rateLimitType === "five_hour" ? "fiveHour" : info.rateLimitType === "seven_day" ? "sevenDay" : null;
|
|
101225
|
+
if (!kind) return null;
|
|
101226
|
+
const resetsAt = resetsAtIso(info.resetsAt);
|
|
101227
|
+
if (resetsAt === null) return null;
|
|
101228
|
+
return {
|
|
101229
|
+
kind,
|
|
101230
|
+
window: {
|
|
101231
|
+
usedPercent: Math.round(info.utilization * 100),
|
|
101232
|
+
resetsAt,
|
|
101233
|
+
status: info.status
|
|
101234
|
+
}
|
|
101235
|
+
};
|
|
101236
|
+
}
|
|
101237
|
+
/**
|
|
101238
|
+
* The one place a used-percent becomes a status. `unifiedWindows` carries no
|
|
101239
|
+
* per-window status of its own, and the `/usage` parser derives one the same
|
|
101240
|
+
* way, so both Claude usage sources agree on where "warning" starts.
|
|
101241
|
+
*/
|
|
101242
|
+
function usageStatus(usedPercent) {
|
|
101243
|
+
if (usedPercent >= 100) return "rejected";
|
|
101244
|
+
return usedPercent >= 80 ? "allowed_warning" : "allowed";
|
|
101245
|
+
}
|
|
101246
|
+
/**
|
|
101247
|
+
* One untyped `utilization` as a whole percent, or nothing. The [0, 100] bound
|
|
101248
|
+
* matches `claudeUsageCli.ts`'s own bound on the `/usage` text, which in turn
|
|
101249
|
+
* matches CMMD's parser.
|
|
101250
|
+
*/
|
|
101251
|
+
function readUsedPercent(utilization) {
|
|
101252
|
+
if (typeof utilization !== "number" || !Number.isFinite(utilization)) return null;
|
|
101253
|
+
const usedPercent = Math.round(utilization * 100);
|
|
101254
|
+
return usedPercent < 0 || usedPercent > 100 ? null : usedPercent;
|
|
101255
|
+
}
|
|
101256
|
+
/**
|
|
101257
|
+
* Reads one `unifiedWindows` entry, or nothing. Every field is checked because
|
|
101258
|
+
* none of them are typed: a non-object entry, a utilization that is not a
|
|
101259
|
+
* finite number in [0, 1], or a reset time that is not a positive finite
|
|
101260
|
+
* number of seconds all mean "this observation said nothing about this
|
|
101261
|
+
* window".
|
|
101262
|
+
*/
|
|
101263
|
+
function readUnifiedWindow(raw) {
|
|
101264
|
+
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) return null;
|
|
101265
|
+
const { utilization, resetsAt } = raw;
|
|
101266
|
+
const usedPercent = readUsedPercent(utilization);
|
|
101267
|
+
if (usedPercent === null) return null;
|
|
101268
|
+
if (typeof resetsAt !== "number") return null;
|
|
101269
|
+
const resetsAtIsoValue = resetsAtIso(resetsAt);
|
|
101270
|
+
if (resetsAtIsoValue === null) return null;
|
|
101271
|
+
return {
|
|
101272
|
+
usedPercent,
|
|
101273
|
+
resetsAt: resetsAtIsoValue,
|
|
101274
|
+
status: usageStatus(usedPercent)
|
|
101275
|
+
};
|
|
101276
|
+
}
|
|
101277
|
+
/**
|
|
101278
|
+
* Reads the `unifiedWindows` sub-object of `rate_limit_info` into the two
|
|
101279
|
+
* rolling windows. The SDK rebuilds that sub-object on every turn from the
|
|
101280
|
+
* `anthropic-ratelimit-unified-{5h,7d}-utilization` and `-reset` response
|
|
101281
|
+
* headers.
|
|
101282
|
+
*
|
|
101283
|
+
* The top-level `rateLimitType` names one window per event and carries a
|
|
101284
|
+
* utilization only for `five_hour` or `seven_day`. This sub-object reports
|
|
101285
|
+
* every window it knows on every observation, which makes it the only
|
|
101286
|
+
* SDK-side source that can feed the Usage card without waking a Sprite and
|
|
101287
|
+
* delivering a credential.
|
|
101288
|
+
*
|
|
101289
|
+
* Its `seven_day_overage_included` window is skipped. `/usage` names each model
|
|
101290
|
+
* row after the display name Anthropic's usage API gives that model, one row
|
|
101291
|
+
* per allowlisted model, and the `7d_oi` header behind this window is one
|
|
101292
|
+
* aggregate that carries no name. Any name given to it here would be a guess
|
|
101293
|
+
* that could disagree with the rows the probe writes, so model rows come from
|
|
101294
|
+
* the probe alone.
|
|
101295
|
+
*
|
|
101296
|
+
* It is also undocumented internal surface stripped from the SDK's exported
|
|
101297
|
+
* types, so this decoder fails closed everywhere. It returns null when the
|
|
101298
|
+
* sub-object is absent or is not a plain object, and null when neither window
|
|
101299
|
+
* in it survives validation. A window that fails validation is reported as
|
|
101300
|
+
* null, and an unrecognized key is skipped, never fatal. A caller that gets
|
|
101301
|
+
* null has learned nothing and must not publish, because a partial write could
|
|
101302
|
+
* overwrite a good `/usage` reading with a guess.
|
|
101303
|
+
*/
|
|
101304
|
+
function mapClaudeUnifiedWindows(info) {
|
|
101305
|
+
const raw = info.unifiedWindows;
|
|
101306
|
+
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) return null;
|
|
101307
|
+
const windows = raw;
|
|
101308
|
+
const fiveHour = readUnifiedWindow(windows.five_hour);
|
|
101309
|
+
const sevenDay = readUnifiedWindow(windows.seven_day);
|
|
101310
|
+
if (!fiveHour && !sevenDay) return null;
|
|
101311
|
+
return {
|
|
101312
|
+
fiveHour,
|
|
101313
|
+
sevenDay
|
|
101314
|
+
};
|
|
101315
|
+
}
|
|
101316
|
+
//#endregion
|
|
101466
101317
|
//#region src/usage/claudeUsageCli.ts
|
|
101467
|
-
const execFileAsync$
|
|
101318
|
+
const execFileAsync$5 = (0, node_util.promisify)(node_child_process.execFile);
|
|
101468
101319
|
const USAGE_ARGS = [
|
|
101469
101320
|
"--safe-mode",
|
|
101470
101321
|
"--no-session-persistence",
|
|
@@ -101488,7 +101339,7 @@ const MONTH_BY_NAME = {
|
|
|
101488
101339
|
Dec: 11
|
|
101489
101340
|
};
|
|
101490
101341
|
async function runClaudeUsageCommand(binaryPath, args, claudeConfigDir) {
|
|
101491
|
-
const { stdout } = await execFileAsync$
|
|
101342
|
+
const { stdout } = await execFileAsync$5(binaryPath, [...args], {
|
|
101492
101343
|
env: {
|
|
101493
101344
|
...process.env,
|
|
101494
101345
|
TZ: "UTC",
|
|
@@ -101563,10 +101414,6 @@ function parseResetAt(value, now) {
|
|
|
101563
101414
|
});
|
|
101564
101415
|
return resetAt === null || Number.isNaN(resetAt.getTime()) ? null : resetAt.toISOString();
|
|
101565
101416
|
}
|
|
101566
|
-
function usageStatus(usedPercent) {
|
|
101567
|
-
if (usedPercent >= 100) return "rejected";
|
|
101568
|
-
return usedPercent >= 80 ? "allowed_warning" : "allowed";
|
|
101569
|
-
}
|
|
101570
101417
|
function parseUsageLine(output, label, now) {
|
|
101571
101418
|
const escapedLabel = label.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
101572
101419
|
const match = new RegExp(`^${escapedLabel}: (\\d+(?:\\.\\d+)?)% used(?: · resets (.+))?$`, "m").exec(output);
|
|
@@ -101635,6 +101482,29 @@ async function fetchClaudeUsageViaOfficialClient(input) {
|
|
|
101635
101482
|
};
|
|
101636
101483
|
}
|
|
101637
101484
|
//#endregion
|
|
101485
|
+
//#region src/usage/claudeUsageTelemetryRequest.ts
|
|
101486
|
+
/** The owner-session route for a Claude usage reading. */
|
|
101487
|
+
const CLAUDE_USAGE_TELEMETRY_PATH = "/api/forge/context/providers/usage/claude/telemetry";
|
|
101488
|
+
/**
|
|
101489
|
+
* Builds the body for either lane.
|
|
101490
|
+
*
|
|
101491
|
+
* `modelWindows` is sent only when the reading carries it. CMMD keeps the
|
|
101492
|
+
* stored model rows when the key is absent, so a reading that cannot name
|
|
101493
|
+
* model rows the way `/usage` does leaves the key out.
|
|
101494
|
+
*/
|
|
101495
|
+
function claudeUsageTelemetryBody(input) {
|
|
101496
|
+
return {
|
|
101497
|
+
providerAccountId: input.attribution.providerAccountId,
|
|
101498
|
+
credentialGenerationId: input.attribution.credentialGenerationId,
|
|
101499
|
+
...input.account,
|
|
101500
|
+
source: input.source,
|
|
101501
|
+
fiveHour: input.windows.fiveHour,
|
|
101502
|
+
sevenDay: input.windows.sevenDay,
|
|
101503
|
+
...input.windows.modelWindows !== void 0 ? { modelWindows: input.windows.modelWindows } : {},
|
|
101504
|
+
observedAt: input.observedAt
|
|
101505
|
+
};
|
|
101506
|
+
}
|
|
101507
|
+
//#endregion
|
|
101638
101508
|
//#region src/cmmd/syncClaudeCredential.ts
|
|
101639
101509
|
/**
|
|
101640
101510
|
* Card 1359 (harvest path A, Claude) — publish a personal Claude subscription
|
|
@@ -101650,7 +101520,6 @@ async function fetchClaudeUsageViaOfficialClient(input) {
|
|
|
101650
101520
|
const USER_CLAUDE_CREDENTIAL_PATH = "/api/forge/context/providers/claude/credential";
|
|
101651
101521
|
const USER_CLAUDE_LINKED_ACCOUNT_PATH = "/api/forge/context/providers/claude/accounts";
|
|
101652
101522
|
const AUTH_RUNTIME_CLAUDE_CREDENTIAL_PATH = "/api/forge/context/auth-runtime-claude-credential";
|
|
101653
|
-
const USER_CLAUDE_USAGE_PATH = "/api/forge/context/providers/usage/claude/telemetry";
|
|
101654
101523
|
const AUTH_RUNTIME_CLAUDE_USAGE_PATH = "/api/forge/context/auth-runtime-claude-credential/usage";
|
|
101655
101524
|
/** Path to the local Claude credentials.json, mirroring Claude Code's resolution. */
|
|
101656
101525
|
function resolveLocalClaudeCredentialPath(homePath) {
|
|
@@ -101679,7 +101548,7 @@ function resolveLocalClaudeCredentialPath(homePath) {
|
|
|
101679
101548
|
* it reaches — this is the first gate and the only one near the user, who is
|
|
101680
101549
|
* standing in front of the sign-in and can simply try again.
|
|
101681
101550
|
*
|
|
101682
|
-
* The floor is ONE token, not both,
|
|
101551
|
+
* The floor is ONE token, not both, because an
|
|
101683
101552
|
* expired or blank access token beside a live refresh token is the normal
|
|
101684
101553
|
* resting state of a rotating login, which Claude Code refreshes silently on
|
|
101685
101554
|
* use. Demanding both would reject a perfectly recoverable credential and break
|
|
@@ -101757,10 +101626,10 @@ function extractClaudeAccountMetadata(credentialJson) {
|
|
|
101757
101626
|
return {};
|
|
101758
101627
|
}
|
|
101759
101628
|
}
|
|
101760
|
-
const execFileAsync$
|
|
101629
|
+
const execFileAsync$4 = (0, node_util.promisify)(node_child_process.execFile);
|
|
101761
101630
|
async function loadClaudeAccountMetadataFromCli(homePath) {
|
|
101762
101631
|
try {
|
|
101763
|
-
const { stdout } = await execFileAsync$
|
|
101632
|
+
const { stdout } = await execFileAsync$4(resolveClaudeBinaryPath("claude"), ["auth", "status"], {
|
|
101764
101633
|
timeout: 2e4,
|
|
101765
101634
|
maxBuffer: 1024 * 1024,
|
|
101766
101635
|
env: {
|
|
@@ -101856,18 +101725,20 @@ async function publishClaudeUsageAfterSync(input) {
|
|
|
101856
101725
|
binaryPath: resolveClaudeBinaryPath("claude"),
|
|
101857
101726
|
...input.homePath ? { claudeConfigDir: input.homePath } : {}
|
|
101858
101727
|
});
|
|
101859
|
-
await publishUsageBody(input.auth.jwt, input.auth.path === AUTH_RUNTIME_CLAUDE_CREDENTIAL_PATH ? AUTH_RUNTIME_CLAUDE_USAGE_PATH :
|
|
101860
|
-
|
|
101861
|
-
|
|
101862
|
-
|
|
101863
|
-
|
|
101864
|
-
|
|
101728
|
+
await publishUsageBody(input.auth.jwt, input.auth.path === AUTH_RUNTIME_CLAUDE_CREDENTIAL_PATH ? AUTH_RUNTIME_CLAUDE_USAGE_PATH : CLAUDE_USAGE_TELEMETRY_PATH, claudeUsageTelemetryBody({
|
|
101729
|
+
attribution: {
|
|
101730
|
+
providerAccountId: input.providerAccountId,
|
|
101731
|
+
credentialGenerationId: input.credentialGenerationId
|
|
101732
|
+
},
|
|
101733
|
+
account: {
|
|
101734
|
+
accountEmail: input.accountEmail,
|
|
101735
|
+
accountDisplayName: input.accountDisplayName,
|
|
101736
|
+
planType: input.planType
|
|
101737
|
+
},
|
|
101865
101738
|
source: "active_poll",
|
|
101866
|
-
|
|
101867
|
-
sevenDay: usage.sevenDay,
|
|
101868
|
-
modelWindows: usage.modelWindows,
|
|
101739
|
+
windows: usage,
|
|
101869
101740
|
observedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
101870
|
-
});
|
|
101741
|
+
}));
|
|
101871
101742
|
return null;
|
|
101872
101743
|
} catch (cause) {
|
|
101873
101744
|
return { cause };
|
|
@@ -150195,7 +150066,7 @@ function issueCmmdLocalUserProviderEgressCredential(input) {
|
|
|
150195
150066
|
});
|
|
150196
150067
|
}
|
|
150197
150068
|
//#endregion
|
|
150198
|
-
//#region src/provider/
|
|
150069
|
+
//#region src/provider/providerAccountTransition.ts
|
|
150199
150070
|
function explicitProviderAccountTransition(fromAccountId, toAccountId) {
|
|
150200
150071
|
if (!fromAccountId || !toAccountId || fromAccountId === toAccountId) return void 0;
|
|
150201
150072
|
return {
|
|
@@ -150203,13 +150074,20 @@ function explicitProviderAccountTransition(fromAccountId, toAccountId) {
|
|
|
150203
150074
|
toAccountId
|
|
150204
150075
|
};
|
|
150205
150076
|
}
|
|
150206
|
-
|
|
150077
|
+
/**
|
|
150078
|
+
* Whether a thread may move from `prior` to `current` because the person asked
|
|
150079
|
+
* for another Provider Account. The credential broker authorizes the selected
|
|
150080
|
+
* account, so only the identity may change, which is what `account_switch`
|
|
150081
|
+
* means.
|
|
150082
|
+
*
|
|
150083
|
+
* Nothing in the rule is specific to a provider, but the Codex resume gate in
|
|
150084
|
+
* `codexAppServerManager.ts` is its only caller. The Claude half is parked
|
|
150085
|
+
* until #3055 wires the Claude resume gate onto it.
|
|
150086
|
+
*/
|
|
150087
|
+
function permitsProviderAccountTransition(input) {
|
|
150207
150088
|
const { transition, requestedAccountId, prior, current } = input;
|
|
150208
|
-
if (!transition || transition.fromAccountId === transition.toAccountId || transition.toAccountId !== requestedAccountId
|
|
150209
|
-
return
|
|
150210
|
-
...prior,
|
|
150211
|
-
identity: current.identity
|
|
150212
|
-
}, current);
|
|
150089
|
+
if (!transition || transition.fromAccountId === transition.toAccountId || transition.toAccountId !== requestedAccountId) return false;
|
|
150090
|
+
return classifyProviderDeliveryBindingChange(prior, current) === "account_switch";
|
|
150213
150091
|
}
|
|
150214
150092
|
//#endregion
|
|
150215
150093
|
//#region src/providerAuth/codexLocalAccountIdentity.ts
|
|
@@ -150709,7 +150587,7 @@ var CodexAppServerManager = class extends node_events.EventEmitter {
|
|
|
150709
150587
|
const rawPriorDeliveryBinding = input.resumeCursor.providerDeliveryBinding;
|
|
150710
150588
|
const priorDeliveryBinding = readProviderDeliveryBinding(rawPriorDeliveryBinding);
|
|
150711
150589
|
if (rawPriorDeliveryBinding !== void 0 && rawPriorDeliveryBinding !== null && !priorDeliveryBinding) throw new Error("Existing Codex history cannot resume because its persisted delivered provider account binding is unreadable.");
|
|
150712
|
-
if (priorDeliveryBinding && !providerDeliveryBindingsEqual(priorDeliveryBinding, currentDeliveryBinding) && !
|
|
150590
|
+
if (priorDeliveryBinding && !providerDeliveryBindingsEqual(priorDeliveryBinding, currentDeliveryBinding) && !permitsProviderAccountTransition({
|
|
150713
150591
|
transition: input.accountTransition,
|
|
150714
150592
|
requestedAccountId: input.providerAccountId,
|
|
150715
150593
|
prior: priorDeliveryBinding,
|
|
@@ -176783,7 +176661,7 @@ function em(e) {
|
|
|
176783
176661
|
BO ??= new Int32Array(new SharedArrayBuffer(4)), Atomics.wait(BO, 0, 0, SY);
|
|
176784
176662
|
}
|
|
176785
176663
|
}
|
|
176786
|
-
function rm$
|
|
176664
|
+
function rm$6(e, t) {
|
|
176787
176665
|
let n = (0, path.resolve)(t).toLowerCase(), r = (0, path.dirname)((0, path.resolve)(e)).toLowerCase();
|
|
176788
176666
|
if (r === n || KO(r, n) || qO(r, n, "lexical")) return !0;
|
|
176789
176667
|
let o = em(t)?.toLowerCase();
|
|
@@ -189633,7 +189511,7 @@ function Wo() {
|
|
|
189633
189511
|
}
|
|
189634
189512
|
function sle(e) {
|
|
189635
189513
|
let t = process.cwd();
|
|
189636
|
-
return e.filter((n) => !rm$
|
|
189514
|
+
return e.filter((n) => !rm$6(n, t));
|
|
189637
189515
|
}
|
|
189638
189516
|
function ZU(e) {
|
|
189639
189517
|
let t = Bun.which(e);
|
|
@@ -211140,11 +211018,243 @@ function hasConfiguredApexCredential(profiles) {
|
|
|
211140
211018
|
}));
|
|
211141
211019
|
}
|
|
211142
211020
|
//#endregion
|
|
211021
|
+
//#region src/provider/claudeCredentialDirectory.ts
|
|
211022
|
+
/**
|
|
211023
|
+
* Per-account credential directories for Claude turns.
|
|
211024
|
+
*
|
|
211025
|
+
* Claude Code keeps conversation transcripts under
|
|
211026
|
+
* `${CLAUDE_CONFIG_DIR}/projects/<cwd-slug>/<sessionId>.jsonl` and its login
|
|
211027
|
+
* under `.credentials.json`. `CLAUDE_SECURESTORAGE_CONFIG_DIR` moves only the
|
|
211028
|
+
* credential file, so each Provider Account can hold its own login while every
|
|
211029
|
+
* session file stays in one place and `--resume` keeps finding its target. See
|
|
211030
|
+
* `docs/adr/claude-accounts-split-credentials-not-sessions.md`.
|
|
211031
|
+
*
|
|
211032
|
+
* That variable is not documented. Under
|
|
211033
|
+
* `docs/adr/unpublished-provider-surface-needs-a-fallback.md` the dependency is
|
|
211034
|
+
* only allowed because absence degrades to a named behaviour that already
|
|
211035
|
+
* works: a thread with no account runs on the Environment's own login. This
|
|
211036
|
+
* module sets the variable only when the resolved directory holds a credential
|
|
211037
|
+
* the CLI can use, because a missing file, or one whose access token a refusal
|
|
211038
|
+
* has blanked, makes the CLI report logged out and a live turn would fail.
|
|
211039
|
+
*
|
|
211040
|
+
* Nothing writes these directories yet. #3049 adds the writer and keys each
|
|
211041
|
+
* directory on the account identity CMMD writes on the delivery receipt it
|
|
211042
|
+
* signs (`docs/adr/provider-account-identity-is-issued-by-cmmd.md`). Until then
|
|
211043
|
+
* every runtime takes the `no-account` skip.
|
|
211044
|
+
*
|
|
211045
|
+
* Free of Effect services on purpose, for the same reason as
|
|
211046
|
+
* `claudeCredentialContainment.ts`: ClaudeAdapter drags in Effect platform
|
|
211047
|
+
* services, the browser manager and the server config, so a unit test that
|
|
211048
|
+
* imports it cannot run in a worktree. This rule decides which account pays for
|
|
211049
|
+
* a turn, so it has to stay directly testable.
|
|
211050
|
+
*/
|
|
211051
|
+
/** Repoints `.credentials.json` without moving the transcripts. */
|
|
211052
|
+
const CLAUDE_SECURESTORAGE_CONFIG_DIR_ENV = "CLAUDE_SECURESTORAGE_CONFIG_DIR";
|
|
211053
|
+
/** The one file Claude Code writes into the credential directory. */
|
|
211054
|
+
const CLAUDE_CREDENTIAL_FILE_NAME = ".credentials.json";
|
|
211055
|
+
/**
|
|
211056
|
+
* Runtime env var naming the Provider Account this runtime's Claude turns
|
|
211057
|
+
* belong to. Nothing in Forge or CMMD sets it, so no runtime names an account
|
|
211058
|
+
* and each keeps its one login.
|
|
211059
|
+
*
|
|
211060
|
+
* It is a bridge that will not get a writer. #3073 settled the key in
|
|
211061
|
+
* `docs/adr/provider-account-identity-is-issued-by-cmmd.md`: the directory name
|
|
211062
|
+
* comes from the identity CMMD issues and writes on the delivery receipt it
|
|
211063
|
+
* signs, and this variable goes away. #3049 makes that change. The credential
|
|
211064
|
+
* generation was ruled out as a key: it rotates on every reconnect, and CMMD
|
|
211065
|
+
* uses it as the primary account's id, so keying on it would strand every
|
|
211066
|
+
* thread pinned to that account on a directory that stops receiving deliveries.
|
|
211067
|
+
*
|
|
211068
|
+
* Tests reach the `credentialDirectory` parameter on
|
|
211069
|
+
* `ClaudeAdapter.buildClaudeQueryEnv` directly.
|
|
211070
|
+
*/
|
|
211071
|
+
const CLAUDE_PROVIDER_ACCOUNT_IDENTITY_ENV = "FORGE_CLAUDE_PROVIDER_ACCOUNT_IDENTITY";
|
|
211072
|
+
/** Directory under the server's base directory that holds one directory per account. */
|
|
211073
|
+
const CREDENTIAL_ROOT_DIRECTORY_NAME = "claude-credentials";
|
|
211074
|
+
const DEFAULT_CLAUDE_CONFIG_DIRECTORY_NAME = ".claude";
|
|
211075
|
+
const MAX_ACCOUNT_IDENTITY_LENGTH = 256;
|
|
211076
|
+
const DIRECTORY_NAME_HASH_LENGTH = 32;
|
|
211077
|
+
/**
|
|
211078
|
+
* Directory name for an account. A hash, so no email or display name reaches a
|
|
211079
|
+
* filesystem path and no path-illegal character can appear in one either.
|
|
211080
|
+
*/
|
|
211081
|
+
function claudeCredentialDirectoryName(accountIdentity) {
|
|
211082
|
+
return `account-${(0, node_crypto.createHash)("sha256").update(accountIdentity, "utf8").digest("hex").slice(0, DIRECTORY_NAME_HASH_LENGTH)}`;
|
|
211083
|
+
}
|
|
211084
|
+
/** The account this runtime's Claude turns belong to, or null while the variable is unset. */
|
|
211085
|
+
function resolveClaudeProviderAccountIdentity(env) {
|
|
211086
|
+
const identity = env[CLAUDE_PROVIDER_ACCOUNT_IDENTITY_ENV]?.trim();
|
|
211087
|
+
if (!identity || identity.length > MAX_ACCOUNT_IDENTITY_LENGTH) return null;
|
|
211088
|
+
return identity;
|
|
211089
|
+
}
|
|
211090
|
+
/**
|
|
211091
|
+
* Where per-account credential directories live: `claude-credentials` under the
|
|
211092
|
+
* server's base directory, `ServerConfig.baseDir`, which is `FORGE_HOME` or
|
|
211093
|
+
* `~/.forge`. A delivery writer gets the same `ServerConfig`, so reader and
|
|
211094
|
+
* writer cannot disagree about the root. Always outside `CLAUDE_CONFIG_DIR`, so
|
|
211095
|
+
* one of these can never be mistaken for the main credential store, and a stray
|
|
211096
|
+
* read of the config directory cannot walk into another account's login.
|
|
211097
|
+
*/
|
|
211098
|
+
function claudeCredentialDirectoryRoot(env, baseDir) {
|
|
211099
|
+
if (!(0, node_path.isAbsolute)(baseDir)) return null;
|
|
211100
|
+
const root = (0, node_path.resolve)(baseDir, CREDENTIAL_ROOT_DIRECTORY_NAME);
|
|
211101
|
+
const configDirectory = claudeConfigDirectory(env);
|
|
211102
|
+
if (configDirectory && isPathWithinRoot({
|
|
211103
|
+
relative: node_path.relative,
|
|
211104
|
+
isAbsolute: node_path.isAbsolute
|
|
211105
|
+
}, configDirectory, root)) return null;
|
|
211106
|
+
return root;
|
|
211107
|
+
}
|
|
211108
|
+
/** Absolute credential directory for one account, or null when either half is missing. */
|
|
211109
|
+
function resolveClaudeCredentialDirectory(input) {
|
|
211110
|
+
if (!input.accountIdentity) return null;
|
|
211111
|
+
const root = claudeCredentialDirectoryRoot(input.env, input.baseDir);
|
|
211112
|
+
if (!root) return null;
|
|
211113
|
+
return (0, node_path.join)(root, claudeCredentialDirectoryName(input.accountIdentity));
|
|
211114
|
+
}
|
|
211115
|
+
/**
|
|
211116
|
+
* Decide whether this turn may point at a per-account credential directory.
|
|
211117
|
+
*
|
|
211118
|
+
* The cheap checks run first and the capability probe runs last, so a runtime
|
|
211119
|
+
* with no usable credential never spawns the CLI. The probe's error channel is
|
|
211120
|
+
* empty, so nothing here catches.
|
|
211121
|
+
*
|
|
211122
|
+
* A skip is not a fallback onto the default account. With no account the turn
|
|
211123
|
+
* runs on the Environment's own login, unchanged. A thread pinned to an account
|
|
211124
|
+
* whose credential is missing is refused by name under
|
|
211125
|
+
* `docs/adr/provider-account-identity-is-issued-by-cmmd.md`, and #3049 turns
|
|
211126
|
+
* `no-usable-credential`, and `capability-not-supported` for a pinned account,
|
|
211127
|
+
* into those refusals. Until #3049 nothing names an account, so `no-account` is
|
|
211128
|
+
* the only skip a runtime sees.
|
|
211129
|
+
*/
|
|
211130
|
+
function decideClaudeCredentialDirectory(input) {
|
|
211131
|
+
return require_Schema$1.gen(function* () {
|
|
211132
|
+
if (!input.accountIdentity) return skip("no-account");
|
|
211133
|
+
if (input.env["CLAUDE_SECURESTORAGE_CONFIG_DIR"]?.trim()) return skip("operator-override");
|
|
211134
|
+
const directory = resolveClaudeCredentialDirectory(input);
|
|
211135
|
+
if (!directory) return skip("no-root");
|
|
211136
|
+
if (!holdsUsableCredential(directory)) return skip("no-usable-credential");
|
|
211137
|
+
return (yield* input.probeSupport) === "supported" ? set(directory) : skip("capability-not-supported");
|
|
211138
|
+
});
|
|
211139
|
+
}
|
|
211140
|
+
/**
|
|
211141
|
+
* True when `.credentials.json` parses and carries a non-empty access token.
|
|
211142
|
+
*
|
|
211143
|
+
* File presence is not readiness. A refused credential is rewritten in place
|
|
211144
|
+
* with `accessToken: ""`, so a file that exists can still leave the CLI logged
|
|
211145
|
+
* out. Only the token's length is checked. The value is never logged, returned
|
|
211146
|
+
* or kept.
|
|
211147
|
+
*/
|
|
211148
|
+
function holdsUsableCredential(directory) {
|
|
211149
|
+
const file = (0, node_path.join)(directory, CLAUDE_CREDENTIAL_FILE_NAME);
|
|
211150
|
+
try {
|
|
211151
|
+
if (!(0, node_fs.statSync)(file).isFile()) return false;
|
|
211152
|
+
return hasAccessToken(JSON.parse((0, node_fs.readFileSync)(file, "utf8")));
|
|
211153
|
+
} catch {
|
|
211154
|
+
return false;
|
|
211155
|
+
}
|
|
211156
|
+
}
|
|
211157
|
+
function hasAccessToken(credential) {
|
|
211158
|
+
if (typeof credential !== "object" || credential === null) return false;
|
|
211159
|
+
const oauth = credential.claudeAiOauth;
|
|
211160
|
+
if (typeof oauth !== "object" || oauth === null) return false;
|
|
211161
|
+
const token = oauth.accessToken;
|
|
211162
|
+
return typeof token === "string" && token.length > 0;
|
|
211163
|
+
}
|
|
211164
|
+
function set(directory) {
|
|
211165
|
+
return {
|
|
211166
|
+
outcome: "set",
|
|
211167
|
+
directory
|
|
211168
|
+
};
|
|
211169
|
+
}
|
|
211170
|
+
function skip(reason) {
|
|
211171
|
+
return {
|
|
211172
|
+
outcome: "skipped",
|
|
211173
|
+
reason
|
|
211174
|
+
};
|
|
211175
|
+
}
|
|
211176
|
+
function claudeConfigDirectory(env) {
|
|
211177
|
+
const configured = env.CLAUDE_CONFIG_DIR?.trim();
|
|
211178
|
+
if (configured) return configured;
|
|
211179
|
+
try {
|
|
211180
|
+
return (0, node_path.join)((0, node_os.homedir)(), DEFAULT_CLAUDE_CONFIG_DIRECTORY_NAME);
|
|
211181
|
+
} catch {
|
|
211182
|
+
return null;
|
|
211183
|
+
}
|
|
211184
|
+
}
|
|
211185
|
+
//#endregion
|
|
211186
|
+
//#region src/provider/personalClaudeInVm.ts
|
|
211187
|
+
/**
|
|
211188
|
+
* CMMD writes this marker when it activates a personal Claude subscription
|
|
211189
|
+
* inside a running sprite. Activation always happens after boot, so a
|
|
211190
|
+
* boot-time env var alone cannot reflect it (FOR-36249).
|
|
211191
|
+
*
|
|
211192
|
+
* CMMD names the file in the Service env (FORGE_PERSONAL_CLAUDE_MARKER_PATH).
|
|
211193
|
+
* Without that, the marker sits directly under FORGE_HOME, which is the same
|
|
211194
|
+
* guest home CMMD derives every sprite path from. A server with neither has no
|
|
211195
|
+
* sprite home to look in, and a desktop never has a marker, so it reads none.
|
|
211196
|
+
*/
|
|
211197
|
+
const PERSONAL_CLAUDE_IN_VM_MARKER_NAME = ".forge-personal-claude-in-vm";
|
|
211198
|
+
const PERSONAL_CLAUDE_GENERATION_FILE_NAME = ".forge-personal-claude-generation";
|
|
211199
|
+
function markerPath(env) {
|
|
211200
|
+
if (env.FORGE_PERSONAL_CLAUDE_MARKER_PATH) return env.FORGE_PERSONAL_CLAUDE_MARKER_PATH;
|
|
211201
|
+
if (env.FORGE_HOME) return (0, node_path.join)(env.FORGE_HOME, PERSONAL_CLAUDE_IN_VM_MARKER_NAME);
|
|
211202
|
+
return null;
|
|
211203
|
+
}
|
|
211204
|
+
/**
|
|
211205
|
+
* True when this runtime should offer the personal Claude subscription lane.
|
|
211206
|
+
*
|
|
211207
|
+
* Accepts either signal: the boot-time env var (backward compatible) or the
|
|
211208
|
+
* activation marker on disk. Reading the marker at call time lets the provider
|
|
211209
|
+
* catalog and the per-turn readiness guard self-heal once CMMD delivers,
|
|
211210
|
+
* without a sprite reboot. Callers still verify the credential is non-empty
|
|
211211
|
+
* before treating it as usable; this only decides whether the lane is active.
|
|
211212
|
+
*/
|
|
211213
|
+
function personalClaudeInVmEnabled(env = process.env) {
|
|
211214
|
+
if (env.FORGE_PERSONAL_CLAUDE_IN_VM === "true") return true;
|
|
211215
|
+
const marker = markerPath(env);
|
|
211216
|
+
if (!marker) return false;
|
|
211217
|
+
try {
|
|
211218
|
+
return (0, node_fs.existsSync)(marker);
|
|
211219
|
+
} catch {
|
|
211220
|
+
return false;
|
|
211221
|
+
}
|
|
211222
|
+
}
|
|
211223
|
+
/** Exported at boot. This is the restart proof CMMD waits on, so it must never read the file. */
|
|
211224
|
+
function bootExportedPersonalClaudeGenerationHash(env = process.env) {
|
|
211225
|
+
return env.FORGE_PERSONAL_CLAUDE_GENERATION_HASH?.trim() ?? "";
|
|
211226
|
+
}
|
|
211227
|
+
/**
|
|
211228
|
+
* The sha256 of the Claude credential generation this runtime holds, or "" when
|
|
211229
|
+
* it holds none. The readiness guard compares it with the generation CMMD
|
|
211230
|
+
* applied. The restart proof must not use it; it uses the boot-exported hash.
|
|
211231
|
+
*
|
|
211232
|
+
* The boot preload exports FORGE_PERSONAL_CLAUDE_GENERATION_HASH, but only when
|
|
211233
|
+
* the marker already exists at boot. Activation in place writes the generation
|
|
211234
|
+
* file and touches the marker without setting any env, so an empty variable
|
|
211235
|
+
* falls back to that file, hashed the way the preload hashes it. CMMD puts the
|
|
211236
|
+
* file beside the marker in the guest home and names no path for it. A marker
|
|
211237
|
+
* with no generation file is a legacy runtime, and gets no hash here either.
|
|
211238
|
+
*/
|
|
211239
|
+
function personalClaudeGenerationHash(env = process.env) {
|
|
211240
|
+
const exported = bootExportedPersonalClaudeGenerationHash(env);
|
|
211241
|
+
if (exported) return exported;
|
|
211242
|
+
const marker = markerPath(env);
|
|
211243
|
+
if (!marker) return "";
|
|
211244
|
+
try {
|
|
211245
|
+
if (!(0, node_fs.existsSync)(marker)) return "";
|
|
211246
|
+
const generation = (0, node_fs.readFileSync)((0, node_path.join)((0, node_path.dirname)(marker), PERSONAL_CLAUDE_GENERATION_FILE_NAME), "utf8").trim();
|
|
211247
|
+
return generation ? (0, node_crypto.createHash)("sha256").update(generation).digest("hex") : "";
|
|
211248
|
+
} catch {
|
|
211249
|
+
return "";
|
|
211250
|
+
}
|
|
211251
|
+
}
|
|
211252
|
+
//#endregion
|
|
211143
211253
|
//#region src/provider/claudeCredentialContainment.ts
|
|
211144
211254
|
/**
|
|
211145
211255
|
* Containment for provider credentials inherited by the Claude Agent SDK child.
|
|
211146
211256
|
*
|
|
211147
|
-
*
|
|
211257
|
+
* Free of Effect services on purpose: ClaudeAdapter pulls in Effect platform
|
|
211148
211258
|
* services, the browser manager, and the server config, so a unit test that
|
|
211149
211259
|
* imports it cannot run in a worktree. This rule is cost-critical and must stay
|
|
211150
211260
|
* directly testable.
|
|
@@ -211191,6 +211301,190 @@ function withoutInheritedAnthropicCredentials(env, options) {
|
|
|
211191
211301
|
if (options?.stripEndpointOverride) delete contained[ANTHROPIC_ENDPOINT_OVERRIDE_KEY];
|
|
211192
211302
|
return contained;
|
|
211193
211303
|
}
|
|
211304
|
+
/**
|
|
211305
|
+
* The environment a Claude child gets when it runs on the personal
|
|
211306
|
+
* subscription. Server-only secrets are scrubbed, inherited Anthropic keys are
|
|
211307
|
+
* removed so they cannot outrank the login, and the credential directory is set
|
|
211308
|
+
* only when one was decided. The turn's personal-subscription branch and the
|
|
211309
|
+
* capability probe both build their environment here, so the probe asks about
|
|
211310
|
+
* the setup the turn will run with.
|
|
211311
|
+
*
|
|
211312
|
+
* The endpoint override goes only when `personalClaudeInVmEnabled` says this
|
|
211313
|
+
* runtime is on the personal Claude lane. That check also reads the activation
|
|
211314
|
+
* marker CMMD writes on disk (#3081), so a Sprite switched on in place, which
|
|
211315
|
+
* has the marker and no env var until it restarts, drops the override too.
|
|
211316
|
+
*/
|
|
211317
|
+
function claudeSubscriptionChildEnv(processEnv, credentialDirectory) {
|
|
211318
|
+
return {
|
|
211319
|
+
...withoutInheritedAnthropicCredentials(scrubForgeProviderChildEnvSecrets(processEnv), { stripEndpointOverride: personalClaudeInVmEnabled(processEnv) }),
|
|
211320
|
+
...credentialDirectory ? { [CLAUDE_SECURESTORAGE_CONFIG_DIR_ENV]: credentialDirectory } : {}
|
|
211321
|
+
};
|
|
211322
|
+
}
|
|
211323
|
+
//#endregion
|
|
211324
|
+
//#region src/provider/claudeCredentialDirectoryProbe.ts
|
|
211325
|
+
/**
|
|
211326
|
+
* Capability probe for `CLAUDE_SECURESTORAGE_CONFIG_DIR`.
|
|
211327
|
+
*
|
|
211328
|
+
* The variable is unpublished, so a runtime is asked rather than assumed to
|
|
211329
|
+
* honour it (`docs/adr/unpublished-provider-surface-needs-a-fallback.md`). The
|
|
211330
|
+
* question the probe can actually answer: point the variable at an empty
|
|
211331
|
+
* directory and a runtime that honours it flips from logged in to logged out.
|
|
211332
|
+
* A runtime with no credential at all is logged out either way and cannot
|
|
211333
|
+
* discriminate, which is `inconclusive`, and callers read that as unsupported.
|
|
211334
|
+
*
|
|
211335
|
+
* An answer only counts for the binary and environment the turn will use. The
|
|
211336
|
+
* probe spawns the `binaryPath` the turn hands the SDK, and builds its
|
|
211337
|
+
* environment with `claudeSubscriptionChildEnv`, the same builder as the turn's
|
|
211338
|
+
* personal-subscription branch.
|
|
211339
|
+
*
|
|
211340
|
+
* `claude auth status` is a pure read. It sends no prompt and leaves
|
|
211341
|
+
* `.credentials.json` byte-identical, so probing spends no subscription usage.
|
|
211342
|
+
* It prints JSON on stdout and exits 1 when logged out, so the exit code is not
|
|
211343
|
+
* a failure signal and the `loggedIn` field is what gets read.
|
|
211344
|
+
*/
|
|
211345
|
+
const execFileAsync$3 = (0, node_util.promisify)(node_child_process.execFile);
|
|
211346
|
+
const AUTH_STATUS_ARGS = ["auth", "status"];
|
|
211347
|
+
const VERSION_ARGS = ["--version"];
|
|
211348
|
+
/**
|
|
211349
|
+
* The probe sits in front of a turn, so a provider process that never answers
|
|
211350
|
+
* must not hold that turn open. `execFile` kills the child at this bound, and
|
|
211351
|
+
* `runBounded` gives up on it at the same bound whatever the deps do, so a
|
|
211352
|
+
* hung or unkillable process costs a few seconds and an `inconclusive`.
|
|
211353
|
+
*/
|
|
211354
|
+
const PROBE_TIMEOUT_MS = 5e3;
|
|
211355
|
+
const PROBE_OUTPUT_LIMIT_BYTES = 1024 * 1024;
|
|
211356
|
+
const PROBE_DIRECTORY_PREFIX = "forge-claude-capability-";
|
|
211357
|
+
const VERSION_PATTERN = /\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?/;
|
|
211358
|
+
function makeClaudeCredentialDirectoryProbe(deps) {
|
|
211359
|
+
const answers = /* @__PURE__ */ new Map();
|
|
211360
|
+
const answer = async () => {
|
|
211361
|
+
const version = await readVersionSafely(deps);
|
|
211362
|
+
if (version === null) return runProbe(deps);
|
|
211363
|
+
const cached = answers.get(version);
|
|
211364
|
+
if (cached) return cached;
|
|
211365
|
+
const pending = runProbe(deps);
|
|
211366
|
+
answers.set(version, pending);
|
|
211367
|
+
const forget = () => {
|
|
211368
|
+
if (answers.get(version) === pending) answers.delete(version);
|
|
211369
|
+
};
|
|
211370
|
+
pending.then((result) => {
|
|
211371
|
+
if (result === "inconclusive") forget();
|
|
211372
|
+
}, forget);
|
|
211373
|
+
return pending;
|
|
211374
|
+
};
|
|
211375
|
+
return { support: require_Schema$1.tryPromise(answer).pipe(require_Schema$1.orElseSucceed(() => "inconclusive")) };
|
|
211376
|
+
}
|
|
211377
|
+
/**
|
|
211378
|
+
* One probe per binary path for the life of the process. A conclusive answer is
|
|
211379
|
+
* cached per binary version, so the two `auth status` spawns run once per path
|
|
211380
|
+
* and version. `claude --version` still spawns on every call, which is how an
|
|
211381
|
+
* in-place CLI upgrade gets a fresh answer.
|
|
211382
|
+
*/
|
|
211383
|
+
const probesByBinaryPath = /* @__PURE__ */ new Map();
|
|
211384
|
+
const INCONCLUSIVE_PROBE = { support: require_Schema$1.succeed("inconclusive") };
|
|
211385
|
+
/**
|
|
211386
|
+
* The probe for the binary a turn with this `binaryPath` setting runs.
|
|
211387
|
+
*
|
|
211388
|
+
* `buildClaudeQueryOptions` hands the setting to the SDK as
|
|
211389
|
+
* `pathToClaudeCodeExecutable`, and the SDK spawns it as given, so the default
|
|
211390
|
+
* `claude` goes through PATH for the probe exactly as for the turn. The SDK
|
|
211391
|
+
* picks a bundled build of its own only when the path is empty, and the
|
|
211392
|
+
* settings schema turns an empty value into `claude`. Should one arrive empty
|
|
211393
|
+
* anyway, the answer is `inconclusive` rather than an answer about another
|
|
211394
|
+
* binary.
|
|
211395
|
+
*/
|
|
211396
|
+
function claudeCredentialDirectoryProbeFor(binaryPath) {
|
|
211397
|
+
if (!binaryPath) return INCONCLUSIVE_PROBE;
|
|
211398
|
+
const existing = probesByBinaryPath.get(binaryPath);
|
|
211399
|
+
if (existing) return existing;
|
|
211400
|
+
const probe = makeClaudeCredentialDirectoryProbe(claudeCredentialDirectoryProbeDeps(binaryPath));
|
|
211401
|
+
probesByBinaryPath.set(binaryPath, probe);
|
|
211402
|
+
return probe;
|
|
211403
|
+
}
|
|
211404
|
+
const spawnClaude = (file, args, options) => execFileAsync$3(file, [...args], { ...options });
|
|
211405
|
+
function claudeCredentialDirectoryProbeDeps(binaryPath, options = {}) {
|
|
211406
|
+
const run = (args, credentialDirectory) => runClaudeCommand$1(options.execFile ?? spawnClaude, binaryPath, args, claudeSubscriptionChildEnv(options.baseEnv ?? process.env, credentialDirectory));
|
|
211407
|
+
return {
|
|
211408
|
+
runAuthStatus: ({ credentialDirectory }) => run(AUTH_STATUS_ARGS, credentialDirectory),
|
|
211409
|
+
readBinaryVersion: async () => {
|
|
211410
|
+
const { stdout } = await run(VERSION_ARGS, null);
|
|
211411
|
+
return VERSION_PATTERN.exec(stdout)?.[0] ?? null;
|
|
211412
|
+
},
|
|
211413
|
+
makeProbeDirectory: () => (0, node_fs_promises.mkdtemp)((0, node_path.join)((0, node_os.tmpdir)(), PROBE_DIRECTORY_PREFIX)),
|
|
211414
|
+
removeProbeDirectory: (directory) => (0, node_fs_promises.rm)(directory, {
|
|
211415
|
+
recursive: true,
|
|
211416
|
+
force: true
|
|
211417
|
+
})
|
|
211418
|
+
};
|
|
211419
|
+
}
|
|
211420
|
+
/** Rejects when the probe could not tell. `support` turns that into `inconclusive`. */
|
|
211421
|
+
async function runProbe(deps) {
|
|
211422
|
+
let probeDirectory = null;
|
|
211423
|
+
try {
|
|
211424
|
+
if (parseLoggedIn(await runBounded(deps, () => deps.runAuthStatus({ credentialDirectory: null }))) !== true) return "inconclusive";
|
|
211425
|
+
probeDirectory = await deps.makeProbeDirectory();
|
|
211426
|
+
const directory = probeDirectory;
|
|
211427
|
+
const redirected = parseLoggedIn(await runBounded(deps, () => deps.runAuthStatus({ credentialDirectory: directory })));
|
|
211428
|
+
if (redirected === false) return "supported";
|
|
211429
|
+
return redirected === true ? "unsupported" : "inconclusive";
|
|
211430
|
+
} finally {
|
|
211431
|
+
if (probeDirectory !== null) await deps.removeProbeDirectory(probeDirectory).catch(() => void 0);
|
|
211432
|
+
}
|
|
211433
|
+
}
|
|
211434
|
+
async function readVersionSafely(deps) {
|
|
211435
|
+
try {
|
|
211436
|
+
return await runBounded(deps, () => deps.readBinaryVersion());
|
|
211437
|
+
} catch {
|
|
211438
|
+
return null;
|
|
211439
|
+
}
|
|
211440
|
+
}
|
|
211441
|
+
/**
|
|
211442
|
+
* Give up on a call that outlives the bound. The underlying promise is left to
|
|
211443
|
+
* settle on its own; the real deps hand `execFile` the same bound, so its child
|
|
211444
|
+
* is already being killed by the time this rejects.
|
|
211445
|
+
*/
|
|
211446
|
+
async function runBounded(deps, operation) {
|
|
211447
|
+
const timeoutMs = deps.timeoutMs ?? PROBE_TIMEOUT_MS;
|
|
211448
|
+
let timer;
|
|
211449
|
+
try {
|
|
211450
|
+
return await Promise.race([operation(), new Promise((_resolve, reject) => {
|
|
211451
|
+
timer = setTimeout(() => reject(/* @__PURE__ */ new Error("claude probe call timed out")), timeoutMs);
|
|
211452
|
+
})]);
|
|
211453
|
+
} finally {
|
|
211454
|
+
if (timer !== void 0) clearTimeout(timer);
|
|
211455
|
+
}
|
|
211456
|
+
}
|
|
211457
|
+
/** The `loggedIn` field, or null when the output does not carry one. */
|
|
211458
|
+
function parseLoggedIn(output) {
|
|
211459
|
+
const parsed = parseJsonObject$1(output.stdout);
|
|
211460
|
+
return typeof parsed?.loggedIn === "boolean" ? parsed.loggedIn : null;
|
|
211461
|
+
}
|
|
211462
|
+
function parseJsonObject$1(output) {
|
|
211463
|
+
const trimmed = output.trim();
|
|
211464
|
+
const start = trimmed.indexOf("{");
|
|
211465
|
+
const end = trimmed.lastIndexOf("}");
|
|
211466
|
+
if (start === -1 || end <= start) return null;
|
|
211467
|
+
try {
|
|
211468
|
+
const parsed = JSON.parse(trimmed.slice(start, end + 1));
|
|
211469
|
+
return typeof parsed === "object" && parsed !== null && !Array.isArray(parsed) ? parsed : null;
|
|
211470
|
+
} catch {
|
|
211471
|
+
return null;
|
|
211472
|
+
}
|
|
211473
|
+
}
|
|
211474
|
+
async function runClaudeCommand$1(execFileImpl, binaryPath, args, env) {
|
|
211475
|
+
try {
|
|
211476
|
+
const { stdout } = await execFileImpl(binaryPath, args, {
|
|
211477
|
+
env,
|
|
211478
|
+
maxBuffer: PROBE_OUTPUT_LIMIT_BYTES,
|
|
211479
|
+
timeout: PROBE_TIMEOUT_MS
|
|
211480
|
+
});
|
|
211481
|
+
return { stdout };
|
|
211482
|
+
} catch (cause) {
|
|
211483
|
+
const failure = cause;
|
|
211484
|
+
if (typeof failure.stdout !== "string") throw cause;
|
|
211485
|
+
return { stdout: failure.stdout };
|
|
211486
|
+
}
|
|
211487
|
+
}
|
|
211194
211488
|
//#endregion
|
|
211195
211489
|
//#region src/provider/makeManagedServerProvider.ts
|
|
211196
211490
|
const makeManagedServerProvider = require_Schema$1.fn("makeManagedServerProvider")(function* (input) {
|
|
@@ -211283,43 +211577,6 @@ const makeProviderAuthProvenance = (input) => {
|
|
|
211283
211577
|
};
|
|
211284
211578
|
};
|
|
211285
211579
|
//#endregion
|
|
211286
|
-
//#region src/provider/personalClaudeInVm.ts
|
|
211287
|
-
/**
|
|
211288
|
-
* CMMD writes this marker when it activates a personal Claude subscription
|
|
211289
|
-
* inside a running sprite. Activation always happens after boot, so a
|
|
211290
|
-
* boot-time env var alone cannot reflect it (FOR-36249).
|
|
211291
|
-
*
|
|
211292
|
-
* CMMD names the file in the Service env (FORGE_PERSONAL_CLAUDE_MARKER_PATH).
|
|
211293
|
-
* Without that, the marker sits directly under FORGE_HOME, which is the same
|
|
211294
|
-
* guest home CMMD derives every sprite path from. A server with neither has no
|
|
211295
|
-
* sprite home to look in, and a desktop never has a marker, so it reads none.
|
|
211296
|
-
*/
|
|
211297
|
-
const PERSONAL_CLAUDE_IN_VM_MARKER_NAME = ".forge-personal-claude-in-vm";
|
|
211298
|
-
function markerPath(env) {
|
|
211299
|
-
if (env.FORGE_PERSONAL_CLAUDE_MARKER_PATH) return env.FORGE_PERSONAL_CLAUDE_MARKER_PATH;
|
|
211300
|
-
if (env.FORGE_HOME) return (0, node_path.join)(env.FORGE_HOME, PERSONAL_CLAUDE_IN_VM_MARKER_NAME);
|
|
211301
|
-
return null;
|
|
211302
|
-
}
|
|
211303
|
-
/**
|
|
211304
|
-
* True when this runtime should offer the personal Claude subscription lane.
|
|
211305
|
-
*
|
|
211306
|
-
* Accepts either signal: the boot-time env var (backward compatible) or the
|
|
211307
|
-
* activation marker on disk. Reading the marker at call time lets the provider
|
|
211308
|
-
* catalog and the per-turn readiness guard self-heal once CMMD delivers,
|
|
211309
|
-
* without a sprite reboot. Callers still verify the credential is non-empty
|
|
211310
|
-
* before treating it as usable; this only decides whether the lane is active.
|
|
211311
|
-
*/
|
|
211312
|
-
function personalClaudeInVmEnabled(env = process.env) {
|
|
211313
|
-
if (env.FORGE_PERSONAL_CLAUDE_IN_VM === "true") return true;
|
|
211314
|
-
const marker = markerPath(env);
|
|
211315
|
-
if (!marker) return false;
|
|
211316
|
-
try {
|
|
211317
|
-
return (0, node_fs.existsSync)(marker);
|
|
211318
|
-
} catch {
|
|
211319
|
-
return false;
|
|
211320
|
-
}
|
|
211321
|
-
}
|
|
211322
|
-
//#endregion
|
|
211323
211580
|
//#region src/provider/Layers/ClaudeProvider.ts
|
|
211324
211581
|
function endpointProfileDisplayNames(settings) {
|
|
211325
211582
|
return Object.fromEntries(settings.endpointProfiles.filter((p) => p.displayName).map((p) => [p.slug, p.displayName]));
|
|
@@ -212738,10 +212995,26 @@ function claudeForgeAttributionPrompt() {
|
|
|
212738
212995
|
const attribution = resolveAgentAttribution();
|
|
212739
212996
|
return `When creating git commits, do NOT add any "Claude" or "Anthropic" co-author trailers. Use exactly:\n ${attribution.coauthorTrailer}\n\nWhen creating GitHub pull request descriptions, do NOT add any "🤖 Generated with [Claude Code]" footer. Use exactly:\n ${attribution.prFooter}`;
|
|
212740
212997
|
}
|
|
212741
|
-
|
|
212998
|
+
/**
|
|
212999
|
+
* `credentialDirectory` points this turn at one Provider Account's own
|
|
213000
|
+
* `.credentials.json` without moving the transcripts, which is what lets an
|
|
213001
|
+
* account switch keep `--resume` working
|
|
213002
|
+
* (`docs/adr/claude-accounts-split-credentials-not-sessions.md`). It stays null
|
|
213003
|
+
* until #3049 gives the directories a writer, keyed on the account identity on
|
|
213004
|
+
* the delivery receipt CMMD signs. `decideClaudeCredentialDirectory` only hands
|
|
213005
|
+
* back a directory that already holds a usable credential, and while none
|
|
213006
|
+
* exists the environment is byte-for-byte what it would be without this
|
|
213007
|
+
* parameter.
|
|
213008
|
+
*
|
|
213009
|
+
* It applies to the personal-subscription branch below and to nothing else. On
|
|
213010
|
+
* the profile / proxy branch the credential file is not what authenticates the
|
|
213011
|
+
* turn, an API key is, so pointing the variable there would add a second source
|
|
213012
|
+
* of truth that a later reader has to rule out.
|
|
213013
|
+
*/
|
|
213014
|
+
function buildClaudeQueryEnv(baseProcessEnv, endpointProfile, claudeSettings, providerProxyCredential, credentialDirectory) {
|
|
212742
213015
|
const baseEnv = scrubForgeProviderChildEnvSecrets(baseProcessEnv);
|
|
212743
213016
|
const effectiveBaseUrl = resolveClaudeQueryBaseUrl(endpointProfile, claudeSettings, providerProxyCredential);
|
|
212744
|
-
if (!effectiveBaseUrl) return
|
|
213017
|
+
if (!effectiveBaseUrl) return claudeSubscriptionChildEnv(baseProcessEnv, credentialDirectory);
|
|
212745
213018
|
const effectiveApiKey = resolveClaudeQueryApiKey(endpointProfile, claudeSettings, providerProxyCredential?.token ?? null);
|
|
212746
213019
|
return {
|
|
212747
213020
|
...baseEnv,
|
|
@@ -215010,6 +215283,17 @@ const makeClaudeAdapter = require_Schema$1.fn("makeClaudeAdapter")(function* (op
|
|
|
215010
215283
|
})));
|
|
215011
215284
|
const claudeSettings = serverSettings.providers.claudeAgent;
|
|
215012
215285
|
const claudeBinaryPath = claudeSettings.binaryPath;
|
|
215286
|
+
const credentialDirectoryDecision = yield* decideClaudeCredentialDirectory({
|
|
215287
|
+
env: process.env,
|
|
215288
|
+
baseDir: serverConfig.baseDir,
|
|
215289
|
+
accountIdentity: resolveClaudeProviderAccountIdentity(process.env),
|
|
215290
|
+
probeSupport: claudeCredentialDirectoryProbeFor(claudeBinaryPath).support
|
|
215291
|
+
});
|
|
215292
|
+
if (credentialDirectoryDecision.outcome === "skipped") {
|
|
215293
|
+
const skipLine = `Claude credential directory skipped: ${credentialDirectoryDecision.reason}`;
|
|
215294
|
+
yield* credentialDirectoryDecision.reason === "no-account" ? require_Schema$1.logDebug(skipLine) : require_Schema$1.logWarning(skipLine);
|
|
215295
|
+
}
|
|
215296
|
+
const claudeCredentialDirectory = credentialDirectoryDecision.outcome === "set" ? credentialDirectoryDecision.directory : null;
|
|
215013
215297
|
const modelConfig = resolveClaudeSessionModelConfig(input.modelSelection, claudeSettings.endpointProfiles);
|
|
215014
215298
|
const providerProxyCredential = yield* issueClaudeProviderProxyCredential(modelConfig.endpointProfile, modelConfig.modelSlug ?? modelConfig.effectiveApiModelId ?? "unknown", threadId);
|
|
215015
215299
|
const permissionMode = permissionModeForRuntimeMode(input.runtimeMode);
|
|
@@ -215034,7 +215318,7 @@ const makeClaudeAdapter = require_Schema$1.fn("makeClaudeAdapter")(function* (op
|
|
|
215034
215318
|
existingResumeSessionId,
|
|
215035
215319
|
newSessionId,
|
|
215036
215320
|
canUseTool,
|
|
215037
|
-
env: buildClaudeQueryEnv(process.env, modelConfig.endpointProfile, claudeSettings, providerProxyCredential),
|
|
215321
|
+
env: buildClaudeQueryEnv(process.env, modelConfig.endpointProfile, claudeSettings, providerProxyCredential, claudeCredentialDirectory),
|
|
215038
215322
|
browserManager,
|
|
215039
215323
|
threadId,
|
|
215040
215324
|
browserToolEnvironmentId,
|
|
@@ -230138,7 +230422,7 @@ function claudeReadinessGuardEffect(config, dependencies, resolved, input) {
|
|
|
230138
230422
|
});
|
|
230139
230423
|
}
|
|
230140
230424
|
function requiresNativeClaudeReadiness(config, provider) {
|
|
230141
|
-
return provider === "claudeAgent" && config.forgeCmmdRuntimeMode === true &&
|
|
230425
|
+
return provider === "claudeAgent" && config.forgeCmmdRuntimeMode === true && personalClaudeInVmEnabled();
|
|
230142
230426
|
}
|
|
230143
230427
|
function resolveGuardAuthority(config, readSessionId, input) {
|
|
230144
230428
|
const runtimeId = config.forgeCmmdRuntimeId?.trim();
|
|
@@ -230150,7 +230434,7 @@ function resolveGuardAuthority(config, readSessionId, input) {
|
|
|
230150
230434
|
runtimeId,
|
|
230151
230435
|
machineId,
|
|
230152
230436
|
credentialEpoch,
|
|
230153
|
-
localGenerationHash:
|
|
230437
|
+
localGenerationHash: personalClaudeGenerationHash(),
|
|
230154
230438
|
sessionId
|
|
230155
230439
|
};
|
|
230156
230440
|
}
|
|
@@ -287365,25 +287649,124 @@ const makeRuntimeDrainReporter = require_Schema$1.gen(function* () {
|
|
|
287365
287649
|
var RuntimeDrainReporter = class extends require_Schema$1.Service()("forge/runtimeDrain/RuntimeDrainReporter") {};
|
|
287366
287650
|
const RuntimeDrainReporterLive = require_Schema$1.effect(RuntimeDrainReporter, makeRuntimeDrainReporter);
|
|
287367
287651
|
//#endregion
|
|
287368
|
-
//#region src/usage/
|
|
287369
|
-
/**
|
|
287370
|
-
*
|
|
287371
|
-
*
|
|
287372
|
-
*
|
|
287373
|
-
*
|
|
287374
|
-
|
|
287375
|
-
|
|
287376
|
-
|
|
287377
|
-
|
|
287378
|
-
|
|
287379
|
-
|
|
287380
|
-
|
|
287381
|
-
|
|
287382
|
-
|
|
287383
|
-
|
|
287384
|
-
|
|
287385
|
-
|
|
287386
|
-
|
|
287652
|
+
//#region src/usage/Services/ClaudeUsageTelemetry.ts
|
|
287653
|
+
/**
|
|
287654
|
+
* Claude usage the SDK reports on every turn, published as passive telemetry.
|
|
287655
|
+
*
|
|
287656
|
+
* Every Claude turn carries a `rate_limit_event` whose `unifiedWindows`
|
|
287657
|
+
* sub-object holds the two rolling windows the Usage card renders. Reading it
|
|
287658
|
+
* costs nothing: no `claude` process, no credential delivery, no woken sprite.
|
|
287659
|
+
*
|
|
287660
|
+
* This module is the whole publish lane behind one call. `observe` takes what
|
|
287661
|
+
* an observation knows and returns void. Whether the floor admits it, which
|
|
287662
|
+
* credential generation it ran on, whether the value is new, and whether CMMD
|
|
287663
|
+
* accepted it are all decided here, so the ingestion layer stays a decoder and
|
|
287664
|
+
* a hand-off. Nothing decided here can fail a turn, and no admitted
|
|
287665
|
+
* observation outlives `CLAUDE_USAGE_TELEMETRY_PUBLISH_TIMEOUT_MS`.
|
|
287666
|
+
*
|
|
287667
|
+
* The `/usage` probe in `syncClaudeCredential.ts` is a separate lane that
|
|
287668
|
+
* publishes `source: "active_poll"`. This one publishes
|
|
287669
|
+
* `source: "passive_sdk_telemetry"`, and both lanes build their body with
|
|
287670
|
+
* `claudeUsageTelemetryBody`.
|
|
287671
|
+
*
|
|
287672
|
+
* @module ClaudeUsageTelemetry
|
|
287673
|
+
*/
|
|
287674
|
+
var ClaudeUsageTelemetry = class extends require_Schema$1.Service()("forge/usage/Services/ClaudeUsageTelemetry") {};
|
|
287675
|
+
/**
|
|
287676
|
+
* The longest one admitted observation may take, from the credential
|
|
287677
|
+
* generation lookup through the CMMD credential and the POST, before it is
|
|
287678
|
+
* abandoned and counted as failed. Ingestion does not wait on it, so this
|
|
287679
|
+
* bounds how long a slow lookup or a slow CMMD keeps a fiber alive, not how
|
|
287680
|
+
* long a turn waits. Together with the floor it caps how many observations for
|
|
287681
|
+
* one account can be in flight at once.
|
|
287682
|
+
*/
|
|
287683
|
+
const CLAUDE_USAGE_TELEMETRY_PUBLISH_TIMEOUT_MS = 1e4;
|
|
287684
|
+
/** How many accounts, and how many pairs, the observer remembers before evicting the oldest. */
|
|
287685
|
+
const MAX_TRACKED_KEYS = 32;
|
|
287686
|
+
/**
|
|
287687
|
+
* Why one publish did not land. Every reason is the same to `observe`, which
|
|
287688
|
+
* logs it and moves on, so the type exists to name the lane rather than to be
|
|
287689
|
+
* matched on.
|
|
287690
|
+
*/
|
|
287691
|
+
var ClaudeUsageTelemetryPublishError = class extends require_Schema$1.TaggedError("ClaudeUsageTelemetryPublishError") {};
|
|
287692
|
+
function pairKey(attribution) {
|
|
287693
|
+
return `${attribution.providerAccountId}\u0000${attribution.credentialGenerationId}`;
|
|
287694
|
+
}
|
|
287695
|
+
function windowFingerprint(value) {
|
|
287696
|
+
return value === null ? "-" : `${value.usedPercent}|${value.resetsAt}|${value.status}`;
|
|
287697
|
+
}
|
|
287698
|
+
/**
|
|
287699
|
+
* A value fingerprint built field by field rather than by `JSON.stringify` on
|
|
287700
|
+
* the whole object, so the comparison does not depend on key insertion order
|
|
287701
|
+
* and cannot silently start ignoring a field a future mapper adds without a
|
|
287702
|
+
* matching change here.
|
|
287703
|
+
*/
|
|
287704
|
+
function windowsFingerprint(windows) {
|
|
287705
|
+
return `${windowFingerprint(windows.fiveHour)}/${windowFingerprint(windows.sevenDay)}`;
|
|
287706
|
+
}
|
|
287707
|
+
/**
|
|
287708
|
+
* Stores `value` as the newest entry for `key` and evicts the oldest entry past
|
|
287709
|
+
* the cap. Map iteration is insertion-ordered, so the first key is the one
|
|
287710
|
+
* touched longest ago. An idle account or a rotated credential generation
|
|
287711
|
+
* rarely comes back, so evicting it costs at most one early lookup or one
|
|
287712
|
+
* redundant publish.
|
|
287713
|
+
*/
|
|
287714
|
+
function remember(map, key, value) {
|
|
287715
|
+
map.delete(key);
|
|
287716
|
+
map.set(key, value);
|
|
287717
|
+
if (map.size > MAX_TRACKED_KEYS) {
|
|
287718
|
+
const oldest = map.keys().next();
|
|
287719
|
+
if (!oldest.done) map.delete(oldest.value);
|
|
287720
|
+
}
|
|
287721
|
+
}
|
|
287722
|
+
/**
|
|
287723
|
+
* Builds the observer over injected dependencies. The HTTP call and the clock
|
|
287724
|
+
* both arrive from outside, so a test drives the whole policy without a network
|
|
287725
|
+
* or a timer.
|
|
287726
|
+
*/
|
|
287727
|
+
function makeClaudeUsageTelemetry(dependencies) {
|
|
287728
|
+
const now = dependencies.now ?? (() => Date.now());
|
|
287729
|
+
/** When each account last had an observation admitted, whatever came of it. */
|
|
287730
|
+
const admittedAt = /* @__PURE__ */ new Map();
|
|
287731
|
+
/** The fingerprint of the last value CMMD accepted, per pair. */
|
|
287732
|
+
const publishedFingerprints = /* @__PURE__ */ new Map();
|
|
287733
|
+
const publishIfNew = (windows, providerAccountId, lookupCredentialGenerationId, at) => require_Schema$1.gen(function* () {
|
|
287734
|
+
const credentialGenerationId = yield* lookupCredentialGenerationId;
|
|
287735
|
+
if (!credentialGenerationId) return;
|
|
287736
|
+
const attribution = {
|
|
287737
|
+
providerAccountId,
|
|
287738
|
+
credentialGenerationId
|
|
287739
|
+
};
|
|
287740
|
+
const key = pairKey(attribution);
|
|
287741
|
+
const fingerprint = windowsFingerprint(windows);
|
|
287742
|
+
if (publishedFingerprints.get(key) === fingerprint) return;
|
|
287743
|
+
yield* dependencies.publish(claudeUsageTelemetryBody({
|
|
287744
|
+
attribution,
|
|
287745
|
+
source: "passive_sdk_telemetry",
|
|
287746
|
+
windows,
|
|
287747
|
+
observedAt: new Date(at).toISOString()
|
|
287748
|
+
}));
|
|
287749
|
+
remember(publishedFingerprints, key, fingerprint);
|
|
287750
|
+
});
|
|
287751
|
+
/**
|
|
287752
|
+
* The floor is read and claimed inside the returned effect, never while
|
|
287753
|
+
* building it, and before anything suspends. Two turns on one account can be
|
|
287754
|
+
* in flight at once, and the floor only holds them apart if the second one
|
|
287755
|
+
* reads what the first one wrote.
|
|
287756
|
+
*/
|
|
287757
|
+
const observe = (observation) => require_Schema$1.suspend$1(() => {
|
|
287758
|
+
const { windows, providerAccountId } = observation;
|
|
287759
|
+
if (!windows || !providerAccountId) return require_Schema$1.void_;
|
|
287760
|
+
const at = now();
|
|
287761
|
+
const lastAdmittedAt = admittedAt.get(providerAccountId);
|
|
287762
|
+
if (lastAdmittedAt !== void 0 && at - lastAdmittedAt < 5e3) return require_Schema$1.void_;
|
|
287763
|
+
remember(admittedAt, providerAccountId, at);
|
|
287764
|
+
return publishIfNew(windows, providerAccountId, observation.credentialGenerationId, at).pipe(require_Schema$1.timeout(CLAUDE_USAGE_TELEMETRY_PUBLISH_TIMEOUT_MS), require_Schema$1.catchCause((cause) => require_Schema$1.logWarning("claude usage passive telemetry publish failed", {
|
|
287765
|
+
providerAccountId,
|
|
287766
|
+
cause
|
|
287767
|
+
})), require_Schema$1.asVoid);
|
|
287768
|
+
});
|
|
287769
|
+
return { observe };
|
|
287387
287770
|
}
|
|
287388
287771
|
//#endregion
|
|
287389
287772
|
//#region src/orchestration/Layers/ProviderRuntimeIngestion.ts
|
|
@@ -287398,20 +287781,37 @@ const BUFFERED_PROPOSED_PLAN_BY_ID_TTL = require_Schema$1.minutes(120);
|
|
|
287398
287781
|
const MAX_BUFFERED_ASSISTANT_CHARS = 24e3;
|
|
287399
287782
|
const PROVIDER_INGESTION_CONCURRENCY = 8;
|
|
287400
287783
|
const STRICT_PROVIDER_LIFECYCLE_GUARD = process.env.FORGE_STRICT_PROVIDER_LIFECYCLE_GUARD !== "0";
|
|
287401
|
-
|
|
287784
|
+
/**
|
|
287785
|
+
* The SDK's own `rate_limit_info` off one ClaudeAdapter rate-limit event, or
|
|
287786
|
+
* nothing. Both readers below start here: the banner takes the single window
|
|
287787
|
+
* `rateLimitType` names, and passive usage telemetry takes the `unifiedWindows`
|
|
287788
|
+
* sub-object riding along on the same object.
|
|
287789
|
+
*/
|
|
287790
|
+
function readClaudeRateLimitInfo(raw) {
|
|
287402
287791
|
const rateLimitInfo = raw.rate_limit_info;
|
|
287403
|
-
if (rateLimitInfo === null || typeof rateLimitInfo !== "object") return
|
|
287792
|
+
if (rateLimitInfo === null || typeof rateLimitInfo !== "object") return null;
|
|
287793
|
+
return rateLimitInfo;
|
|
287794
|
+
}
|
|
287795
|
+
function decodeClaudeRateLimitWindows(raw) {
|
|
287796
|
+
const rateLimitInfo = readClaudeRateLimitInfo(raw);
|
|
287797
|
+
if (!rateLimitInfo) return [];
|
|
287404
287798
|
const mapped = mapClaudeRateLimitInfo(rateLimitInfo);
|
|
287405
287799
|
return mapped ? [{
|
|
287406
287800
|
window: mapped.kind === "fiveHour" ? "five_hour" : "seven_day",
|
|
287407
287801
|
...mapped.window
|
|
287408
287802
|
}] : [];
|
|
287409
287803
|
}
|
|
287804
|
+
/** The raw telemetry object ClaudeAdapter/CodexAdapter wrap as `payload.rateLimits`. */
|
|
287805
|
+
function readRateLimitPayload(event) {
|
|
287806
|
+
if (event.type !== "account.rate-limits.updated") return null;
|
|
287807
|
+
const raw = event.payload.rateLimits;
|
|
287808
|
+
return raw !== null && typeof raw === "object" ? raw : null;
|
|
287809
|
+
}
|
|
287410
287810
|
/**
|
|
287411
287811
|
* Codex's app-server protocol shape isn't type-published anywhere this
|
|
287412
|
-
* codebase has access to, so it's read defensively
|
|
287413
|
-
* is skipped rather than thrown, since this is a best-effort in-chat notice
|
|
287414
|
-
*
|
|
287812
|
+
* codebase has access to, so it's read defensively. An unrecognized shape
|
|
287813
|
+
* is skipped rather than thrown, since this is a best-effort in-chat notice
|
|
287814
|
+
* that the turn itself never depends on.
|
|
287415
287815
|
*/
|
|
287416
287816
|
/**
|
|
287417
287817
|
* Decodes ONE window of the codex app-server `account/rateLimits/updated`
|
|
@@ -287724,6 +288124,9 @@ function shouldApplyProviderTurnCompleted(activeTurnId, eventTurnId) {
|
|
|
287724
288124
|
}
|
|
287725
288125
|
const make$9 = require_Schema$1.fn("make")(function* () {
|
|
287726
288126
|
const orchestrationEngine = yield* OrchestrationEngineService;
|
|
288127
|
+
const claudeUsageTelemetry = yield* ClaudeUsageTelemetry;
|
|
288128
|
+
const claudeUsageScope = yield* require_Schema$1.make$3("parallel");
|
|
288129
|
+
yield* require_Schema$1.addFinalizer(() => require_Schema$1.close(claudeUsageScope, require_Schema$1.void_$1));
|
|
287727
288130
|
const providerService = yield* ProviderService;
|
|
287728
288131
|
const projectionTurnRepository = yield* ProjectionTurnRepository;
|
|
287729
288132
|
const serverSettingsService = yield* ServerSettingsService;
|
|
@@ -288132,18 +288535,53 @@ const make$9 = require_Schema$1.fn("make")(function* () {
|
|
|
288132
288535
|
* SDKRateLimitInfo, reused from apps/server/src/usage/providerRateLimits.ts
|
|
288133
288536
|
* (same decoder the Usage page cache uses). Codex's app-server protocol
|
|
288134
288537
|
* shape isn't type-published anywhere this codebase has access to, so it's
|
|
288135
|
-
* read defensively
|
|
288136
|
-
* thrown error, since this is a best-effort in-chat notice
|
|
288137
|
-
*
|
|
288538
|
+
* read defensively. An unrecognized shape means no window update, not a
|
|
288539
|
+
* thrown error, since this is a best-effort in-chat notice that the turn
|
|
288540
|
+
* itself never depends on.
|
|
288138
288541
|
*/
|
|
288139
288542
|
function decodeRateLimitWindows(event) {
|
|
288140
|
-
|
|
288141
|
-
|
|
288142
|
-
if (raw === null || typeof raw !== "object") return [];
|
|
288543
|
+
const raw = readRateLimitPayload(event);
|
|
288544
|
+
if (!raw) return [];
|
|
288143
288545
|
if (event.provider === "claudeAgent") return decodeClaudeRateLimitWindows(raw);
|
|
288144
288546
|
if (event.provider === "codex") return decodeCodexRateLimitWindows(raw);
|
|
288145
288547
|
return [];
|
|
288146
288548
|
}
|
|
288549
|
+
/**
|
|
288550
|
+
* The credential generation a Claude thread's live session runs on, or null.
|
|
288551
|
+
*
|
|
288552
|
+
* It is the delivery identity the readiness guard verified before the
|
|
288553
|
+
* session started, which ProviderService writes into the provider resume
|
|
288554
|
+
* cursor and `listSessions` hands back on the live session. Claude's
|
|
288555
|
+
* delivery identity is `{kind: "credential_generation"}` and carries NO
|
|
288556
|
+
* account field, so it cannot stand in for the account the thread's model
|
|
288557
|
+
* selection names, and neither id is derivable from the other. The usage
|
|
288558
|
+
* observer drops an observation missing either one, because publishing under
|
|
288559
|
+
* a guessed id would write one account's usage onto another account's card.
|
|
288560
|
+
*/
|
|
288561
|
+
const claudeCredentialGenerationId = require_Schema$1.fn("claudeCredentialGenerationId")(function* (threadId) {
|
|
288562
|
+
const cursor = (yield* providerService.listSessions()).find((entry) => entry.threadId === threadId && entry.provider === "claudeAgent")?.resumeCursor;
|
|
288563
|
+
const binding = cursor !== null && typeof cursor === "object" ? readProviderDeliveryBinding(cursor.providerDeliveryBinding) : null;
|
|
288564
|
+
return binding?.identity.kind === "credential_generation" ? binding.identity.credentialGenerationId : null;
|
|
288565
|
+
});
|
|
288566
|
+
/**
|
|
288567
|
+
* Publishes the unified windows the same event already carried for the
|
|
288568
|
+
* banner. Passive: no probe, no credential delivery, no woken sprite. The SDK
|
|
288569
|
+
* reports these figures on every turn whether anything reads them or not.
|
|
288570
|
+
* The observer owns the floor, the credential generation lookup, dedupe, the
|
|
288571
|
+
* timeout and the HTTP call, and never fails.
|
|
288572
|
+
*/
|
|
288573
|
+
const observeClaudeUsage = require_Schema$1.fn("observeClaudeUsage")(function* (event, thread) {
|
|
288574
|
+
if (event.provider !== "claudeAgent") return;
|
|
288575
|
+
const raw = readRateLimitPayload(event);
|
|
288576
|
+
const rateLimitInfo = raw ? readClaudeRateLimitInfo(raw) : null;
|
|
288577
|
+
const windows = rateLimitInfo ? mapClaudeUnifiedWindows(rateLimitInfo) : null;
|
|
288578
|
+
if (!windows) return;
|
|
288579
|
+
yield* claudeUsageTelemetry.observe({
|
|
288580
|
+
windows,
|
|
288581
|
+
providerAccountId: readProviderAccountId(thread.modelSelection) ?? null,
|
|
288582
|
+
credentialGenerationId: claudeCredentialGenerationId(thread.id)
|
|
288583
|
+
});
|
|
288584
|
+
});
|
|
288147
288585
|
const handleRateLimitEvent = require_Schema$1.fn("handleRateLimitEvent")(function* (event, thread) {
|
|
288148
288586
|
const provider = event.provider === "claudeAgent" ? "claude" : event.provider === "codex" ? "codex" : null;
|
|
288149
288587
|
if (!provider) return;
|
|
@@ -288160,6 +288598,10 @@ const make$9 = require_Schema$1.fn("make")(function* () {
|
|
|
288160
288598
|
resetsAt: win.resetsAt,
|
|
288161
288599
|
createdAt: event.createdAt
|
|
288162
288600
|
}), { discard: true });
|
|
288601
|
+
yield* observeClaudeUsage(event, thread).pipe(require_Schema$1.catchCause((cause) => require_Schema$1.hasInterruptsOnly(cause) ? require_Schema$1.void_ : require_Schema$1.logWarning("claude usage observation failed", {
|
|
288602
|
+
eventId: event.eventId,
|
|
288603
|
+
cause
|
|
288604
|
+
})), require_Schema$1.forkIn(claudeUsageScope));
|
|
288163
288605
|
});
|
|
288164
288606
|
const processRuntimeEvent = require_Schema$1.fn("processRuntimeEvent")(function* (event) {
|
|
288165
288607
|
const thread = (yield* orchestrationEngine.getReadModel()).threads.find((entry) => entry.id === event.threadId);
|
|
@@ -288275,6 +288717,44 @@ const make$9 = require_Schema$1.fn("make")(function* () {
|
|
|
288275
288717
|
});
|
|
288276
288718
|
const ProviderRuntimeIngestionLive = require_Schema$1.effect(ProviderRuntimeIngestionService, make$9()).pipe(require_Schema$1.provide$1(ProjectionTurnRepositoryLive));
|
|
288277
288719
|
//#endregion
|
|
288720
|
+
//#region src/usage/Layers/ClaudeUsageTelemetry.ts
|
|
288721
|
+
/**
|
|
288722
|
+
* The real publish behind `ClaudeUsageTelemetry`: the owner's CMMD credential
|
|
288723
|
+
* and one POST to `CLAUDE_USAGE_TELEMETRY_PATH`.
|
|
288724
|
+
*
|
|
288725
|
+
* The call always goes out on the owner session credential, never on a
|
|
288726
|
+
* runtime grant.
|
|
288727
|
+
*
|
|
288728
|
+
* A machine with nobody signed in, or one whose CMMD credential cannot be
|
|
288729
|
+
* minted, fails the publish rather than reporting success. The observer only
|
|
288730
|
+
* remembers a value it managed to send, so a quiet success here would suppress
|
|
288731
|
+
* the publish for as long as that reading stayed the same, which on a stable
|
|
288732
|
+
* account is hours.
|
|
288733
|
+
*
|
|
288734
|
+
* @module ClaudeUsageTelemetryLive
|
|
288735
|
+
*/
|
|
288736
|
+
const LOG_PREFIX = "claude-usage-passive-telemetry";
|
|
288737
|
+
const ClaudeUsageTelemetryLive = require_Schema$1.effect(ClaudeUsageTelemetry, require_Schema$1.gen(function* () {
|
|
288738
|
+
const sql = yield* require_SqlError.SqlClient;
|
|
288739
|
+
const publish = (body) => require_Schema$1.gen(function* () {
|
|
288740
|
+
const credential = yield* resolveCmmdOwnerSessionCredential(sql, { logPrefix: LOG_PREFIX });
|
|
288741
|
+
if (credential.kind !== "credential") return yield* new ClaudeUsageTelemetryPublishError({ reason: credential.kind });
|
|
288742
|
+
yield* require_Schema$1.tryPromise({
|
|
288743
|
+
try: () => cmmdGateway.request({
|
|
288744
|
+
method: "POST",
|
|
288745
|
+
jwt: credential.jwt,
|
|
288746
|
+
path: CLAUDE_USAGE_TELEMETRY_PATH,
|
|
288747
|
+
body
|
|
288748
|
+
}),
|
|
288749
|
+
catch: (cause) => new ClaudeUsageTelemetryPublishError({
|
|
288750
|
+
reason: "request-failed",
|
|
288751
|
+
cause
|
|
288752
|
+
})
|
|
288753
|
+
});
|
|
288754
|
+
});
|
|
288755
|
+
return makeClaudeUsageTelemetry({ publish });
|
|
288756
|
+
}));
|
|
288757
|
+
//#endregion
|
|
288278
288758
|
//#region ../../packages/shared/src/DrainableWorker.ts
|
|
288279
288759
|
/**
|
|
288280
288760
|
* Create a drainable worker that processes items from an unbounded queue.
|
|
@@ -292460,6 +292940,382 @@ const makeWorkspaceFileSystem = require_Schema$1.gen(function* () {
|
|
|
292460
292940
|
});
|
|
292461
292941
|
const WorkspaceFileSystemLive = require_Schema$1.effect(WorkspaceFileSystem, makeWorkspaceFileSystem);
|
|
292462
292942
|
//#endregion
|
|
292943
|
+
//#region src/workspace/Layers/WorkspaceBulkTransfer.ts
|
|
292944
|
+
const TRANSFER_TTL_MS = 1440 * 60 * 1e3;
|
|
292945
|
+
const TRANSFER_LOCK_STRIPE_COUNT = 64;
|
|
292946
|
+
function transferError(input) {
|
|
292947
|
+
return new BulkTransferError({
|
|
292948
|
+
code: input.code,
|
|
292949
|
+
message: input.message,
|
|
292950
|
+
transferId: input.transferId ?? null,
|
|
292951
|
+
retryable: input.retryable ?? false,
|
|
292952
|
+
...input.cause === void 0 ? {} : { cause: input.cause }
|
|
292953
|
+
});
|
|
292954
|
+
}
|
|
292955
|
+
function mapFailure(operation, transferId) {
|
|
292956
|
+
return (cause) => require_Schema$1.is(BulkTransferError)(cause) ? cause : transferError({
|
|
292957
|
+
code: "io_error",
|
|
292958
|
+
message: `Workspace bulk transfer failed during ${operation}.`,
|
|
292959
|
+
...transferId === void 0 ? {} : { transferId },
|
|
292960
|
+
retryable: true,
|
|
292961
|
+
cause
|
|
292962
|
+
});
|
|
292963
|
+
}
|
|
292964
|
+
function transferLockStripe(transferId) {
|
|
292965
|
+
let hash = 2166136261;
|
|
292966
|
+
for (let index = 0; index < transferId.length; index += 1) hash = Math.imul(hash ^ transferId.charCodeAt(index), 16777619);
|
|
292967
|
+
return (hash >>> 0) % TRANSFER_LOCK_STRIPE_COUNT;
|
|
292968
|
+
}
|
|
292969
|
+
const makeWorkspaceBulkTransfer = (options = {}) => require_Schema$1.gen(function* () {
|
|
292970
|
+
const fileSystem = yield* require_Path.FileSystem;
|
|
292971
|
+
const path = yield* require_Path.Path;
|
|
292972
|
+
const sql = yield* require_SqlError.SqlClient;
|
|
292973
|
+
const workspacePaths = yield* WorkspacePaths;
|
|
292974
|
+
const workspaceEntries = yield* WorkspaceEntries;
|
|
292975
|
+
const fault = options.fault ?? (() => require_Schema$1.void_);
|
|
292976
|
+
const transferLocks = yield* require_Schema$1.forEach(Array.from({ length: TRANSFER_LOCK_STRIPE_COUNT }), () => require_Path.make$5(1));
|
|
292977
|
+
const withTransferLock = (transferId, effect) => transferLocks[transferLockStripe(transferId)].withPermit(effect);
|
|
292978
|
+
const findTransfer = (transferId) => sql`
|
|
292979
|
+
SELECT transfer_id AS "transferId", workspace_root AS "workspaceRoot",
|
|
292980
|
+
relative_path AS "relativePath", target_path AS "targetPath", temp_path AS "tempPath",
|
|
292981
|
+
total_bytes AS "totalBytes", whole_sha256 AS "wholeSha256",
|
|
292982
|
+
next_offset AS "nextOffset", state, expires_at AS "expiresAt"
|
|
292983
|
+
FROM workspace_bulk_transfers WHERE transfer_id = ${transferId} LIMIT 1
|
|
292984
|
+
`.pipe(require_Schema$1.map((rows) => rows[0] ?? null));
|
|
292985
|
+
const requireTransfer = (transferId) => findTransfer(transferId).pipe(require_Schema$1.flatMap((row) => row === null ? require_Schema$1.fail(transferError({
|
|
292986
|
+
code: "transfer_not_found",
|
|
292987
|
+
message: "Bulk transfer does not exist.",
|
|
292988
|
+
transferId
|
|
292989
|
+
})) : require_Schema$1.succeed(row)));
|
|
292990
|
+
const requireOpenTransfer = (transferId) => requireTransfer(transferId).pipe(require_Schema$1.flatMap((row) => {
|
|
292991
|
+
if (row.state === "cancelled") return require_Schema$1.fail(transferError({
|
|
292992
|
+
code: "transfer_cancelled",
|
|
292993
|
+
message: "Bulk transfer was cancelled.",
|
|
292994
|
+
transferId
|
|
292995
|
+
}));
|
|
292996
|
+
if (row.state === "expired" || Date.parse(row.expiresAt) <= Date.now()) return require_Schema$1.fail(transferError({
|
|
292997
|
+
code: "transfer_expired",
|
|
292998
|
+
message: "Bulk transfer expired.",
|
|
292999
|
+
transferId
|
|
293000
|
+
}));
|
|
293001
|
+
return require_Schema$1.succeed(row);
|
|
293002
|
+
}));
|
|
293003
|
+
const resolveTarget = require_Schema$1.fn("WorkspaceBulkTransfer.resolveTarget")(function* (input) {
|
|
293004
|
+
const workspaceRoot = yield* workspacePaths.normalizeWorkspaceRoot(input.cwd);
|
|
293005
|
+
const resolved = yield* workspacePaths.resolveRelativePathWithinRoot({
|
|
293006
|
+
workspaceRoot,
|
|
293007
|
+
relativePath: input.relativePath
|
|
293008
|
+
});
|
|
293009
|
+
const canonicalRoot = yield* fileSystem.realPath(workspaceRoot);
|
|
293010
|
+
const parentSegments = resolved.relativePath.split("/").slice(0, -1);
|
|
293011
|
+
let canonicalParent = canonicalRoot;
|
|
293012
|
+
for (const segment of parentSegments) {
|
|
293013
|
+
const candidate = path.join(canonicalParent, segment);
|
|
293014
|
+
const canonicalCandidate = yield* fileSystem.realPath(candidate).pipe(require_Schema$1.catch_(() => input.createParent ? fileSystem.makeDirectory(candidate).pipe(require_Schema$1.flatMap(() => fileSystem.realPath(candidate))) : fileSystem.realPath(candidate)));
|
|
293015
|
+
if (!isPathWithinRoot(path, canonicalRoot, canonicalCandidate)) return yield* transferError({
|
|
293016
|
+
code: "symlink_escape",
|
|
293017
|
+
message: "Workspace file path resolves outside the project root."
|
|
293018
|
+
});
|
|
293019
|
+
canonicalParent = canonicalCandidate;
|
|
293020
|
+
}
|
|
293021
|
+
const targetPath = path.join(canonicalParent, path.basename(resolved.relativePath));
|
|
293022
|
+
const targetRealPath = yield* fileSystem.realPath(targetPath).pipe(require_Schema$1.catch_(() => require_Schema$1.succeed(null)));
|
|
293023
|
+
if (targetRealPath !== null && !isPathWithinRoot(path, canonicalRoot, targetRealPath)) return yield* transferError({
|
|
293024
|
+
code: "symlink_escape",
|
|
293025
|
+
message: "Workspace file target resolves outside the project root."
|
|
293026
|
+
});
|
|
293027
|
+
return {
|
|
293028
|
+
workspaceRoot: canonicalRoot,
|
|
293029
|
+
relativePath: resolved.relativePath,
|
|
293030
|
+
targetPath,
|
|
293031
|
+
canonicalParent
|
|
293032
|
+
};
|
|
293033
|
+
});
|
|
293034
|
+
const readFile = (input) => require_Path.unwrap(require_Schema$1.gen(function* () {
|
|
293035
|
+
const target = yield* resolveTarget({
|
|
293036
|
+
...input,
|
|
293037
|
+
createParent: false
|
|
293038
|
+
});
|
|
293039
|
+
const info = yield* fileSystem.stat(target.targetPath);
|
|
293040
|
+
const maxBytes = input.maxBytes ?? 16777216;
|
|
293041
|
+
if (Number(info.size) > maxBytes) return yield* transferError({
|
|
293042
|
+
code: "size_limit_exceeded",
|
|
293043
|
+
message: `Workspace file exceeds the ${maxBytes}-byte read limit.`
|
|
293044
|
+
});
|
|
293045
|
+
const bytes = yield* fileSystem.readFile(target.targetPath);
|
|
293046
|
+
const transfer = yield* require_Schema$1.promise(() => chunkBulkTransfer(bytes));
|
|
293047
|
+
const transferId = transfer.chunks[0]?.transferId ?? crypto.randomUUID().replaceAll("-", "_");
|
|
293048
|
+
return require_Path.fromIterable([
|
|
293049
|
+
{
|
|
293050
|
+
kind: "started",
|
|
293051
|
+
transferId,
|
|
293052
|
+
totalBytes: transfer.totalBytes,
|
|
293053
|
+
sha256: transfer.sha256
|
|
293054
|
+
},
|
|
293055
|
+
...transfer.chunks.map((chunk) => ({
|
|
293056
|
+
...chunk,
|
|
293057
|
+
transferId
|
|
293058
|
+
})),
|
|
293059
|
+
{
|
|
293060
|
+
kind: "completed",
|
|
293061
|
+
transferId,
|
|
293062
|
+
totalBytes: transfer.totalBytes,
|
|
293063
|
+
sha256: transfer.sha256
|
|
293064
|
+
}
|
|
293065
|
+
]);
|
|
293066
|
+
}).pipe(require_Schema$1.mapError(mapFailure("read file"))));
|
|
293067
|
+
const resumeWrite = require_Schema$1.fn("WorkspaceBulkTransfer.resumeWrite")(function* (input, existing) {
|
|
293068
|
+
const target = yield* resolveTarget({
|
|
293069
|
+
...input,
|
|
293070
|
+
createParent: true
|
|
293071
|
+
});
|
|
293072
|
+
if (!(existing.workspaceRoot === target.workspaceRoot && existing.relativePath === target.relativePath && existing.totalBytes === input.totalBytes && existing.wholeSha256 === input.sha256)) return yield* transferError({
|
|
293073
|
+
code: "transfer_identity_conflict",
|
|
293074
|
+
message: "Bulk transfer ID is already bound to different content or a different path.",
|
|
293075
|
+
transferId: input.transferId
|
|
293076
|
+
});
|
|
293077
|
+
if (existing.state === "cancelled") return yield* transferError({
|
|
293078
|
+
code: "transfer_cancelled",
|
|
293079
|
+
message: "Bulk transfer was cancelled.",
|
|
293080
|
+
transferId: input.transferId
|
|
293081
|
+
});
|
|
293082
|
+
if (existing.state === "expired" || Date.parse(existing.expiresAt) <= Date.now()) return yield* transferError({
|
|
293083
|
+
code: "transfer_expired",
|
|
293084
|
+
message: "Bulk transfer expired.",
|
|
293085
|
+
transferId: input.transferId
|
|
293086
|
+
});
|
|
293087
|
+
return {
|
|
293088
|
+
transferId: input.transferId,
|
|
293089
|
+
nextOffset: existing.nextOffset,
|
|
293090
|
+
state: existing.state === "completed" ? "completed" : "open"
|
|
293091
|
+
};
|
|
293092
|
+
});
|
|
293093
|
+
const createWrite = require_Schema$1.fn("WorkspaceBulkTransfer.createWrite")(function* (input) {
|
|
293094
|
+
const target = yield* resolveTarget({
|
|
293095
|
+
...input,
|
|
293096
|
+
createParent: true
|
|
293097
|
+
}).pipe(require_Schema$1.mapError((cause) => require_Schema$1.is(WorkspacePathOutsideRootError)(cause) ? transferError({
|
|
293098
|
+
code: "path_outside_workspace",
|
|
293099
|
+
message: "Workspace file path must stay within the project root.",
|
|
293100
|
+
transferId: input.transferId,
|
|
293101
|
+
cause
|
|
293102
|
+
}) : cause));
|
|
293103
|
+
const tempPath = path.join(target.canonicalParent, `.forge-transfer-${input.transferId}.part`);
|
|
293104
|
+
yield* require_Schema$1.scoped(fileSystem.open(tempPath, {
|
|
293105
|
+
flag: "wx",
|
|
293106
|
+
mode: 384
|
|
293107
|
+
}).pipe(require_Schema$1.flatMap((file) => file.sync), require_Schema$1.catch_(() => fileSystem.stat(tempPath).pipe(require_Schema$1.flatMap((stat) => Number(stat.size) === 0 ? require_Schema$1.void_ : require_Schema$1.fail(transferError({
|
|
293108
|
+
code: "transfer_identity_conflict",
|
|
293109
|
+
message: "Bulk transfer temporary path already contains data.",
|
|
293110
|
+
transferId: input.transferId
|
|
293111
|
+
})))))));
|
|
293112
|
+
const now = /* @__PURE__ */ new Date();
|
|
293113
|
+
yield* sql`
|
|
293114
|
+
INSERT INTO workspace_bulk_transfers (
|
|
293115
|
+
transfer_id, workspace_root, relative_path, target_path, temp_path, total_bytes,
|
|
293116
|
+
whole_sha256, next_offset, state, expires_at, created_at, updated_at
|
|
293117
|
+
) VALUES (
|
|
293118
|
+
${input.transferId}, ${target.workspaceRoot}, ${target.relativePath}, ${target.targetPath},
|
|
293119
|
+
${tempPath}, ${input.totalBytes}, ${input.sha256}, 0, 'open',
|
|
293120
|
+
${new Date(now.getTime() + TRANSFER_TTL_MS).toISOString()}, ${now.toISOString()},
|
|
293121
|
+
${now.toISOString()}
|
|
293122
|
+
)
|
|
293123
|
+
`;
|
|
293124
|
+
return {
|
|
293125
|
+
transferId: input.transferId,
|
|
293126
|
+
nextOffset: 0,
|
|
293127
|
+
state: "open"
|
|
293128
|
+
};
|
|
293129
|
+
});
|
|
293130
|
+
const beginWriteEffect = require_Schema$1.fn("WorkspaceBulkTransfer.beginWrite")(function* (input) {
|
|
293131
|
+
const existing = yield* findTransfer(input.transferId);
|
|
293132
|
+
return yield* existing === null ? createWrite(input) : resumeWrite(input, existing);
|
|
293133
|
+
});
|
|
293134
|
+
const beginWrite = (input) => withTransferLock(input.transferId, beginWriteEffect(input)).pipe(require_Schema$1.mapError(mapFailure("begin write", input.transferId)));
|
|
293135
|
+
const acceptRetriedChunk = require_Schema$1.fn("WorkspaceBulkTransfer.acceptRetriedChunk")(function* (input, row) {
|
|
293136
|
+
const accepted = (yield* sql`
|
|
293137
|
+
SELECT chunk_offset AS "chunkOffset", chunk_bytes AS "chunkBytes",
|
|
293138
|
+
chunk_sha256 AS "chunkSha256"
|
|
293139
|
+
FROM workspace_bulk_transfer_chunks
|
|
293140
|
+
WHERE transfer_id = ${input.transferId} AND chunk_offset = ${input.offset}
|
|
293141
|
+
LIMIT 1
|
|
293142
|
+
`)[0];
|
|
293143
|
+
if (!(accepted !== void 0 && accepted.chunkBytes === input.bytes && accepted.chunkSha256 === input.sha256)) return yield* transferError({
|
|
293144
|
+
code: "integrity_mismatch",
|
|
293145
|
+
message: "Retried bulk transfer chunk differs from the accepted chunk.",
|
|
293146
|
+
transferId: input.transferId
|
|
293147
|
+
});
|
|
293148
|
+
return {
|
|
293149
|
+
transferId: input.transferId,
|
|
293150
|
+
nextOffset: row.nextOffset,
|
|
293151
|
+
duplicate: true
|
|
293152
|
+
};
|
|
293153
|
+
});
|
|
293154
|
+
const validateChunkRange = require_Schema$1.fn("WorkspaceBulkTransfer.validateChunkRange")(function* (input, row, byteLength) {
|
|
293155
|
+
if (input.offset !== row.nextOffset) return yield* transferError({
|
|
293156
|
+
code: "offset_mismatch",
|
|
293157
|
+
message: `Expected chunk offset ${row.nextOffset}, received ${input.offset}.`,
|
|
293158
|
+
transferId: input.transferId,
|
|
293159
|
+
retryable: true
|
|
293160
|
+
});
|
|
293161
|
+
const nextOffset = input.offset + byteLength;
|
|
293162
|
+
if (nextOffset > row.totalBytes) return yield* transferError({
|
|
293163
|
+
code: "size_limit_exceeded",
|
|
293164
|
+
message: "Bulk transfer chunk exceeds the declared total byte count.",
|
|
293165
|
+
transferId: input.transferId
|
|
293166
|
+
});
|
|
293167
|
+
return nextOffset;
|
|
293168
|
+
});
|
|
293169
|
+
const reconcileChunkBytes = require_Schema$1.fn("WorkspaceBulkTransfer.reconcileChunkBytes")(function* (input, row, bytes, nextOffset) {
|
|
293170
|
+
const tempInfo = yield* fileSystem.stat(row.tempPath);
|
|
293171
|
+
if (Number(tempInfo.size) > row.nextOffset) {
|
|
293172
|
+
if (Number(tempInfo.size) !== nextOffset) return yield* transferError({
|
|
293173
|
+
code: "integrity_mismatch",
|
|
293174
|
+
message: "Bulk transfer temporary file cannot be reconciled with the next chunk.",
|
|
293175
|
+
transferId: input.transferId
|
|
293176
|
+
});
|
|
293177
|
+
const recovered = (yield* fileSystem.readFile(row.tempPath)).slice(input.offset, nextOffset);
|
|
293178
|
+
if ((yield* require_Schema$1.promise(() => sha256Hex(recovered))) !== input.sha256) return yield* transferError({
|
|
293179
|
+
code: "integrity_mismatch",
|
|
293180
|
+
message: "Bulk transfer temporary bytes differ from the retried chunk.",
|
|
293181
|
+
transferId: input.transferId
|
|
293182
|
+
});
|
|
293183
|
+
return;
|
|
293184
|
+
}
|
|
293185
|
+
yield* fault("before-byte-write");
|
|
293186
|
+
yield* require_Schema$1.scoped(fileSystem.open(row.tempPath, { flag: "r+" }).pipe(require_Schema$1.flatMap((file) => file.seek(input.offset, "start").pipe(require_Schema$1.flatMap(() => file.writeAll(bytes)), require_Schema$1.flatMap(() => file.sync)))));
|
|
293187
|
+
yield* fault("after-byte-write-before-metadata");
|
|
293188
|
+
});
|
|
293189
|
+
const persistAcceptedChunk = require_Schema$1.fn("WorkspaceBulkTransfer.persistAcceptedChunk")(function* (input, nextOffset) {
|
|
293190
|
+
const updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
293191
|
+
yield* sql.withTransaction(sql`
|
|
293192
|
+
INSERT INTO workspace_bulk_transfer_chunks (
|
|
293193
|
+
transfer_id, chunk_offset, chunk_bytes, chunk_sha256
|
|
293194
|
+
) VALUES (${input.transferId}, ${input.offset}, ${input.bytes}, ${input.sha256})
|
|
293195
|
+
ON CONFLICT DO NOTHING
|
|
293196
|
+
`.pipe(require_Schema$1.flatMap(() => sql`
|
|
293197
|
+
UPDATE workspace_bulk_transfers
|
|
293198
|
+
SET next_offset = ${nextOffset}, updated_at = ${updatedAt}
|
|
293199
|
+
WHERE transfer_id = ${input.transferId} AND next_offset = ${input.offset} AND state = 'open'
|
|
293200
|
+
`)));
|
|
293201
|
+
yield* fault("after-metadata-advance");
|
|
293202
|
+
});
|
|
293203
|
+
const writeChunkEffect = require_Schema$1.fn("WorkspaceBulkTransfer.writeChunk")(function* (input) {
|
|
293204
|
+
const bytes = yield* require_Schema$1.promise(() => verifyBulkTransferChunk({
|
|
293205
|
+
...input,
|
|
293206
|
+
kind: "chunk"
|
|
293207
|
+
}));
|
|
293208
|
+
const row = yield* requireOpenTransfer(input.transferId);
|
|
293209
|
+
if (row.state === "completed") return yield* transferError({
|
|
293210
|
+
code: "offset_mismatch",
|
|
293211
|
+
message: "Bulk transfer is already complete.",
|
|
293212
|
+
transferId: input.transferId
|
|
293213
|
+
});
|
|
293214
|
+
if (input.offset < row.nextOffset) return yield* acceptRetriedChunk(input, row);
|
|
293215
|
+
const nextOffset = yield* validateChunkRange(input, row, bytes.byteLength);
|
|
293216
|
+
yield* reconcileChunkBytes(input, row, bytes, nextOffset);
|
|
293217
|
+
yield* persistAcceptedChunk(input, nextOffset);
|
|
293218
|
+
return {
|
|
293219
|
+
transferId: input.transferId,
|
|
293220
|
+
nextOffset,
|
|
293221
|
+
duplicate: false
|
|
293222
|
+
};
|
|
293223
|
+
});
|
|
293224
|
+
const writeChunk = (input) => withTransferLock(input.transferId, writeChunkEffect(input)).pipe(require_Schema$1.mapError(mapFailure("write chunk", input.transferId)));
|
|
293225
|
+
const commitWriteEffect = require_Schema$1.fn("WorkspaceBulkTransfer.commitWrite")(function* (input) {
|
|
293226
|
+
const row = yield* requireOpenTransfer(input.transferId);
|
|
293227
|
+
if (row.state === "completed") return {
|
|
293228
|
+
transferId: row.transferId,
|
|
293229
|
+
relativePath: row.relativePath,
|
|
293230
|
+
totalBytes: row.totalBytes,
|
|
293231
|
+
sha256: row.wholeSha256
|
|
293232
|
+
};
|
|
293233
|
+
if (row.nextOffset !== row.totalBytes) return yield* transferError({
|
|
293234
|
+
code: "offset_mismatch",
|
|
293235
|
+
message: `Bulk transfer is incomplete at byte ${row.nextOffset} of ${row.totalBytes}.`,
|
|
293236
|
+
transferId: row.transferId,
|
|
293237
|
+
retryable: true
|
|
293238
|
+
});
|
|
293239
|
+
if ((yield* resolveTarget({
|
|
293240
|
+
cwd: row.workspaceRoot,
|
|
293241
|
+
relativePath: row.relativePath,
|
|
293242
|
+
createParent: false
|
|
293243
|
+
})).targetPath !== row.targetPath) return yield* transferError({
|
|
293244
|
+
code: "symlink_escape",
|
|
293245
|
+
message: "Workspace file destination changed during transfer.",
|
|
293246
|
+
transferId: row.transferId
|
|
293247
|
+
});
|
|
293248
|
+
const tempInfo = yield* fileSystem.stat(row.tempPath).pipe(require_Schema$1.catch_(() => require_Schema$1.succeed(null)));
|
|
293249
|
+
const candidatePath = tempInfo === null ? row.targetPath : row.tempPath;
|
|
293250
|
+
const candidate = yield* fileSystem.readFile(candidatePath);
|
|
293251
|
+
if (candidate.byteLength !== row.totalBytes || (yield* require_Schema$1.promise(() => sha256Hex(candidate))) !== row.wholeSha256) return yield* transferError({
|
|
293252
|
+
code: "integrity_mismatch",
|
|
293253
|
+
message: "Bulk transfer payload does not match its declared size and digest.",
|
|
293254
|
+
transferId: row.transferId
|
|
293255
|
+
});
|
|
293256
|
+
if (tempInfo !== null) {
|
|
293257
|
+
yield* fileSystem.rename(row.tempPath, row.targetPath);
|
|
293258
|
+
yield* fault("after-atomic-rename");
|
|
293259
|
+
}
|
|
293260
|
+
yield* sql`
|
|
293261
|
+
UPDATE workspace_bulk_transfers SET state = 'completed', updated_at = ${(/* @__PURE__ */ new Date()).toISOString()}
|
|
293262
|
+
WHERE transfer_id = ${row.transferId}
|
|
293263
|
+
`;
|
|
293264
|
+
yield* workspaceEntries.invalidate(row.workspaceRoot);
|
|
293265
|
+
return {
|
|
293266
|
+
transferId: row.transferId,
|
|
293267
|
+
relativePath: row.relativePath,
|
|
293268
|
+
totalBytes: row.totalBytes,
|
|
293269
|
+
sha256: row.wholeSha256
|
|
293270
|
+
};
|
|
293271
|
+
});
|
|
293272
|
+
const commitWrite = (input) => withTransferLock(input.transferId, commitWriteEffect(input)).pipe(require_Schema$1.mapError(mapFailure("commit write", input.transferId)));
|
|
293273
|
+
const cancelWriteEffect = require_Schema$1.fn("WorkspaceBulkTransfer.cancelWrite")(function* (input) {
|
|
293274
|
+
const row = yield* requireTransfer(input.transferId);
|
|
293275
|
+
if (row.state !== "completed") {
|
|
293276
|
+
yield* fileSystem.remove(row.tempPath, { force: true });
|
|
293277
|
+
yield* sql`
|
|
293278
|
+
UPDATE workspace_bulk_transfers SET state = 'cancelled', updated_at = ${(/* @__PURE__ */ new Date()).toISOString()}
|
|
293279
|
+
WHERE transfer_id = ${input.transferId}
|
|
293280
|
+
`;
|
|
293281
|
+
}
|
|
293282
|
+
return {
|
|
293283
|
+
transferId: input.transferId,
|
|
293284
|
+
state: row.state === "completed" ? "completed" : "cancelled"
|
|
293285
|
+
};
|
|
293286
|
+
});
|
|
293287
|
+
const cancelWrite = (input) => withTransferLock(input.transferId, cancelWriteEffect(input)).pipe(require_Schema$1.mapError(mapFailure("cancel write", input.transferId)));
|
|
293288
|
+
const cleanupExpiredEffect = require_Schema$1.fn("WorkspaceBulkTransfer.cleanupExpired")(function* () {
|
|
293289
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
293290
|
+
const rows = yield* sql`
|
|
293291
|
+
SELECT transfer_id AS "transferId", temp_path AS "tempPath"
|
|
293292
|
+
FROM workspace_bulk_transfers WHERE state = 'open' AND expires_at <= ${now}
|
|
293293
|
+
`;
|
|
293294
|
+
let expiredCount = 0;
|
|
293295
|
+
for (const row of rows) if (yield* withTransferLock(row.transferId, require_Schema$1.gen(function* () {
|
|
293296
|
+
const current = yield* findTransfer(row.transferId);
|
|
293297
|
+
if (current === null || current.state !== "open" || Date.parse(current.expiresAt) > Date.parse(now)) return false;
|
|
293298
|
+
yield* fileSystem.remove(current.tempPath, { force: true });
|
|
293299
|
+
yield* sql`
|
|
293300
|
+
UPDATE workspace_bulk_transfers SET state = 'expired', updated_at = ${now}
|
|
293301
|
+
WHERE transfer_id = ${row.transferId} AND state = 'open' AND expires_at <= ${now}
|
|
293302
|
+
`;
|
|
293303
|
+
return true;
|
|
293304
|
+
}))) expiredCount += 1;
|
|
293305
|
+
return expiredCount;
|
|
293306
|
+
});
|
|
293307
|
+
const cleanupExpired = () => cleanupExpiredEffect().pipe(require_Schema$1.mapError(mapFailure("expiry cleanup")));
|
|
293308
|
+
return {
|
|
293309
|
+
readFile,
|
|
293310
|
+
beginWrite,
|
|
293311
|
+
writeChunk,
|
|
293312
|
+
commitWrite,
|
|
293313
|
+
cancelWrite,
|
|
293314
|
+
cleanupExpired
|
|
293315
|
+
};
|
|
293316
|
+
});
|
|
293317
|
+
const WorkspaceBulkTransferLive = require_Schema$1.effect(WorkspaceBulkTransfer, makeWorkspaceBulkTransfer());
|
|
293318
|
+
//#endregion
|
|
292463
293319
|
//#region src/workspace/Layers/WorkspacePaths.ts
|
|
292464
293320
|
function toPosixRelativePath(input) {
|
|
292465
293321
|
return input.replaceAll("\\", "/");
|
|
@@ -294072,7 +294928,7 @@ function resolveBuildCommitFromEnv(env) {
|
|
|
294072
294928
|
* environment descriptor down instead of reporting an honest "unknown".
|
|
294073
294929
|
*/
|
|
294074
294930
|
function readBakedBuildCommit() {
|
|
294075
|
-
return "
|
|
294931
|
+
return "b79da36c868d5629c76dcec88a3326634a02c2c7";
|
|
294076
294932
|
}
|
|
294077
294933
|
async function resolveServerBuildCommit(input) {
|
|
294078
294934
|
if (isFullCommitSha(input.baked)) return input.baked;
|
|
@@ -316677,7 +317533,7 @@ function claimClaudeReadinessJti(input) {
|
|
|
316677
317533
|
return "claimed";
|
|
316678
317534
|
}
|
|
316679
317535
|
const readUsableClaudeGenerationHash = (config) => {
|
|
316680
|
-
const configuredGenerationHash =
|
|
317536
|
+
const configuredGenerationHash = bootExportedPersonalClaudeGenerationHash();
|
|
316681
317537
|
if (!/^[a-f0-9]{64}$/.test(configuredGenerationHash)) return require_Schema$1.succeed(null);
|
|
316682
317538
|
return require_Schema$1.tryPromise(() => (0, node_fs_promises.readFile)(resolveLocalClaudeCredentialPath(config.forgeClaudeConfigPath), "utf8")).pipe(require_Schema$1.map((credentialJson) => isClaudeSubscriptionCredential(credentialJson) ? configuredGenerationHash : null), require_Schema$1.orElseSucceed(() => null));
|
|
316683
317539
|
};
|
|
@@ -317195,7 +318051,7 @@ const PlatformServicesLive = require_Schema$1.unwrap(require_Schema$1.gen(functi
|
|
|
317195
318051
|
}));
|
|
317196
318052
|
const RuntimeReplicationLayerLive = RuntimeReplicationLive.pipe(require_Schema$1.provideMerge(RuntimeReplicationClientLive), require_Schema$1.provideMerge(RuntimeReplicationOutboxLive), require_Schema$1.provideMerge(ActiveHourMeterLive.pipe(require_Schema$1.provide$1(RuntimeReplicationOutboxLive))));
|
|
317197
318053
|
const RuntimeDrainLayerLive = RuntimeDrainReporterLive.pipe(require_Schema$1.provideMerge(RuntimeDrainStateLive));
|
|
317198
|
-
const RuntimeAwareProviderReactorsLive = require_Schema$1.mergeAll(ProviderRuntimeIngestionLive, ProviderCommandReactorLive).pipe(require_Schema$1.provide$1(RuntimeDrainLayerLive));
|
|
318054
|
+
const RuntimeAwareProviderReactorsLive = require_Schema$1.mergeAll(ProviderRuntimeIngestionLive, ProviderCommandReactorLive).pipe(require_Schema$1.provide$1(RuntimeDrainLayerLive), require_Schema$1.provide$1(ClaudeUsageTelemetryLive));
|
|
317199
318055
|
const PipelineWorkerCoordinatorLayerLive = PipelineWorkerCoordinatorLive.pipe(require_Schema$1.provideMerge(PipelineRunClientLive));
|
|
317200
318056
|
const AgentProtocolReactorLayerLive = AgentProtocolReactorLive.pipe(require_Schema$1.provideMerge(PipelineRunStoreLive));
|
|
317201
318057
|
const PipelineRunCoordinatorLayerLive = PipelineRunCoordinatorLive.pipe(require_Schema$1.provideMerge(PipelineProviderAuthorityInstallerLive), require_Schema$1.provideMerge(PipelineChangeInspectorLive), require_Schema$1.provideMerge(PipelineContextArtifactStoreLive), require_Schema$1.provideMerge(PipelineModelPolicyLive), require_Schema$1.provideMerge(PipelineRunClientLive), require_Schema$1.provideMerge(PipelineRunStoreLive), require_Schema$1.provideMerge(PipelineRunWorkspaceAllocatorLive), require_Schema$1.provideMerge(PipelineStagePolicyLive), require_Schema$1.provideMerge(PipelineWorkerCoordinatorLayerLive));
|