@spotpatch/bridge 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +15 -5
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +1878 -168
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +110 -2
- package/dist/index.d.ts +110 -2
- package/dist/index.js +1884 -164
- package/dist/index.js.map +1 -1
- package/package.json +4 -3
package/dist/index.js
CHANGED
|
@@ -542,11 +542,11 @@ function createSpotPatchBridgeClient(cwd = process.cwd()) {
|
|
|
542
542
|
}
|
|
543
543
|
|
|
544
544
|
// src/cli-runner.ts
|
|
545
|
-
import
|
|
545
|
+
import path9 from "path";
|
|
546
546
|
import {
|
|
547
|
-
ERROR_CODES as
|
|
548
|
-
EXTERNAL_HANDOFF_LIMITS as
|
|
549
|
-
SpotPatchError as
|
|
547
|
+
ERROR_CODES as ERROR_CODES11,
|
|
548
|
+
EXTERNAL_HANDOFF_LIMITS as EXTERNAL_HANDOFF_LIMITS10,
|
|
549
|
+
SpotPatchError as SpotPatchError11
|
|
550
550
|
} from "@spotpatch/shared";
|
|
551
551
|
|
|
552
552
|
// src/active/claude/channel-adapter.ts
|
|
@@ -802,7 +802,7 @@ import { z as z3 } from "zod";
|
|
|
802
802
|
// package.json
|
|
803
803
|
var package_default = {
|
|
804
804
|
name: "@spotpatch/bridge",
|
|
805
|
-
version: "0.
|
|
805
|
+
version: "0.2.0",
|
|
806
806
|
description: "Local MCP inbox and explicit active Agent connectors for SpotPatch handoffs.",
|
|
807
807
|
license: "MIT",
|
|
808
808
|
repository: {
|
|
@@ -853,6 +853,7 @@ var package_default = {
|
|
|
853
853
|
},
|
|
854
854
|
dependencies: {
|
|
855
855
|
"@modelcontextprotocol/server": "2.0.0",
|
|
856
|
+
"@spotpatch/agent": "workspace:^",
|
|
856
857
|
"@spotpatch/shared": "workspace:^",
|
|
857
858
|
zod: "4.4.3"
|
|
858
859
|
},
|
|
@@ -1836,15 +1837,19 @@ async function applyBridgeSetupPlan(plan) {
|
|
|
1836
1837
|
|
|
1837
1838
|
// src/active/codex/errors.ts
|
|
1838
1839
|
var CODEX_ADAPTER_ERROR_CODES = Object.freeze({
|
|
1840
|
+
AUTH_REQUIRED: "CODEX_AUTH_REQUIRED",
|
|
1839
1841
|
BUSY: "CODEX_ADAPTER_BUSY",
|
|
1840
1842
|
CLOSED: "CODEX_ADAPTER_CLOSED",
|
|
1843
|
+
CONFIG_ISOLATION_UNSUPPORTED: "CODEX_CONFIG_ISOLATION_UNSUPPORTED",
|
|
1841
1844
|
EXECUTABLE_NOT_FOUND: "CODEX_EXECUTABLE_NOT_FOUND",
|
|
1842
1845
|
EXECUTABLE_UNTRUSTED: "CODEX_EXECUTABLE_UNTRUSTED",
|
|
1843
1846
|
MCP_NOT_READY: "CODEX_MCP_NOT_READY",
|
|
1847
|
+
MODEL_UNAVAILABLE: "CODEX_MODEL_UNAVAILABLE",
|
|
1844
1848
|
PROCESS_EXITED: "CODEX_PROCESS_EXITED",
|
|
1845
1849
|
PROTOCOL: "CODEX_APP_SERVER_PROTOCOL_ERROR",
|
|
1846
1850
|
REQUEST_FAILED: "CODEX_APP_SERVER_REQUEST_FAILED",
|
|
1847
1851
|
REQUEST_TIMEOUT: "CODEX_APP_SERVER_REQUEST_TIMEOUT",
|
|
1852
|
+
THREAD_CLEANUP_INCOMPLETE: "CODEX_THREAD_CLEANUP_INCOMPLETE",
|
|
1848
1853
|
UNSUPPORTED_VERSION: "CODEX_UNSUPPORTED_VERSION",
|
|
1849
1854
|
WORKSPACE_WRITE_REQUIRED: "CODEX_WORKSPACE_WRITE_REQUIRED"
|
|
1850
1855
|
});
|
|
@@ -1862,9 +1867,14 @@ import { constants as fsConstants } from "fs";
|
|
|
1862
1867
|
import { access, realpath as realpath3, stat } from "fs/promises";
|
|
1863
1868
|
import path4 from "path";
|
|
1864
1869
|
import { spawn } from "child_process";
|
|
1865
|
-
var SUPPORTED_CODEX_VERSION = "0.149.0";
|
|
1866
1870
|
var VERSION_OUTPUT_LIMIT_BYTES = 8 * 1024;
|
|
1867
1871
|
var VERSION_PROBE_TIMEOUT_MS = 5e3;
|
|
1872
|
+
function supportedVersion(value) {
|
|
1873
|
+
const match = /^codex-cli (0)\.(149)\.(\d+)$/u.exec(value);
|
|
1874
|
+
if (match === null) return void 0;
|
|
1875
|
+
const patchVersion = Number(match[3]);
|
|
1876
|
+
return Number.isSafeInteger(patchVersion) ? `0.149.${String(patchVersion)}` : void 0;
|
|
1877
|
+
}
|
|
1868
1878
|
function isWithin(root, candidate) {
|
|
1869
1879
|
const relative = path4.relative(root, candidate);
|
|
1870
1880
|
return relative === "" || !relative.startsWith(`..${path4.sep}`) && relative !== "..";
|
|
@@ -1955,24 +1965,24 @@ async function readCodexVersion(executable) {
|
|
|
1955
1965
|
});
|
|
1956
1966
|
}
|
|
1957
1967
|
async function resolveCodexExecutable(projectRoot, options = {}) {
|
|
1958
|
-
const
|
|
1959
|
-
if (!(await stat(
|
|
1968
|
+
const canonicalRoot2 = await realpath3(projectRoot);
|
|
1969
|
+
if (!(await stat(canonicalRoot2)).isDirectory()) {
|
|
1960
1970
|
throw new CodexAdapterError(CODEX_ADAPTER_ERROR_CODES.EXECUTABLE_UNTRUSTED);
|
|
1961
1971
|
}
|
|
1962
1972
|
const executable = await findOnTrustedPath(
|
|
1963
1973
|
options.pathValue ?? process.env.PATH ?? ""
|
|
1964
1974
|
);
|
|
1965
|
-
if (isWithin(
|
|
1975
|
+
if (isWithin(canonicalRoot2, executable)) {
|
|
1966
1976
|
throw new CodexAdapterError(CODEX_ADAPTER_ERROR_CODES.EXECUTABLE_UNTRUSTED);
|
|
1967
1977
|
}
|
|
1968
1978
|
const output = await readCodexVersion(executable);
|
|
1969
|
-
const
|
|
1970
|
-
if (
|
|
1979
|
+
const version = supportedVersion(output);
|
|
1980
|
+
if (version === void 0) {
|
|
1971
1981
|
throw new CodexAdapterError(CODEX_ADAPTER_ERROR_CODES.UNSUPPORTED_VERSION);
|
|
1972
1982
|
}
|
|
1973
1983
|
return Object.freeze({
|
|
1974
1984
|
path: executable,
|
|
1975
|
-
version
|
|
1985
|
+
version
|
|
1976
1986
|
});
|
|
1977
1987
|
}
|
|
1978
1988
|
|
|
@@ -2844,178 +2854,1173 @@ async function connectCodexAppServer(options) {
|
|
|
2844
2854
|
return CodexAppServerAdapter.connect(options);
|
|
2845
2855
|
}
|
|
2846
2856
|
|
|
2847
|
-
// src/
|
|
2848
|
-
|
|
2849
|
-
|
|
2850
|
-
|
|
2851
|
-
|
|
2852
|
-
|
|
2853
|
-
|
|
2857
|
+
// src/active/codex/managed-adapter.ts
|
|
2858
|
+
import { spawn as spawn3 } from "child_process";
|
|
2859
|
+
import { realpath as realpath7, stat as stat3 } from "fs/promises";
|
|
2860
|
+
import path8 from "path";
|
|
2861
|
+
import {
|
|
2862
|
+
ERROR_CODES as ERROR_CODES10,
|
|
2863
|
+
EXTERNAL_HANDOFF_LIMITS as EXTERNAL_HANDOFF_LIMITS9,
|
|
2864
|
+
SpotPatchError as SpotPatchError10
|
|
2865
|
+
} from "@spotpatch/shared";
|
|
2866
|
+
|
|
2867
|
+
// src/active/codex/managed-runtime.ts
|
|
2868
|
+
import { randomBytes as randomBytes4 } from "crypto";
|
|
2869
|
+
import { lstat as lstat4, readlink, realpath as realpath6, rename as rename3, rm as rm2, symlink } from "fs/promises";
|
|
2870
|
+
import os2 from "os";
|
|
2871
|
+
import path7 from "path";
|
|
2872
|
+
|
|
2873
|
+
// src/supervisor/private-store.ts
|
|
2874
|
+
import { randomBytes as randomBytes3 } from "crypto";
|
|
2875
|
+
import { lstat as lstat3, mkdir as mkdir2, open as open3, readFile, realpath as realpath5, rename as rename2, rm } from "fs/promises";
|
|
2876
|
+
import os from "os";
|
|
2877
|
+
import path6 from "path";
|
|
2878
|
+
function defaultConfigBase() {
|
|
2879
|
+
if (process.platform === "darwin") {
|
|
2880
|
+
return path6.join(os.homedir(), "Library", "Application Support", "SpotPatch");
|
|
2854
2881
|
}
|
|
2855
|
-
|
|
2882
|
+
if (process.platform === "win32") {
|
|
2883
|
+
const localAppData = process.env.LOCALAPPDATA;
|
|
2884
|
+
return path6.join(
|
|
2885
|
+
localAppData !== void 0 && path6.isAbsolute(localAppData) ? localAppData : os.homedir(),
|
|
2886
|
+
"SpotPatch"
|
|
2887
|
+
);
|
|
2888
|
+
}
|
|
2889
|
+
const xdgConfig = process.env.XDG_CONFIG_HOME;
|
|
2890
|
+
return path6.join(
|
|
2891
|
+
xdgConfig !== void 0 && path6.isAbsolute(xdgConfig) ? xdgConfig : path6.join(os.homedir(), ".config"),
|
|
2892
|
+
"spotpatch"
|
|
2893
|
+
);
|
|
2856
2894
|
}
|
|
2857
|
-
function
|
|
2858
|
-
|
|
2859
|
-
|
|
2860
|
-
|
|
2861
|
-
|
|
2862
|
-
|
|
2863
|
-
|
|
2895
|
+
function permissionsArePrivate(mode) {
|
|
2896
|
+
return process.platform === "win32" || (mode & 63) === 0;
|
|
2897
|
+
}
|
|
2898
|
+
function isOwnedByCurrentUser(uid) {
|
|
2899
|
+
const currentUid = process.getuid?.();
|
|
2900
|
+
return currentUid === void 0 || uid === currentUid;
|
|
2901
|
+
}
|
|
2902
|
+
async function ensurePrivateDirectory(directory) {
|
|
2903
|
+
await mkdir2(directory, { recursive: true, mode: 448 });
|
|
2904
|
+
const metadata = await lstat3(directory);
|
|
2905
|
+
if (!metadata.isDirectory() || metadata.isSymbolicLink() || !permissionsArePrivate(metadata.mode) || !isOwnedByCurrentUser(metadata.uid)) {
|
|
2906
|
+
throw new Error("SpotPatch private storage directory is not private.");
|
|
2907
|
+
}
|
|
2908
|
+
}
|
|
2909
|
+
async function resolvePrivateConfigBase(configuredBase) {
|
|
2910
|
+
const configBase = path6.resolve(configuredBase ?? defaultConfigBase());
|
|
2911
|
+
await ensurePrivateDirectory(configBase);
|
|
2912
|
+
return realpath5(configBase);
|
|
2913
|
+
}
|
|
2914
|
+
async function readPrivateJson(filePath, maximumBytes) {
|
|
2915
|
+
const metadata = await lstat3(filePath).catch((error) => {
|
|
2916
|
+
if (typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT") {
|
|
2917
|
+
return void 0;
|
|
2864
2918
|
}
|
|
2865
|
-
|
|
2866
|
-
|
|
2867
|
-
|
|
2868
|
-
|
|
2869
|
-
|
|
2870
|
-
|
|
2871
|
-
|
|
2919
|
+
throw error;
|
|
2920
|
+
});
|
|
2921
|
+
if (metadata === void 0) return void 0;
|
|
2922
|
+
if (!metadata.isFile() || metadata.isSymbolicLink() || !permissionsArePrivate(metadata.mode) || !isOwnedByCurrentUser(metadata.uid) || metadata.size > maximumBytes) {
|
|
2923
|
+
throw new Error("SpotPatch private storage file is not private or bounded.");
|
|
2924
|
+
}
|
|
2925
|
+
return JSON.parse(await readFile(filePath, "utf8"));
|
|
2926
|
+
}
|
|
2927
|
+
async function writePrivateJsonAtomic(filePath, temporaryPrefix, value) {
|
|
2928
|
+
const directory = path6.dirname(filePath);
|
|
2929
|
+
await ensurePrivateDirectory(directory);
|
|
2930
|
+
const temporaryPath = path6.join(
|
|
2931
|
+
directory,
|
|
2932
|
+
`.${temporaryPrefix}-${randomBytes3(12).toString("hex")}.tmp`
|
|
2933
|
+
);
|
|
2934
|
+
try {
|
|
2935
|
+
const handle = await open3(temporaryPath, "wx", 384);
|
|
2936
|
+
try {
|
|
2937
|
+
await handle.writeFile(`${JSON.stringify(value)}
|
|
2938
|
+
`, "utf8");
|
|
2939
|
+
await handle.sync();
|
|
2940
|
+
} finally {
|
|
2941
|
+
await handle.close();
|
|
2872
2942
|
}
|
|
2943
|
+
await rename2(temporaryPath, filePath);
|
|
2944
|
+
} catch (error) {
|
|
2945
|
+
await rm(temporaryPath, { force: true }).catch(() => void 0);
|
|
2946
|
+
throw error;
|
|
2873
2947
|
}
|
|
2874
2948
|
}
|
|
2875
|
-
|
|
2876
|
-
|
|
2877
|
-
|
|
2878
|
-
|
|
2879
|
-
|
|
2949
|
+
|
|
2950
|
+
// src/active/codex/managed-runtime.ts
|
|
2951
|
+
var AUTH_FILE_NAME = "auth.json";
|
|
2952
|
+
var MAXIMUM_AUTH_FILE_BYTES = 1024 * 1024;
|
|
2953
|
+
var RUNTIME_KEY_PATTERN = /^[a-f0-9]{64}$/u;
|
|
2954
|
+
function isMissingPathError(error) {
|
|
2955
|
+
return typeof error === "object" && error !== null && "code" in error && (error.code === "ENOENT" || error.code === "ENOTDIR");
|
|
2956
|
+
}
|
|
2957
|
+
function permissionsArePrivate2(mode) {
|
|
2958
|
+
return process.platform === "win32" || (mode & 63) === 0;
|
|
2959
|
+
}
|
|
2960
|
+
function isOwnedByCurrentUser2(uid) {
|
|
2961
|
+
const currentUid = process.getuid?.();
|
|
2962
|
+
return currentUid === void 0 || uid === currentUid;
|
|
2963
|
+
}
|
|
2964
|
+
function validateRuntimeKey(runtimeKey) {
|
|
2965
|
+
if (!RUNTIME_KEY_PATTERN.test(runtimeKey)) {
|
|
2966
|
+
throw new TypeError("The managed Codex runtime key is invalid.");
|
|
2967
|
+
}
|
|
2968
|
+
}
|
|
2969
|
+
function isWithin2(root, candidate) {
|
|
2970
|
+
const relative = path7.relative(root, candidate);
|
|
2971
|
+
return relative === "" || !relative.startsWith(`..${path7.sep}`) && relative !== "..";
|
|
2972
|
+
}
|
|
2973
|
+
async function canonicalProspectivePath(candidate) {
|
|
2974
|
+
let existing = path7.resolve(candidate);
|
|
2975
|
+
const missingSegments = [];
|
|
2976
|
+
for (; ; ) {
|
|
2977
|
+
const metadata = await lstat4(existing).catch((error) => {
|
|
2978
|
+
if (isMissingPathError(error)) return void 0;
|
|
2979
|
+
throw error;
|
|
2980
|
+
});
|
|
2981
|
+
if (metadata !== void 0) {
|
|
2982
|
+
return path7.join(await realpath6(existing), ...missingSegments.reverse());
|
|
2880
2983
|
}
|
|
2881
|
-
|
|
2882
|
-
|
|
2984
|
+
const parent = path7.dirname(existing);
|
|
2985
|
+
if (parent === existing) throw new Error("No existing path ancestor was found.");
|
|
2986
|
+
missingSegments.push(path7.basename(existing));
|
|
2987
|
+
existing = parent;
|
|
2988
|
+
}
|
|
2989
|
+
}
|
|
2990
|
+
async function resolveSourceAuthFile(environment) {
|
|
2991
|
+
const configuredHome = environment.CODEX_HOME;
|
|
2992
|
+
const sourceHome = configuredHome ?? path7.join(os2.homedir(), ".codex");
|
|
2993
|
+
if (!path7.isAbsolute(sourceHome)) {
|
|
2994
|
+
throw new Error("CODEX_HOME must be absolute for managed isolation.");
|
|
2995
|
+
}
|
|
2996
|
+
const authFile = path7.join(sourceHome, AUTH_FILE_NAME);
|
|
2997
|
+
const metadata = await lstat4(authFile).catch((error) => {
|
|
2998
|
+
if (isMissingPathError(error)) return void 0;
|
|
2999
|
+
throw error;
|
|
3000
|
+
});
|
|
3001
|
+
if (metadata === void 0) return void 0;
|
|
3002
|
+
if (!metadata.isFile() || metadata.isSymbolicLink() || metadata.size > MAXIMUM_AUTH_FILE_BYTES || !permissionsArePrivate2(metadata.mode) || !isOwnedByCurrentUser2(metadata.uid)) {
|
|
3003
|
+
throw new Error("The Codex authentication file is not private and bounded.");
|
|
3004
|
+
}
|
|
3005
|
+
return realpath6(authFile);
|
|
3006
|
+
}
|
|
3007
|
+
async function replaceAuthLink(runtimeHome, sourceAuthFile) {
|
|
3008
|
+
const authLink = path7.join(runtimeHome, AUTH_FILE_NAME);
|
|
3009
|
+
const existing = await lstat4(authLink).catch((error) => {
|
|
3010
|
+
if (isMissingPathError(error)) return void 0;
|
|
3011
|
+
throw error;
|
|
3012
|
+
});
|
|
3013
|
+
if (sourceAuthFile === void 0) {
|
|
3014
|
+
if (existing === void 0) return;
|
|
3015
|
+
if (!existing.isSymbolicLink() || !isOwnedByCurrentUser2(existing.uid)) {
|
|
3016
|
+
throw new Error("The managed Codex authentication entry is not owned.");
|
|
2883
3017
|
}
|
|
2884
|
-
|
|
3018
|
+
await rm2(authLink);
|
|
3019
|
+
return;
|
|
2885
3020
|
}
|
|
2886
|
-
if (
|
|
2887
|
-
|
|
2888
|
-
|
|
2889
|
-
|
|
2890
|
-
return
|
|
3021
|
+
if (existing !== void 0) {
|
|
3022
|
+
if (!existing.isSymbolicLink() || !isOwnedByCurrentUser2(existing.uid)) {
|
|
3023
|
+
throw new Error("The managed Codex authentication entry is not owned.");
|
|
3024
|
+
}
|
|
3025
|
+
if (await readlink(authLink) === sourceAuthFile) return;
|
|
2891
3026
|
}
|
|
2892
|
-
|
|
2893
|
-
|
|
2894
|
-
|
|
2895
|
-
|
|
2896
|
-
|
|
3027
|
+
const temporaryLink = path7.join(
|
|
3028
|
+
runtimeHome,
|
|
3029
|
+
`.auth-${randomBytes4(12).toString("hex")}.tmp`
|
|
3030
|
+
);
|
|
3031
|
+
try {
|
|
3032
|
+
await symlink(sourceAuthFile, temporaryLink, "file");
|
|
3033
|
+
await rename3(temporaryLink, authLink);
|
|
3034
|
+
} finally {
|
|
3035
|
+
await rm2(temporaryLink, { force: true }).catch(() => void 0);
|
|
2897
3036
|
}
|
|
2898
|
-
return 1;
|
|
2899
3037
|
}
|
|
2900
|
-
function
|
|
2901
|
-
|
|
2902
|
-
|
|
3038
|
+
async function resolveRuntimeHome(options) {
|
|
3039
|
+
validateRuntimeKey(options.runtimeKey);
|
|
3040
|
+
if (options.runtimeBase !== void 0 && options.excludedRoot !== void 0 && isWithin2(
|
|
3041
|
+
await realpath6(options.excludedRoot),
|
|
3042
|
+
await canonicalProspectivePath(options.runtimeBase)
|
|
3043
|
+
)) {
|
|
3044
|
+
throw new Error("The managed Codex runtime cannot be stored in the project.");
|
|
3045
|
+
}
|
|
3046
|
+
const configBase = await resolvePrivateConfigBase(options.runtimeBase);
|
|
3047
|
+
const configuredRuntimeRoot = path7.join(
|
|
3048
|
+
configBase,
|
|
3049
|
+
"external-agent-runtime",
|
|
3050
|
+
"codex"
|
|
3051
|
+
);
|
|
3052
|
+
if (options.excludedRoot !== void 0 && isWithin2(await realpath6(options.excludedRoot), configuredRuntimeRoot)) {
|
|
3053
|
+
throw new Error("The managed Codex runtime cannot be stored in the project.");
|
|
3054
|
+
}
|
|
3055
|
+
await ensurePrivateDirectory(configuredRuntimeRoot);
|
|
3056
|
+
const runtimeRoot = await realpath6(configuredRuntimeRoot);
|
|
3057
|
+
const runtimeHome = path7.join(runtimeRoot, options.runtimeKey);
|
|
3058
|
+
await ensurePrivateDirectory(runtimeHome);
|
|
3059
|
+
return realpath6(runtimeHome);
|
|
2903
3060
|
}
|
|
2904
|
-
function
|
|
2905
|
-
|
|
2906
|
-
|
|
3061
|
+
async function prepareManagedCodexRuntimeHome(options) {
|
|
3062
|
+
const sourceAuthFile = await resolveSourceAuthFile(
|
|
3063
|
+
options.environment ?? process.env
|
|
3064
|
+
);
|
|
3065
|
+
const runtimeHome = await resolveRuntimeHome(options);
|
|
3066
|
+
await replaceAuthLink(runtimeHome, sourceAuthFile);
|
|
3067
|
+
return runtimeHome;
|
|
3068
|
+
}
|
|
3069
|
+
async function removeManagedCodexRuntimeHome(options) {
|
|
3070
|
+
validateRuntimeKey(options.runtimeKey);
|
|
3071
|
+
const configBase = await resolvePrivateConfigBase(options.runtimeBase);
|
|
3072
|
+
const configuredRuntimeRoot = path7.join(
|
|
3073
|
+
configBase,
|
|
3074
|
+
"external-agent-runtime",
|
|
3075
|
+
"codex"
|
|
3076
|
+
);
|
|
3077
|
+
const runtimeRootMetadata = await lstat4(configuredRuntimeRoot).catch(
|
|
3078
|
+
(error) => {
|
|
3079
|
+
if (isMissingPathError(error)) return void 0;
|
|
3080
|
+
throw error;
|
|
3081
|
+
}
|
|
2907
3082
|
);
|
|
3083
|
+
if (runtimeRootMetadata === void 0) return;
|
|
3084
|
+
if (!runtimeRootMetadata.isDirectory() || runtimeRootMetadata.isSymbolicLink() || !permissionsArePrivate2(runtimeRootMetadata.mode) || !isOwnedByCurrentUser2(runtimeRootMetadata.uid)) {
|
|
3085
|
+
throw new Error("The managed Codex runtime root is not private.");
|
|
3086
|
+
}
|
|
3087
|
+
const runtimeRoot = await realpath6(configuredRuntimeRoot);
|
|
3088
|
+
const runtimeHome = path7.join(runtimeRoot, options.runtimeKey);
|
|
3089
|
+
const runtimeMetadata = await lstat4(runtimeHome).catch((error) => {
|
|
3090
|
+
if (isMissingPathError(error)) return void 0;
|
|
3091
|
+
throw error;
|
|
3092
|
+
});
|
|
3093
|
+
if (runtimeMetadata === void 0) return;
|
|
3094
|
+
if (!runtimeMetadata.isDirectory() || runtimeMetadata.isSymbolicLink() || !permissionsArePrivate2(runtimeMetadata.mode) || !isOwnedByCurrentUser2(runtimeMetadata.uid)) {
|
|
3095
|
+
throw new Error("The managed Codex runtime is not owned.");
|
|
3096
|
+
}
|
|
3097
|
+
await rm2(runtimeHome, { recursive: true, force: true });
|
|
3098
|
+
}
|
|
3099
|
+
|
|
3100
|
+
// src/active/codex/managed-adapter.ts
|
|
3101
|
+
var CLIENT_NAME2 = "spotpatch";
|
|
3102
|
+
var CLIENT_TITLE2 = "SpotPatch";
|
|
3103
|
+
var MANAGED_PERMISSION_PROFILE = "spotpatch-managed";
|
|
3104
|
+
var DEFAULT_PROCESS_SHUTDOWN_TIMEOUT_MS2 = 2e3;
|
|
3105
|
+
var MAXIMUM_MODEL_PAGES = 8;
|
|
3106
|
+
var MAXIMUM_MCP_STATUS_PAGES2 = 8;
|
|
3107
|
+
var MANAGED_SHELL_ENVIRONMENT_NAMES = Object.freeze([
|
|
3108
|
+
"LANG",
|
|
3109
|
+
"LC_ALL",
|
|
3110
|
+
"NODE_ENV",
|
|
3111
|
+
"NO_COLOR",
|
|
3112
|
+
"PATH",
|
|
3113
|
+
"PATHEXT",
|
|
3114
|
+
"SSL_CERT_DIR",
|
|
3115
|
+
"SSL_CERT_FILE",
|
|
3116
|
+
"SYSTEMROOT",
|
|
3117
|
+
"TEMP",
|
|
3118
|
+
"TMP",
|
|
3119
|
+
"TMPDIR"
|
|
3120
|
+
]);
|
|
3121
|
+
var MANAGED_SHELL_ENVIRONMENT_FILTERS = Object.freeze(
|
|
3122
|
+
Object.fromEntries(
|
|
3123
|
+
MANAGED_SHELL_ENVIRONMENT_NAMES.map((name) => [name, "include"])
|
|
3124
|
+
)
|
|
3125
|
+
);
|
|
3126
|
+
var MANAGED_SHELL_ENVIRONMENT_POLICY = Object.freeze({
|
|
3127
|
+
inherit: "all",
|
|
3128
|
+
ignore_default_excludes: false,
|
|
3129
|
+
filters: MANAGED_SHELL_ENVIRONMENT_FILTERS
|
|
3130
|
+
});
|
|
3131
|
+
var MANAGED_SHELL_ENVIRONMENT_POLICY_TOML = `{ inherit="all", ignore_default_excludes=false, filters={ ${MANAGED_SHELL_ENVIRONMENT_NAMES.map(
|
|
3132
|
+
(name) => `${name}="include"`
|
|
3133
|
+
).join(", ")} } }`;
|
|
3134
|
+
var MANAGED_CODEX_CONFIG_OVERRIDES = Object.freeze([
|
|
3135
|
+
"agents.enabled=false",
|
|
3136
|
+
"features.apps=false",
|
|
3137
|
+
"features.hooks=false",
|
|
3138
|
+
"features.plugins=false",
|
|
3139
|
+
"features.remote_plugin=false",
|
|
3140
|
+
'web_search="disabled"',
|
|
3141
|
+
"mcp_servers={}",
|
|
3142
|
+
`shell_environment_policy=${MANAGED_SHELL_ENVIRONMENT_POLICY_TOML}`
|
|
3143
|
+
]);
|
|
3144
|
+
function isRecord3(value) {
|
|
3145
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
3146
|
+
}
|
|
3147
|
+
function hasOnlyKeys2(value, keys) {
|
|
3148
|
+
const actual = Object.keys(value);
|
|
3149
|
+
return actual.length === keys.length && keys.every((key) => key in value);
|
|
2908
3150
|
}
|
|
2909
3151
|
function abortError4() {
|
|
2910
|
-
const error = new Error("The
|
|
3152
|
+
const error = new Error("The managed Codex operation was aborted.");
|
|
2911
3153
|
error.name = "AbortError";
|
|
2912
3154
|
return error;
|
|
2913
3155
|
}
|
|
2914
|
-
function
|
|
2915
|
-
if (
|
|
2916
|
-
|
|
2917
|
-
|
|
2918
|
-
|
|
2919
|
-
|
|
2920
|
-
|
|
2921
|
-
|
|
2922
|
-
|
|
2923
|
-
|
|
2924
|
-
|
|
2925
|
-
|
|
2926
|
-
|
|
2927
|
-
|
|
2928
|
-
|
|
2929
|
-
|
|
2930
|
-
|
|
2931
|
-
|
|
2932
|
-
|
|
2933
|
-
|
|
2934
|
-
|
|
2935
|
-
|
|
2936
|
-
|
|
2937
|
-
|
|
3156
|
+
function managedFailureReason(error, fallback) {
|
|
3157
|
+
if (error instanceof CodexAdapterError) {
|
|
3158
|
+
if (error.code === CODEX_ADAPTER_ERROR_CODES.CONFIG_ISOLATION_UNSUPPORTED) {
|
|
3159
|
+
return "config-isolation";
|
|
3160
|
+
}
|
|
3161
|
+
if (error.code === CODEX_ADAPTER_ERROR_CODES.PROTOCOL || error.code === CODEX_ADAPTER_ERROR_CODES.REQUEST_FAILED) {
|
|
3162
|
+
return "protocol";
|
|
3163
|
+
}
|
|
3164
|
+
return fallback;
|
|
3165
|
+
}
|
|
3166
|
+
if (!(error instanceof SpotPatchError10)) return fallback;
|
|
3167
|
+
switch (error.code) {
|
|
3168
|
+
case ERROR_CODES10.AGENT_LIMIT_EXCEEDED:
|
|
3169
|
+
return "change-limit";
|
|
3170
|
+
case ERROR_CODES10.PATCH_REJECTED:
|
|
3171
|
+
case ERROR_CODES10.HANDOFF_VALIDATION_FAILED:
|
|
3172
|
+
return "scope";
|
|
3173
|
+
case ERROR_CODES10.VALIDATION_FAILED:
|
|
3174
|
+
return "validation";
|
|
3175
|
+
case ERROR_CODES10.APPLY_CONFLICT:
|
|
3176
|
+
return "workspace-conflict";
|
|
3177
|
+
case ERROR_CODES10.WORKTREE_CONFLICTED:
|
|
3178
|
+
case ERROR_CODES10.WORKTREE_DIRTY:
|
|
3179
|
+
case ERROR_CODES10.WORKTREE_LOCAL_CHANGES_TOO_LARGE:
|
|
3180
|
+
case ERROR_CODES10.WORKTREE_LOCAL_CHANGES_UNSUPPORTED:
|
|
3181
|
+
case ERROR_CODES10.WORKTREE_NOT_REPOSITORY:
|
|
3182
|
+
case ERROR_CODES10.WORKTREE_OPERATION_IN_PROGRESS:
|
|
3183
|
+
case ERROR_CODES10.WORKTREE_UNTRACKED_UNSUPPORTED:
|
|
3184
|
+
return "snapshot";
|
|
3185
|
+
default:
|
|
3186
|
+
return fallback;
|
|
3187
|
+
}
|
|
2938
3188
|
}
|
|
2939
|
-
function
|
|
2940
|
-
|
|
2941
|
-
|
|
2942
|
-
|
|
2943
|
-
|
|
2944
|
-
|
|
3189
|
+
function throwIfAborted3(signal) {
|
|
3190
|
+
if (signal?.aborted === true) throw abortError4();
|
|
3191
|
+
}
|
|
3192
|
+
function signalProcessTree2(child, signal) {
|
|
3193
|
+
if (process.platform !== "win32" && child.pid !== void 0) {
|
|
3194
|
+
try {
|
|
3195
|
+
process.kill(-child.pid, signal);
|
|
3196
|
+
return;
|
|
3197
|
+
} catch {
|
|
3198
|
+
}
|
|
3199
|
+
}
|
|
3200
|
+
child.kill(signal);
|
|
3201
|
+
}
|
|
3202
|
+
function managedEnvironment(codexHome) {
|
|
3203
|
+
const names = [
|
|
3204
|
+
"CODEX_ACCESS_TOKEN",
|
|
3205
|
+
"CODEX_API_KEY",
|
|
3206
|
+
"OPENAI_API_KEY",
|
|
3207
|
+
"PATH",
|
|
3208
|
+
"PATHEXT",
|
|
3209
|
+
"SYSTEMROOT",
|
|
3210
|
+
"TMPDIR",
|
|
3211
|
+
"TMP",
|
|
3212
|
+
"TEMP",
|
|
3213
|
+
"LANG",
|
|
3214
|
+
"LC_ALL",
|
|
3215
|
+
"SSL_CERT_FILE",
|
|
3216
|
+
"SSL_CERT_DIR",
|
|
3217
|
+
"HTTP_PROXY",
|
|
3218
|
+
"HTTPS_PROXY",
|
|
3219
|
+
"ALL_PROXY",
|
|
3220
|
+
"http_proxy",
|
|
3221
|
+
"https_proxy",
|
|
3222
|
+
"all_proxy"
|
|
3223
|
+
];
|
|
3224
|
+
const environment = {
|
|
3225
|
+
CODEX_HOME: codexHome,
|
|
3226
|
+
HOME: codexHome,
|
|
3227
|
+
NO_COLOR: "1",
|
|
3228
|
+
NODE_ENV: process.env.NODE_ENV ?? "development",
|
|
3229
|
+
USERPROFILE: codexHome
|
|
2945
3230
|
};
|
|
2946
|
-
|
|
2947
|
-
|
|
3231
|
+
for (const name of names) {
|
|
3232
|
+
const value = process.env[name];
|
|
3233
|
+
if (value !== void 0) environment[name] = value;
|
|
3234
|
+
}
|
|
3235
|
+
const loopback = "localhost,127.0.0.1,::1";
|
|
3236
|
+
const existing = process.env.NO_PROXY ?? process.env.no_proxy;
|
|
3237
|
+
environment.NO_PROXY = existing === void 0 ? loopback : `${loopback},${existing}`;
|
|
3238
|
+
environment.no_proxy = environment.NO_PROXY;
|
|
3239
|
+
return environment;
|
|
3240
|
+
}
|
|
3241
|
+
function managedThreadConfig() {
|
|
2948
3242
|
return Object.freeze({
|
|
2949
|
-
|
|
2950
|
-
|
|
2951
|
-
|
|
2952
|
-
|
|
2953
|
-
|
|
3243
|
+
agents: Object.freeze({ enabled: false }),
|
|
3244
|
+
default_permissions: MANAGED_PERMISSION_PROFILE,
|
|
3245
|
+
features: Object.freeze({
|
|
3246
|
+
apps: false,
|
|
3247
|
+
hooks: false,
|
|
3248
|
+
plugins: false,
|
|
3249
|
+
remote_plugin: false
|
|
3250
|
+
}),
|
|
3251
|
+
mcp_servers: Object.freeze({}),
|
|
3252
|
+
permissions: Object.freeze({
|
|
3253
|
+
[MANAGED_PERMISSION_PROFILE]: Object.freeze({
|
|
3254
|
+
filesystem: Object.freeze({
|
|
3255
|
+
":root": "deny",
|
|
3256
|
+
":minimal": "read",
|
|
3257
|
+
":workspace_roots": Object.freeze({ ".": "write" })
|
|
3258
|
+
}),
|
|
3259
|
+
network: Object.freeze({ enabled: false })
|
|
3260
|
+
})
|
|
3261
|
+
}),
|
|
3262
|
+
shell_environment_policy: MANAGED_SHELL_ENVIRONMENT_POLICY,
|
|
3263
|
+
web_search: "disabled"
|
|
2954
3264
|
});
|
|
2955
3265
|
}
|
|
2956
|
-
|
|
2957
|
-
|
|
2958
|
-
|
|
2959
|
-
|
|
3266
|
+
function verifyInitializeResponse2(value, codexHome) {
|
|
3267
|
+
if (!isRecord3(value) || typeof value.userAgent !== "string" || value.codexHome !== codexHome || typeof value.platformFamily !== "string" || typeof value.platformOs !== "string") {
|
|
3268
|
+
throw new CodexAdapterError(CODEX_ADAPTER_ERROR_CODES.PROTOCOL);
|
|
3269
|
+
}
|
|
3270
|
+
}
|
|
3271
|
+
function parseAuthReadiness(value) {
|
|
3272
|
+
if (!isRecord3(value) || !hasOnlyKeys2(value, ["account", "requiresOpenaiAuth"]) || typeof value.requiresOpenaiAuth !== "boolean" || value.account !== null && !isRecord3(value.account)) {
|
|
3273
|
+
throw new CodexAdapterError(CODEX_ADAPTER_ERROR_CODES.PROTOCOL);
|
|
3274
|
+
}
|
|
3275
|
+
if (value.account !== null) return "authenticated";
|
|
3276
|
+
return value.requiresOpenaiAuth ? "signed-out" : "auth-not-required";
|
|
3277
|
+
}
|
|
3278
|
+
function parseModelPage(value) {
|
|
3279
|
+
if (!isRecord3(value) || !Array.isArray(value.data) || value.nextCursor !== null && typeof value.nextCursor !== "string") {
|
|
3280
|
+
throw new CodexAdapterError(CODEX_ADAPTER_ERROR_CODES.PROTOCOL);
|
|
3281
|
+
}
|
|
3282
|
+
const models = value.data.map((item) => {
|
|
3283
|
+
if (!isRecord3(item) || typeof item.model !== "string" || item.model.length === 0 || typeof item.isDefault !== "boolean") {
|
|
3284
|
+
throw new CodexAdapterError(CODEX_ADAPTER_ERROR_CODES.PROTOCOL);
|
|
3285
|
+
}
|
|
3286
|
+
return Object.freeze({ model: item.model, isDefault: item.isDefault });
|
|
2960
3287
|
});
|
|
2961
|
-
|
|
2962
|
-
|
|
2963
|
-
|
|
2964
|
-
|
|
2965
|
-
|
|
2966
|
-
|
|
2967
|
-
|
|
2968
|
-
|
|
2969
|
-
|
|
2970
|
-
|
|
3288
|
+
return Object.freeze({ models: Object.freeze(models), nextCursor: value.nextCursor });
|
|
3289
|
+
}
|
|
3290
|
+
function verifyConfigRequirements(value) {
|
|
3291
|
+
if (!isRecord3(value) || !hasOnlyKeys2(value, ["requirements"])) {
|
|
3292
|
+
throw new CodexAdapterError(CODEX_ADAPTER_ERROR_CODES.PROTOCOL);
|
|
3293
|
+
}
|
|
3294
|
+
if (value.requirements === null) return;
|
|
3295
|
+
if (!isRecord3(value.requirements)) {
|
|
3296
|
+
throw new CodexAdapterError(CODEX_ADAPTER_ERROR_CODES.PROTOCOL);
|
|
3297
|
+
}
|
|
3298
|
+
const approvals = value.requirements.allowedApprovalPolicies;
|
|
3299
|
+
const sandboxes = value.requirements.allowedSandboxModes;
|
|
3300
|
+
const permissionProfiles = value.requirements.allowedPermissionProfiles;
|
|
3301
|
+
if (Array.isArray(approvals) && !approvals.includes("never") || permissionProfiles !== null && (!isRecord3(permissionProfiles) || permissionProfiles[MANAGED_PERMISSION_PROFILE] !== true) || permissionProfiles === null && Array.isArray(sandboxes) && !sandboxes.includes("workspaceWrite")) {
|
|
3302
|
+
throw new CodexAdapterError(CODEX_ADAPTER_ERROR_CODES.PROTOCOL);
|
|
2971
3303
|
}
|
|
2972
3304
|
}
|
|
2973
|
-
function
|
|
2974
|
-
if (
|
|
2975
|
-
|
|
2976
|
-
"[spotpatch:bridge] Codex connected and ready for SpotPatch requests.\n"
|
|
2977
|
-
);
|
|
2978
|
-
return;
|
|
3305
|
+
function parseThreadStartResponse2(value, root) {
|
|
3306
|
+
if (!isRecord3(value) || !isRecord3(value.thread) || typeof value.thread.id !== "string" || value.thread.ephemeral !== false || value.thread.cwd !== root || value.cwd !== root || value.approvalPolicy !== "never" || !Array.isArray(value.runtimeWorkspaceRoots) || value.runtimeWorkspaceRoots.length !== 1 || value.runtimeWorkspaceRoots[0] !== root || !isRecord3(value.activePermissionProfile) || value.activePermissionProfile.id !== MANAGED_PERMISSION_PROFILE) {
|
|
3307
|
+
throw new CodexAdapterError(CODEX_ADAPTER_ERROR_CODES.PROTOCOL);
|
|
2979
3308
|
}
|
|
2980
|
-
|
|
2981
|
-
|
|
2982
|
-
|
|
2983
|
-
|
|
2984
|
-
|
|
2985
|
-
|
|
2986
|
-
|
|
2987
|
-
|
|
2988
|
-
case "working":
|
|
2989
|
-
stderr.write(`[spotpatch:bridge] Codex started revision ${revision}.
|
|
2990
|
-
`);
|
|
2991
|
-
return;
|
|
2992
|
-
case "completed":
|
|
2993
|
-
stderr.write(
|
|
2994
|
-
`[spotpatch:bridge] Codex turn ended for revision ${revision}; review the workspace. Ready for the next request.
|
|
2995
|
-
`
|
|
2996
|
-
);
|
|
2997
|
-
return;
|
|
2998
|
-
case "failed":
|
|
2999
|
-
stderr.write(
|
|
3000
|
-
`[spotpatch:bridge] Revision ${revision} failed before a verified Codex turn completed; review the connector output and workspace. Ready for the next request.
|
|
3001
|
-
`
|
|
3002
|
-
);
|
|
3003
|
-
return;
|
|
3004
|
-
case "delivery-unknown":
|
|
3005
|
-
stderr.write(
|
|
3006
|
-
`[spotpatch:bridge] Codex delivery for revision ${revision} is unknown; the connector will stop.
|
|
3007
|
-
`
|
|
3008
|
-
);
|
|
3009
|
-
return;
|
|
3010
|
-
case "dispatched":
|
|
3011
|
-
stderr.write(`[spotpatch:bridge] Codex accepted revision ${revision}.
|
|
3012
|
-
`);
|
|
3013
|
-
return;
|
|
3309
|
+
return value.thread.id;
|
|
3310
|
+
}
|
|
3311
|
+
function parseTurn2(value) {
|
|
3312
|
+
if (!isRecord3(value) || typeof value.id !== "string") {
|
|
3313
|
+
throw new CodexAdapterError(CODEX_ADAPTER_ERROR_CODES.PROTOCOL);
|
|
3314
|
+
}
|
|
3315
|
+
if (value.status !== "inProgress" && value.status !== "completed" && value.status !== "failed" && value.status !== "interrupted") {
|
|
3316
|
+
throw new CodexAdapterError(CODEX_ADAPTER_ERROR_CODES.PROTOCOL);
|
|
3014
3317
|
}
|
|
3318
|
+
return Object.freeze({ id: value.id, status: value.status });
|
|
3015
3319
|
}
|
|
3016
|
-
|
|
3017
|
-
|
|
3018
|
-
|
|
3320
|
+
function parseTurnEvent2(method, params) {
|
|
3321
|
+
if (method !== "turn/started" && method !== "turn/completed") return void 0;
|
|
3322
|
+
if (!isRecord3(params) || typeof params.threadId !== "string") {
|
|
3323
|
+
throw new CodexAdapterError(CODEX_ADAPTER_ERROR_CODES.PROTOCOL);
|
|
3324
|
+
}
|
|
3325
|
+
const turn = parseTurn2(params.turn);
|
|
3326
|
+
if (method === "turn/started" && turn.status !== "inProgress" || method === "turn/completed" && turn.status === "inProgress") {
|
|
3327
|
+
throw new CodexAdapterError(CODEX_ADAPTER_ERROR_CODES.PROTOCOL);
|
|
3328
|
+
}
|
|
3329
|
+
return Object.freeze({
|
|
3330
|
+
kind: method === "turn/started" ? "started" : "completed",
|
|
3331
|
+
status: turn.status,
|
|
3332
|
+
threadId: params.threadId,
|
|
3333
|
+
turnId: turn.id
|
|
3334
|
+
});
|
|
3335
|
+
}
|
|
3336
|
+
async function canonicalRoot(root) {
|
|
3337
|
+
const canonical = await realpath7(root);
|
|
3338
|
+
if (!(await stat3(canonical)).isDirectory()) {
|
|
3339
|
+
throw new CodexAdapterError(CODEX_ADAPTER_ERROR_CODES.EXECUTABLE_UNTRUSTED);
|
|
3340
|
+
}
|
|
3341
|
+
return canonical;
|
|
3342
|
+
}
|
|
3343
|
+
var ManagedCodexAppServerAdapter = class _ManagedCodexAppServerAdapter {
|
|
3344
|
+
kind = "codex-app-server";
|
|
3345
|
+
#child;
|
|
3346
|
+
#client;
|
|
3347
|
+
#cleanupJournal;
|
|
3348
|
+
#execution;
|
|
3349
|
+
#onEvent;
|
|
3350
|
+
#processShutdownTimeoutMs;
|
|
3351
|
+
#terminalTimeoutMs;
|
|
3352
|
+
#active;
|
|
3353
|
+
#closed = false;
|
|
3354
|
+
#closePromise;
|
|
3355
|
+
#fatalError;
|
|
3356
|
+
constructor(child, client, options) {
|
|
3357
|
+
this.#child = child;
|
|
3358
|
+
this.#client = client;
|
|
3359
|
+
this.#cleanupJournal = options.cleanupJournal;
|
|
3360
|
+
this.#execution = options.execution;
|
|
3361
|
+
this.#onEvent = options.onEvent;
|
|
3362
|
+
this.#processShutdownTimeoutMs = options.processShutdownTimeoutMs ?? DEFAULT_PROCESS_SHUTDOWN_TIMEOUT_MS2;
|
|
3363
|
+
this.#terminalTimeoutMs = options.terminalTimeoutMs ?? EXTERNAL_HANDOFF_LIMITS9.activeDispatchTimeoutMs;
|
|
3364
|
+
}
|
|
3365
|
+
static async connect(options) {
|
|
3366
|
+
throwIfAborted3(options.signal);
|
|
3367
|
+
const projectRoot = await canonicalRoot(options.projectRoot);
|
|
3368
|
+
const executable = await resolveCodexExecutable(projectRoot, {
|
|
3369
|
+
...options.pathValue === void 0 ? {} : { pathValue: options.pathValue }
|
|
3370
|
+
});
|
|
3371
|
+
let codexHome;
|
|
3372
|
+
try {
|
|
3373
|
+
codexHome = await prepareManagedCodexRuntimeHome({
|
|
3374
|
+
excludedRoot: projectRoot,
|
|
3375
|
+
...options.privateRuntimeBase === void 0 ? {} : { runtimeBase: options.privateRuntimeBase },
|
|
3376
|
+
runtimeKey: options.runtimeKey
|
|
3377
|
+
});
|
|
3378
|
+
} catch (error) {
|
|
3379
|
+
throw new CodexAdapterError(
|
|
3380
|
+
CODEX_ADAPTER_ERROR_CODES.CONFIG_ISOLATION_UNSUPPORTED,
|
|
3381
|
+
error
|
|
3382
|
+
);
|
|
3383
|
+
}
|
|
3384
|
+
const args = MANAGED_CODEX_CONFIG_OVERRIDES.flatMap((value) => ["-c", value]);
|
|
3385
|
+
args.push("app-server");
|
|
3386
|
+
const child = spawn3(executable.path, args, {
|
|
3387
|
+
cwd: path8.dirname(executable.path),
|
|
3388
|
+
detached: process.platform !== "win32",
|
|
3389
|
+
env: managedEnvironment(codexHome),
|
|
3390
|
+
shell: false,
|
|
3391
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
3392
|
+
windowsHide: true
|
|
3393
|
+
});
|
|
3394
|
+
const state = {};
|
|
3395
|
+
let earlyFatal;
|
|
3396
|
+
const client = new CodexJsonlClient(child, {
|
|
3397
|
+
...options.maximumLineBytes === void 0 ? {} : { maximumLineBytes: options.maximumLineBytes },
|
|
3398
|
+
...options.maximumStderrBytes === void 0 ? {} : { maximumStderrBytes: options.maximumStderrBytes },
|
|
3399
|
+
...options.requestTimeoutMs === void 0 ? {} : { requestTimeoutMs: options.requestTimeoutMs },
|
|
3400
|
+
onFatal(error) {
|
|
3401
|
+
if (state.adapter === void 0) earlyFatal = error;
|
|
3402
|
+
else state.adapter.#handleFatal(error);
|
|
3403
|
+
},
|
|
3404
|
+
onNotification(method, params) {
|
|
3405
|
+
if (state.adapter !== void 0) {
|
|
3406
|
+
state.adapter.#handleNotification(method, params);
|
|
3407
|
+
}
|
|
3408
|
+
}
|
|
3409
|
+
});
|
|
3410
|
+
const adapter = new _ManagedCodexAppServerAdapter(child, client, options);
|
|
3411
|
+
state.adapter = adapter;
|
|
3412
|
+
if (earlyFatal !== void 0) adapter.#handleFatal(earlyFatal);
|
|
3413
|
+
const abort = () => {
|
|
3414
|
+
void adapter.close().catch(() => void 0);
|
|
3415
|
+
};
|
|
3416
|
+
options.signal.addEventListener("abort", abort, { once: true });
|
|
3417
|
+
try {
|
|
3418
|
+
const initialized = await adapter.#request("initialize", {
|
|
3419
|
+
clientInfo: {
|
|
3420
|
+
name: CLIENT_NAME2,
|
|
3421
|
+
title: CLIENT_TITLE2,
|
|
3422
|
+
version: package_default.version
|
|
3423
|
+
},
|
|
3424
|
+
capabilities: { experimentalApi: true, requestAttestation: false }
|
|
3425
|
+
});
|
|
3426
|
+
verifyInitializeResponse2(initialized, codexHome);
|
|
3427
|
+
client.notify("initialized");
|
|
3428
|
+
const account = parseAuthReadiness(
|
|
3429
|
+
await adapter.#request("account/read", { refreshToken: false })
|
|
3430
|
+
);
|
|
3431
|
+
if (account === "signed-out") {
|
|
3432
|
+
throw new CodexAdapterError(CODEX_ADAPTER_ERROR_CODES.AUTH_REQUIRED);
|
|
3433
|
+
}
|
|
3434
|
+
const requestedModel = await adapter.#readDefaultModel();
|
|
3435
|
+
verifyConfigRequirements(await adapter.#request("configRequirements/read", {}));
|
|
3436
|
+
await adapter.#recoverThreadCleanup();
|
|
3437
|
+
throwIfAborted3(options.signal);
|
|
3438
|
+
return Object.freeze({
|
|
3439
|
+
adapter,
|
|
3440
|
+
authReadiness: account,
|
|
3441
|
+
requestedModel,
|
|
3442
|
+
effectiveModel: requestedModel
|
|
3443
|
+
});
|
|
3444
|
+
} catch (error) {
|
|
3445
|
+
await adapter.close();
|
|
3446
|
+
throwIfAborted3(options.signal);
|
|
3447
|
+
throw error;
|
|
3448
|
+
} finally {
|
|
3449
|
+
options.signal.removeEventListener("abort", abort);
|
|
3450
|
+
}
|
|
3451
|
+
}
|
|
3452
|
+
diagnostics() {
|
|
3453
|
+
return this.#client.diagnostics();
|
|
3454
|
+
}
|
|
3455
|
+
async deliver(handoff, lifecycle, signal) {
|
|
3456
|
+
if (this.#active !== void 0) {
|
|
3457
|
+
throw new CodexAdapterError(CODEX_ADAPTER_ERROR_CODES.BUSY);
|
|
3458
|
+
}
|
|
3459
|
+
if (this.#closed) {
|
|
3460
|
+
throw this.#fatalError ?? new CodexAdapterError(CODEX_ADAPTER_ERROR_CODES.CLOSED);
|
|
3461
|
+
}
|
|
3462
|
+
this.#onEvent({ type: "phase", revision: handoff.revision, phase: "preparing" });
|
|
3463
|
+
let task;
|
|
3464
|
+
try {
|
|
3465
|
+
task = await this.#execution.prepare(
|
|
3466
|
+
{ annotation: handoff.annotation, revision: handoff.revision },
|
|
3467
|
+
signal
|
|
3468
|
+
);
|
|
3469
|
+
} catch (error) {
|
|
3470
|
+
this.#onEvent({
|
|
3471
|
+
type: "failure",
|
|
3472
|
+
revision: handoff.revision,
|
|
3473
|
+
reason: managedFailureReason(error, "snapshot")
|
|
3474
|
+
});
|
|
3475
|
+
await lifecycle.report("failed");
|
|
3476
|
+
return;
|
|
3477
|
+
}
|
|
3478
|
+
let threadId;
|
|
3479
|
+
try {
|
|
3480
|
+
const thread = await this.#request("thread/start", {
|
|
3481
|
+
cwd: task.workspaceRoot,
|
|
3482
|
+
runtimeWorkspaceRoots: [task.workspaceRoot],
|
|
3483
|
+
approvalPolicy: "never",
|
|
3484
|
+
permissions: MANAGED_PERMISSION_PROFILE,
|
|
3485
|
+
config: managedThreadConfig(),
|
|
3486
|
+
ephemeral: false
|
|
3487
|
+
});
|
|
3488
|
+
threadId = parseThreadStartResponse2(thread, task.workspaceRoot);
|
|
3489
|
+
try {
|
|
3490
|
+
await this.#cleanupJournal.add(threadId);
|
|
3491
|
+
} catch (error) {
|
|
3492
|
+
await this.#deleteThread(threadId).catch(() => void 0);
|
|
3493
|
+
throw new CodexAdapterError(
|
|
3494
|
+
CODEX_ADAPTER_ERROR_CODES.THREAD_CLEANUP_INCOMPLETE,
|
|
3495
|
+
error
|
|
3496
|
+
);
|
|
3497
|
+
}
|
|
3498
|
+
await this.#assertNoHooks(task.workspaceRoot);
|
|
3499
|
+
await this.#assertNoMcpServers(threadId);
|
|
3500
|
+
await this.#runTurn(handoff, task, threadId, lifecycle, signal);
|
|
3501
|
+
} catch (error) {
|
|
3502
|
+
if (signal.aborted) throw abortError4();
|
|
3503
|
+
if (error instanceof ActiveDeliveryUnknownError) throw error;
|
|
3504
|
+
this.#onEvent({
|
|
3505
|
+
type: "failure",
|
|
3506
|
+
revision: handoff.revision,
|
|
3507
|
+
reason: managedFailureReason(error, "apply")
|
|
3508
|
+
});
|
|
3509
|
+
await lifecycle.report("failed").catch(() => void 0);
|
|
3510
|
+
} finally {
|
|
3511
|
+
if (threadId !== void 0 && this.#threadCleanupAvailable()) {
|
|
3512
|
+
try {
|
|
3513
|
+
await this.#deleteThread(threadId);
|
|
3514
|
+
await this.#cleanupJournal.remove(threadId);
|
|
3515
|
+
} catch {
|
|
3516
|
+
this.#onEvent({
|
|
3517
|
+
type: "cleanup-warning",
|
|
3518
|
+
revision: handoff.revision
|
|
3519
|
+
});
|
|
3520
|
+
}
|
|
3521
|
+
}
|
|
3522
|
+
}
|
|
3523
|
+
}
|
|
3524
|
+
close() {
|
|
3525
|
+
this.#closePromise ??= this.#closeResources();
|
|
3526
|
+
return this.#closePromise;
|
|
3527
|
+
}
|
|
3528
|
+
async #readDefaultModel() {
|
|
3529
|
+
let cursor = null;
|
|
3530
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3531
|
+
let first;
|
|
3532
|
+
for (let page = 0; page < MAXIMUM_MODEL_PAGES; page += 1) {
|
|
3533
|
+
const value = parseModelPage(
|
|
3534
|
+
await this.#request("model/list", {
|
|
3535
|
+
cursor,
|
|
3536
|
+
limit: 100,
|
|
3537
|
+
includeHidden: false
|
|
3538
|
+
})
|
|
3539
|
+
);
|
|
3540
|
+
first ??= value.models[0]?.model;
|
|
3541
|
+
const selected = value.models.find((model) => model.isDefault)?.model;
|
|
3542
|
+
if (selected !== void 0) return selected;
|
|
3543
|
+
if (value.nextCursor === null || seen.has(value.nextCursor)) break;
|
|
3544
|
+
seen.add(value.nextCursor);
|
|
3545
|
+
cursor = value.nextCursor;
|
|
3546
|
+
}
|
|
3547
|
+
if (first === void 0) {
|
|
3548
|
+
throw new CodexAdapterError(CODEX_ADAPTER_ERROR_CODES.MODEL_UNAVAILABLE);
|
|
3549
|
+
}
|
|
3550
|
+
return first;
|
|
3551
|
+
}
|
|
3552
|
+
async #assertNoMcpServers(threadId) {
|
|
3553
|
+
let cursor = null;
|
|
3554
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3555
|
+
for (let page = 0; page < MAXIMUM_MCP_STATUS_PAGES2; page += 1) {
|
|
3556
|
+
const value = await this.#request("mcpServerStatus/list", {
|
|
3557
|
+
cursor,
|
|
3558
|
+
limit: 100,
|
|
3559
|
+
detail: "toolsAndAuthOnly",
|
|
3560
|
+
threadId
|
|
3561
|
+
});
|
|
3562
|
+
if (!isRecord3(value) || !Array.isArray(value.data) || value.nextCursor !== null && value.nextCursor !== void 0 && typeof value.nextCursor !== "string") {
|
|
3563
|
+
throw new CodexAdapterError(CODEX_ADAPTER_ERROR_CODES.PROTOCOL);
|
|
3564
|
+
}
|
|
3565
|
+
if (value.data.length > 0) {
|
|
3566
|
+
throw new CodexAdapterError(
|
|
3567
|
+
CODEX_ADAPTER_ERROR_CODES.CONFIG_ISOLATION_UNSUPPORTED
|
|
3568
|
+
);
|
|
3569
|
+
}
|
|
3570
|
+
const nextCursor = value.nextCursor ?? null;
|
|
3571
|
+
if (nextCursor === null || seen.has(nextCursor)) return;
|
|
3572
|
+
seen.add(nextCursor);
|
|
3573
|
+
cursor = nextCursor;
|
|
3574
|
+
}
|
|
3575
|
+
throw new CodexAdapterError(CODEX_ADAPTER_ERROR_CODES.PROTOCOL);
|
|
3576
|
+
}
|
|
3577
|
+
async #assertNoHooks(workspaceRoot) {
|
|
3578
|
+
const value = await this.#request("hooks/list", { cwds: [workspaceRoot] });
|
|
3579
|
+
if (!isRecord3(value) || !Array.isArray(value.data) || value.data.length !== 1) {
|
|
3580
|
+
throw new CodexAdapterError(CODEX_ADAPTER_ERROR_CODES.PROTOCOL);
|
|
3581
|
+
}
|
|
3582
|
+
const [entry] = value.data;
|
|
3583
|
+
if (!isRecord3(entry) || entry.cwd !== workspaceRoot || !Array.isArray(entry.hooks) || !Array.isArray(entry.warnings) || !Array.isArray(entry.errors)) {
|
|
3584
|
+
throw new CodexAdapterError(CODEX_ADAPTER_ERROR_CODES.PROTOCOL);
|
|
3585
|
+
}
|
|
3586
|
+
if (entry.hooks.length > 0 || entry.warnings.length > 0 || entry.errors.length > 0) {
|
|
3587
|
+
throw new CodexAdapterError(
|
|
3588
|
+
CODEX_ADAPTER_ERROR_CODES.CONFIG_ISOLATION_UNSUPPORTED
|
|
3589
|
+
);
|
|
3590
|
+
}
|
|
3591
|
+
}
|
|
3592
|
+
async #recoverThreadCleanup() {
|
|
3593
|
+
try {
|
|
3594
|
+
for (const entry of await this.#cleanupJournal.list()) {
|
|
3595
|
+
await this.#deleteThread(entry.threadId);
|
|
3596
|
+
await this.#cleanupJournal.remove(entry.threadId);
|
|
3597
|
+
}
|
|
3598
|
+
} catch (error) {
|
|
3599
|
+
throw new CodexAdapterError(
|
|
3600
|
+
CODEX_ADAPTER_ERROR_CODES.THREAD_CLEANUP_INCOMPLETE,
|
|
3601
|
+
error
|
|
3602
|
+
);
|
|
3603
|
+
}
|
|
3604
|
+
}
|
|
3605
|
+
async #runTurn(handoff, task, threadId, lifecycle, signal) {
|
|
3606
|
+
let resolveTerminal;
|
|
3607
|
+
let rejectTerminal;
|
|
3608
|
+
const terminal = new Promise((resolve, reject) => {
|
|
3609
|
+
resolveTerminal = resolve;
|
|
3610
|
+
rejectTerminal = reject;
|
|
3611
|
+
});
|
|
3612
|
+
const active = {
|
|
3613
|
+
events: [],
|
|
3614
|
+
handoff,
|
|
3615
|
+
lifecycle,
|
|
3616
|
+
reject: (error) => rejectTerminal?.(error),
|
|
3617
|
+
resolve: () => resolveTerminal?.(),
|
|
3618
|
+
signal,
|
|
3619
|
+
task,
|
|
3620
|
+
terminal,
|
|
3621
|
+
threadId,
|
|
3622
|
+
finalized: false,
|
|
3623
|
+
processing: Promise.resolve(),
|
|
3624
|
+
started: false,
|
|
3625
|
+
timeout: void 0,
|
|
3626
|
+
terminalEvent: void 0,
|
|
3627
|
+
turnId: void 0,
|
|
3628
|
+
written: false
|
|
3629
|
+
};
|
|
3630
|
+
this.#active = active;
|
|
3631
|
+
const abort = () => {
|
|
3632
|
+
void this.#interruptAndFail(active, threadId);
|
|
3633
|
+
};
|
|
3634
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
3635
|
+
try {
|
|
3636
|
+
const value = await this.#client.request(
|
|
3637
|
+
"turn/start",
|
|
3638
|
+
{
|
|
3639
|
+
threadId,
|
|
3640
|
+
input: [{ type: "text", text: task.prompt, text_elements: [] }],
|
|
3641
|
+
cwd: task.workspaceRoot,
|
|
3642
|
+
approvalPolicy: "never"
|
|
3643
|
+
},
|
|
3644
|
+
() => {
|
|
3645
|
+
active.written = true;
|
|
3646
|
+
}
|
|
3647
|
+
);
|
|
3648
|
+
if (!isRecord3(value) || !hasOnlyKeys2(value, ["turn"])) {
|
|
3649
|
+
throw new CodexAdapterError(CODEX_ADAPTER_ERROR_CODES.PROTOCOL);
|
|
3650
|
+
}
|
|
3651
|
+
const turn = parseTurn2(value.turn);
|
|
3652
|
+
if (turn.status !== "inProgress") {
|
|
3653
|
+
throw new CodexAdapterError(CODEX_ADAPTER_ERROR_CODES.PROTOCOL);
|
|
3654
|
+
}
|
|
3655
|
+
active.turnId = turn.id;
|
|
3656
|
+
await lifecycle.report("dispatched");
|
|
3657
|
+
active.timeout = setTimeout(() => {
|
|
3658
|
+
void this.#finishUnknown(active);
|
|
3659
|
+
}, this.#terminalTimeoutMs);
|
|
3660
|
+
active.timeout.unref();
|
|
3661
|
+
this.#schedule(active);
|
|
3662
|
+
await terminal;
|
|
3663
|
+
} catch (error) {
|
|
3664
|
+
const explicitlyRejected = error instanceof CodexAdapterError && error.code === CODEX_ADAPTER_ERROR_CODES.REQUEST_FAILED;
|
|
3665
|
+
if (!active.finalized) {
|
|
3666
|
+
if (explicitlyRejected) {
|
|
3667
|
+
this.#onEvent({
|
|
3668
|
+
type: "failure",
|
|
3669
|
+
revision: handoff.revision,
|
|
3670
|
+
reason: "protocol"
|
|
3671
|
+
});
|
|
3672
|
+
await this.#finishFailed(active);
|
|
3673
|
+
} else if (active.written) {
|
|
3674
|
+
await this.#finishUnknown(active);
|
|
3675
|
+
} else {
|
|
3676
|
+
await this.#finishFailed(active);
|
|
3677
|
+
}
|
|
3678
|
+
}
|
|
3679
|
+
await terminal;
|
|
3680
|
+
if (error instanceof ActiveDeliveryUnknownError) throw error;
|
|
3681
|
+
} finally {
|
|
3682
|
+
signal.removeEventListener("abort", abort);
|
|
3683
|
+
this.#cleanup(active);
|
|
3684
|
+
}
|
|
3685
|
+
}
|
|
3686
|
+
#handleNotification(method, params) {
|
|
3687
|
+
let event;
|
|
3688
|
+
try {
|
|
3689
|
+
event = parseTurnEvent2(method, params);
|
|
3690
|
+
} catch {
|
|
3691
|
+
const active2 = this.#active;
|
|
3692
|
+
if (active2 !== void 0) void this.#finishUnknown(active2);
|
|
3693
|
+
this.#client.close();
|
|
3694
|
+
return;
|
|
3695
|
+
}
|
|
3696
|
+
if (event === void 0) return;
|
|
3697
|
+
const active = this.#active;
|
|
3698
|
+
if (active === void 0 || active.finalized || event.threadId !== active.threadId) {
|
|
3699
|
+
return;
|
|
3700
|
+
}
|
|
3701
|
+
active.events.push(event);
|
|
3702
|
+
this.#schedule(active);
|
|
3703
|
+
}
|
|
3704
|
+
#schedule(active) {
|
|
3705
|
+
if (active.turnId === void 0 || active.finalized) return;
|
|
3706
|
+
active.processing = active.processing.then(async () => {
|
|
3707
|
+
while (active.events.length > 0 && !active.finalized) {
|
|
3708
|
+
const event = active.events.shift();
|
|
3709
|
+
if (event?.threadId !== active.threadId || event.turnId !== active.turnId) {
|
|
3710
|
+
continue;
|
|
3711
|
+
}
|
|
3712
|
+
if (event.kind === "started") {
|
|
3713
|
+
if (!active.started) {
|
|
3714
|
+
active.started = true;
|
|
3715
|
+
this.#onEvent({
|
|
3716
|
+
type: "phase",
|
|
3717
|
+
revision: active.handoff.revision,
|
|
3718
|
+
phase: "running"
|
|
3719
|
+
});
|
|
3720
|
+
await active.lifecycle.report("working");
|
|
3721
|
+
}
|
|
3722
|
+
const terminalEvent = active.terminalEvent;
|
|
3723
|
+
active.terminalEvent = void 0;
|
|
3724
|
+
if (terminalEvent !== void 0) {
|
|
3725
|
+
if (terminalEvent.status === "completed") {
|
|
3726
|
+
await this.#finishCompleted(active);
|
|
3727
|
+
} else {
|
|
3728
|
+
await this.#finishFailed(active);
|
|
3729
|
+
}
|
|
3730
|
+
}
|
|
3731
|
+
continue;
|
|
3732
|
+
}
|
|
3733
|
+
if (!active.started) {
|
|
3734
|
+
active.terminalEvent = event;
|
|
3735
|
+
continue;
|
|
3736
|
+
}
|
|
3737
|
+
if (event.status === "completed") await this.#finishCompleted(active);
|
|
3738
|
+
else await this.#finishFailed(active);
|
|
3739
|
+
}
|
|
3740
|
+
}).catch(async () => this.#finishUnknown(active));
|
|
3741
|
+
}
|
|
3742
|
+
async #finishCompleted(active) {
|
|
3743
|
+
if (active.finalized) return;
|
|
3744
|
+
active.finalized = true;
|
|
3745
|
+
try {
|
|
3746
|
+
this.#onEvent({
|
|
3747
|
+
type: "phase",
|
|
3748
|
+
revision: active.handoff.revision,
|
|
3749
|
+
phase: "auditing"
|
|
3750
|
+
});
|
|
3751
|
+
const result = await this.#execution.auditAndApply(
|
|
3752
|
+
active.task,
|
|
3753
|
+
active.signal,
|
|
3754
|
+
(phase) => {
|
|
3755
|
+
this.#onEvent({
|
|
3756
|
+
type: "phase",
|
|
3757
|
+
revision: active.handoff.revision,
|
|
3758
|
+
phase
|
|
3759
|
+
});
|
|
3760
|
+
}
|
|
3761
|
+
);
|
|
3762
|
+
this.#onEvent({ type: "result", result });
|
|
3763
|
+
await active.lifecycle.report("completed");
|
|
3764
|
+
active.resolve();
|
|
3765
|
+
} catch (error) {
|
|
3766
|
+
this.#onEvent({
|
|
3767
|
+
type: "failure",
|
|
3768
|
+
revision: active.handoff.revision,
|
|
3769
|
+
reason: managedFailureReason(error, "apply")
|
|
3770
|
+
});
|
|
3771
|
+
active.finalized = false;
|
|
3772
|
+
await this.#finishFailed(active);
|
|
3773
|
+
}
|
|
3774
|
+
}
|
|
3775
|
+
async #finishFailed(active) {
|
|
3776
|
+
if (active.finalized) return;
|
|
3777
|
+
active.finalized = true;
|
|
3778
|
+
try {
|
|
3779
|
+
await active.lifecycle.report("failed");
|
|
3780
|
+
} finally {
|
|
3781
|
+
active.resolve();
|
|
3782
|
+
}
|
|
3783
|
+
}
|
|
3784
|
+
async #finishUnknown(active) {
|
|
3785
|
+
if (active.finalized) return;
|
|
3786
|
+
active.finalized = true;
|
|
3787
|
+
await active.lifecycle.report("delivery-unknown").catch(() => void 0);
|
|
3788
|
+
active.reject(new ActiveDeliveryUnknownError());
|
|
3789
|
+
}
|
|
3790
|
+
async #interruptAndFail(active, threadId) {
|
|
3791
|
+
if (active.turnId !== void 0 && !this.#closed) {
|
|
3792
|
+
await this.#client.request("turn/interrupt", { threadId, turnId: active.turnId }).catch(() => void 0);
|
|
3793
|
+
}
|
|
3794
|
+
await this.#finishFailed(active);
|
|
3795
|
+
}
|
|
3796
|
+
async #deleteThread(threadId) {
|
|
3797
|
+
const value = await this.#request("thread/delete", { threadId });
|
|
3798
|
+
if (!isRecord3(value) || Object.keys(value).length !== 0) {
|
|
3799
|
+
throw new CodexAdapterError(CODEX_ADAPTER_ERROR_CODES.PROTOCOL);
|
|
3800
|
+
}
|
|
3801
|
+
}
|
|
3802
|
+
async #request(method, params) {
|
|
3803
|
+
this.#throwIfFatal();
|
|
3804
|
+
const value = await this.#client.request(method, params).catch((error) => {
|
|
3805
|
+
this.#throwIfFatal();
|
|
3806
|
+
throw error;
|
|
3807
|
+
});
|
|
3808
|
+
this.#throwIfFatal();
|
|
3809
|
+
return value;
|
|
3810
|
+
}
|
|
3811
|
+
#throwIfFatal() {
|
|
3812
|
+
if (this.#fatalError !== void 0) throw this.#fatalError;
|
|
3813
|
+
}
|
|
3814
|
+
#threadCleanupAvailable() {
|
|
3815
|
+
return !this.#closed;
|
|
3816
|
+
}
|
|
3817
|
+
#handleFatal(error) {
|
|
3818
|
+
this.#fatalError = error;
|
|
3819
|
+
this.#closed = true;
|
|
3820
|
+
if (this.#active !== void 0) void this.#finishUnknown(this.#active);
|
|
3821
|
+
}
|
|
3822
|
+
#cleanup(active) {
|
|
3823
|
+
if (active.timeout !== void 0) clearTimeout(active.timeout);
|
|
3824
|
+
if (this.#active === active) this.#active = void 0;
|
|
3825
|
+
}
|
|
3826
|
+
async #closeResources() {
|
|
3827
|
+
this.#closed = true;
|
|
3828
|
+
if (this.#active !== void 0) await this.#finishUnknown(this.#active);
|
|
3829
|
+
this.#client.close();
|
|
3830
|
+
signalProcessTree2(this.#child, "SIGTERM");
|
|
3831
|
+
await new Promise((resolve) => {
|
|
3832
|
+
let settled = false;
|
|
3833
|
+
const finish = () => {
|
|
3834
|
+
if (settled) return;
|
|
3835
|
+
settled = true;
|
|
3836
|
+
clearTimeout(timeout);
|
|
3837
|
+
this.#child.removeListener("exit", finish);
|
|
3838
|
+
signalProcessTree2(this.#child, "SIGKILL");
|
|
3839
|
+
resolve();
|
|
3840
|
+
};
|
|
3841
|
+
const timeout = setTimeout(finish, this.#processShutdownTimeoutMs);
|
|
3842
|
+
timeout.unref();
|
|
3843
|
+
this.#child.once("exit", finish);
|
|
3844
|
+
if (this.#child.exitCode !== null || this.#child.signalCode !== null) finish();
|
|
3845
|
+
});
|
|
3846
|
+
}
|
|
3847
|
+
};
|
|
3848
|
+
async function connectManagedCodexAppServer(options) {
|
|
3849
|
+
return ManagedCodexAppServerAdapter.connect(options);
|
|
3850
|
+
}
|
|
3851
|
+
|
|
3852
|
+
// src/cli-runner.ts
|
|
3853
|
+
function optionValue(arguments_, name) {
|
|
3854
|
+
const index = arguments_.indexOf(name);
|
|
3855
|
+
if (index === -1) return void 0;
|
|
3856
|
+
const value = arguments_[index + 1];
|
|
3857
|
+
if (value === void 0 || value.startsWith("--")) {
|
|
3858
|
+
throw new SpotPatchError11(ERROR_CODES11.INVALID_REQUEST);
|
|
3859
|
+
}
|
|
3860
|
+
return value;
|
|
3861
|
+
}
|
|
3862
|
+
function allowedArguments(arguments_, booleanOptions, valueOptions) {
|
|
3863
|
+
const allowed = /* @__PURE__ */ new Set([...booleanOptions, ...valueOptions]);
|
|
3864
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3865
|
+
for (let index = 0; index < arguments_.length; index += 1) {
|
|
3866
|
+
const argument = arguments_[index];
|
|
3867
|
+
if (argument === void 0 || !allowed.has(argument) || seen.has(argument)) {
|
|
3868
|
+
throw new SpotPatchError11(ERROR_CODES11.INVALID_REQUEST);
|
|
3869
|
+
}
|
|
3870
|
+
seen.add(argument);
|
|
3871
|
+
if (valueOptions.includes(argument)) {
|
|
3872
|
+
const value = arguments_[index + 1];
|
|
3873
|
+
if (value === void 0 || value.startsWith("--")) {
|
|
3874
|
+
throw new SpotPatchError11(ERROR_CODES11.INVALID_REQUEST);
|
|
3875
|
+
}
|
|
3876
|
+
index += 1;
|
|
3877
|
+
}
|
|
3878
|
+
}
|
|
3879
|
+
}
|
|
3880
|
+
function exitCode(error) {
|
|
3881
|
+
if (error instanceof CodexAdapterError) {
|
|
3882
|
+
if (error.code === CODEX_ADAPTER_ERROR_CODES.EXECUTABLE_UNTRUSTED) return 7;
|
|
3883
|
+
if (error.code === CODEX_ADAPTER_ERROR_CODES.MCP_NOT_READY || error.code === CODEX_ADAPTER_ERROR_CODES.PROTOCOL || error.code === CODEX_ADAPTER_ERROR_CODES.UNSUPPORTED_VERSION) {
|
|
3884
|
+
return 6;
|
|
3885
|
+
}
|
|
3886
|
+
if (error.code === CODEX_ADAPTER_ERROR_CODES.BUSY || error.code === CODEX_ADAPTER_ERROR_CODES.CLOSED || error.code === CODEX_ADAPTER_ERROR_CODES.EXECUTABLE_NOT_FOUND || error.code === CODEX_ADAPTER_ERROR_CODES.PROCESS_EXITED || error.code === CODEX_ADAPTER_ERROR_CODES.REQUEST_TIMEOUT) {
|
|
3887
|
+
return 8;
|
|
3888
|
+
}
|
|
3889
|
+
return 1;
|
|
3890
|
+
}
|
|
3891
|
+
if (!(error instanceof SpotPatchError11)) return 1;
|
|
3892
|
+
if (error.code === ERROR_CODES11.INVALID_REQUEST) return 2;
|
|
3893
|
+
if (error.code === ERROR_CODES11.SESSION_NOT_FOUND) return 3;
|
|
3894
|
+
if (error.code === ERROR_CODES11.HANDOFF_NOT_FOUND || error.code === ERROR_CODES11.HANDOFF_EXPIRED) {
|
|
3895
|
+
return 4;
|
|
3896
|
+
}
|
|
3897
|
+
if (error.code === ERROR_CODES11.SESSION_AMBIGUOUS) return 5;
|
|
3898
|
+
if (error.code === ERROR_CODES11.BRIDGE_PROTOCOL_MISMATCH) return 6;
|
|
3899
|
+
if (error.code === ERROR_CODES11.BRIDGE_UNAUTHORIZED) return 7;
|
|
3900
|
+
if (error.code === ERROR_CODES11.SESSION_CLOSED || error.code === ERROR_CODES11.EXTERNAL_HANDOFF_UNAVAILABLE) {
|
|
3901
|
+
return 8;
|
|
3902
|
+
}
|
|
3903
|
+
return 1;
|
|
3904
|
+
}
|
|
3905
|
+
function writeJson(stdout, command, data) {
|
|
3906
|
+
stdout.write(`${JSON.stringify({ schemaVersion: 1, command, data })}
|
|
3907
|
+
`);
|
|
3908
|
+
}
|
|
3909
|
+
function usage(stderr) {
|
|
3910
|
+
stderr.write(
|
|
3911
|
+
"Usage: spotpatch-bridge <sessions|current|wait|ack|mcp|channel|connect|setup> [options]\n"
|
|
3912
|
+
);
|
|
3913
|
+
}
|
|
3914
|
+
function abortError5() {
|
|
3915
|
+
const error = new Error("The SpotPatch command was interrupted.");
|
|
3916
|
+
error.name = "AbortError";
|
|
3917
|
+
return error;
|
|
3918
|
+
}
|
|
3919
|
+
function abortableOperation(operation, signal) {
|
|
3920
|
+
if (signal.aborted) return Promise.reject(abortError5());
|
|
3921
|
+
return new Promise((resolve, reject) => {
|
|
3922
|
+
const finish = () => {
|
|
3923
|
+
signal.removeEventListener("abort", abort);
|
|
3924
|
+
};
|
|
3925
|
+
const abort = () => {
|
|
3926
|
+
finish();
|
|
3927
|
+
reject(abortError5());
|
|
3928
|
+
};
|
|
3929
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
3930
|
+
void operation.then(
|
|
3931
|
+
(value) => {
|
|
3932
|
+
finish();
|
|
3933
|
+
resolve(value);
|
|
3934
|
+
},
|
|
3935
|
+
(error) => {
|
|
3936
|
+
finish();
|
|
3937
|
+
reject(
|
|
3938
|
+
error instanceof Error ? error : new Error("The SpotPatch operation failed.", { cause: error })
|
|
3939
|
+
);
|
|
3940
|
+
}
|
|
3941
|
+
);
|
|
3942
|
+
});
|
|
3943
|
+
}
|
|
3944
|
+
function processSignalScope(onInterrupt) {
|
|
3945
|
+
let interrupted = false;
|
|
3946
|
+
const interrupt = () => {
|
|
3947
|
+
if (interrupted) return;
|
|
3948
|
+
interrupted = true;
|
|
3949
|
+
onInterrupt();
|
|
3950
|
+
};
|
|
3951
|
+
process.once("SIGINT", interrupt);
|
|
3952
|
+
process.once("SIGTERM", interrupt);
|
|
3953
|
+
return Object.freeze({
|
|
3954
|
+
interrupted: () => interrupted,
|
|
3955
|
+
remove() {
|
|
3956
|
+
process.removeListener("SIGINT", interrupt);
|
|
3957
|
+
process.removeListener("SIGTERM", interrupt);
|
|
3958
|
+
}
|
|
3959
|
+
});
|
|
3960
|
+
}
|
|
3961
|
+
async function runClaudeChannel(cwd, sessionId) {
|
|
3962
|
+
let close;
|
|
3963
|
+
const signals = processSignalScope(() => {
|
|
3964
|
+
void close?.();
|
|
3965
|
+
});
|
|
3966
|
+
try {
|
|
3967
|
+
const exactSessionId = await resolveExactProjectSessionId(cwd, sessionId);
|
|
3968
|
+
const handle = await serveClaudeChannelMcp({ cwd, sessionId: exactSessionId });
|
|
3969
|
+
close = handle.close;
|
|
3970
|
+
if (signals.interrupted()) await handle.close();
|
|
3971
|
+
await handle.done;
|
|
3972
|
+
return signals.interrupted() ? 130 : 0;
|
|
3973
|
+
} finally {
|
|
3974
|
+
signals.remove();
|
|
3975
|
+
await close?.().catch(() => void 0);
|
|
3976
|
+
}
|
|
3977
|
+
}
|
|
3978
|
+
function writeCodexConnectorEvent(event, stderr) {
|
|
3979
|
+
if (event.type === "ready") {
|
|
3980
|
+
stderr.write(
|
|
3981
|
+
"[spotpatch:bridge] Codex connected and ready for SpotPatch requests.\n"
|
|
3982
|
+
);
|
|
3983
|
+
return;
|
|
3984
|
+
}
|
|
3985
|
+
const revision = String(event.revision);
|
|
3986
|
+
switch (event.phase) {
|
|
3987
|
+
case "dispatching":
|
|
3988
|
+
stderr.write(
|
|
3989
|
+
`[spotpatch:bridge] SpotPatch is preparing revision ${revision} for Codex.
|
|
3990
|
+
`
|
|
3991
|
+
);
|
|
3992
|
+
return;
|
|
3993
|
+
case "working":
|
|
3994
|
+
stderr.write(`[spotpatch:bridge] Codex started revision ${revision}.
|
|
3995
|
+
`);
|
|
3996
|
+
return;
|
|
3997
|
+
case "completed":
|
|
3998
|
+
stderr.write(
|
|
3999
|
+
`[spotpatch:bridge] Codex turn ended for revision ${revision}; review the workspace. Ready for the next request.
|
|
4000
|
+
`
|
|
4001
|
+
);
|
|
4002
|
+
return;
|
|
4003
|
+
case "failed":
|
|
4004
|
+
stderr.write(
|
|
4005
|
+
`[spotpatch:bridge] Revision ${revision} failed before a verified Codex turn completed; review the connector output and workspace. Ready for the next request.
|
|
4006
|
+
`
|
|
4007
|
+
);
|
|
4008
|
+
return;
|
|
4009
|
+
case "delivery-unknown":
|
|
4010
|
+
stderr.write(
|
|
4011
|
+
`[spotpatch:bridge] Codex delivery for revision ${revision} is unknown; the connector will stop.
|
|
4012
|
+
`
|
|
4013
|
+
);
|
|
4014
|
+
return;
|
|
4015
|
+
case "dispatched":
|
|
4016
|
+
stderr.write(`[spotpatch:bridge] Codex accepted revision ${revision}.
|
|
4017
|
+
`);
|
|
4018
|
+
return;
|
|
4019
|
+
}
|
|
4020
|
+
}
|
|
4021
|
+
async function runCodexConnector(adapterKind, cwd, stderr, sessionId) {
|
|
4022
|
+
const pumpController = new AbortController();
|
|
4023
|
+
const startupController = new AbortController();
|
|
3019
4024
|
let fatalError;
|
|
3020
4025
|
const signals = processSignalScope(() => {
|
|
3021
4026
|
startupController.abort("cli-interrupted");
|
|
@@ -3082,7 +4087,7 @@ async function runSpotPatchBridgeCli(arguments_, options = {}) {
|
|
|
3082
4087
|
}
|
|
3083
4088
|
if (command === "channel") {
|
|
3084
4089
|
if (rest[0] !== "claude") {
|
|
3085
|
-
throw new
|
|
4090
|
+
throw new SpotPatchError11(ERROR_CODES11.INVALID_REQUEST);
|
|
3086
4091
|
}
|
|
3087
4092
|
const channelArguments = rest.slice(1);
|
|
3088
4093
|
allowedArguments(channelArguments, [], ["--session"]);
|
|
@@ -3090,12 +4095,12 @@ async function runSpotPatchBridgeCli(arguments_, options = {}) {
|
|
|
3090
4095
|
}
|
|
3091
4096
|
if (command === "connect") {
|
|
3092
4097
|
if (rest[0] !== "codex") {
|
|
3093
|
-
throw new
|
|
4098
|
+
throw new SpotPatchError11(ERROR_CODES11.INVALID_REQUEST);
|
|
3094
4099
|
}
|
|
3095
4100
|
const connectorArguments2 = rest.slice(1);
|
|
3096
4101
|
allowedArguments(connectorArguments2, ["--allow-workspace-write"], ["--session"]);
|
|
3097
4102
|
if (!connectorArguments2.includes("--allow-workspace-write")) {
|
|
3098
|
-
throw new
|
|
4103
|
+
throw new SpotPatchError11(ERROR_CODES11.INVALID_REQUEST);
|
|
3099
4104
|
}
|
|
3100
4105
|
return await runCodexConnector(
|
|
3101
4106
|
adapter,
|
|
@@ -3110,12 +4115,12 @@ async function runSpotPatchBridgeCli(arguments_, options = {}) {
|
|
|
3110
4115
|
const mode = optionValue(rest, "--mode") ?? "inbox";
|
|
3111
4116
|
const scope = optionValue(rest, "--scope") ?? "project";
|
|
3112
4117
|
if (scope !== "project" || mode !== "inbox" && mode !== "active" || mode === "active" && client2 !== "claude" || client2 !== "claude" && client2 !== "cursor" && client2 !== "codex") {
|
|
3113
|
-
throw new
|
|
4118
|
+
throw new SpotPatchError11(ERROR_CODES11.INVALID_REQUEST);
|
|
3114
4119
|
}
|
|
3115
4120
|
const plan = createBridgeSetupPlan(client2, adapter, cwd, mode);
|
|
3116
4121
|
const write = rest.includes("--write");
|
|
3117
4122
|
const result = write ? await applyBridgeSetupPlan(plan) : "dry-run";
|
|
3118
|
-
const displayPath =
|
|
4123
|
+
const displayPath = path9.relative(cwd, plan.path).split(path9.sep).join("/");
|
|
3119
4124
|
const backup = result === "updated" ? `Backup: ${displayPath}.spotpatch.bak
|
|
3120
4125
|
` : "";
|
|
3121
4126
|
stdout.write(
|
|
@@ -3156,8 +4161,8 @@ ${backup}${plan.content}`
|
|
|
3156
4161
|
allowedArguments(rest, ["--json"], ["--session", "--after", "--timeout"]);
|
|
3157
4162
|
const timeoutText = optionValue(rest, "--timeout");
|
|
3158
4163
|
const timeout = timeoutText === void 0 ? void 0 : Number(timeoutText);
|
|
3159
|
-
if (timeout !== void 0 && (!Number.isSafeInteger(timeout) || timeout <= 0 || timeout >
|
|
3160
|
-
throw new
|
|
4164
|
+
if (timeout !== void 0 && (!Number.isSafeInteger(timeout) || timeout <= 0 || timeout > EXTERNAL_HANDOFF_LIMITS10.maximumWaitMs)) {
|
|
4165
|
+
throw new SpotPatchError11(ERROR_CODES11.INVALID_REQUEST);
|
|
3161
4166
|
}
|
|
3162
4167
|
const controller = new AbortController();
|
|
3163
4168
|
const abort = () => {
|
|
@@ -3187,7 +4192,7 @@ ${backup}${plan.content}`
|
|
|
3187
4192
|
if (command === "ack") {
|
|
3188
4193
|
allowedArguments(rest, ["--json"], ["--session", "--cursor"]);
|
|
3189
4194
|
const cursor = optionValue(rest, "--cursor");
|
|
3190
|
-
if (cursor === void 0) throw new
|
|
4195
|
+
if (cursor === void 0) throw new SpotPatchError11(ERROR_CODES11.INVALID_REQUEST);
|
|
3191
4196
|
const summary = await client.ack(cursor, optionValue(rest, "--session"));
|
|
3192
4197
|
const result = { outcome: "acknowledged", summary };
|
|
3193
4198
|
if (json) writeJson(stdout, command, result);
|
|
@@ -3201,10 +4206,10 @@ ${backup}${plan.content}`
|
|
|
3201
4206
|
usage(stderr);
|
|
3202
4207
|
return 2;
|
|
3203
4208
|
} catch (error) {
|
|
3204
|
-
const code = error instanceof
|
|
4209
|
+
const code = error instanceof SpotPatchError11 || error instanceof CodexAdapterError ? error.code : ERROR_CODES11.INTERNAL_ERROR;
|
|
3205
4210
|
stderr.write(`[spotpatch:bridge] ${code}
|
|
3206
4211
|
`);
|
|
3207
|
-
if (command === "connect" && (code ===
|
|
4212
|
+
if (command === "connect" && (code === ERROR_CODES11.SESSION_NOT_FOUND || code === ERROR_CODES11.SESSION_CLOSED)) {
|
|
3208
4213
|
stderr.write(
|
|
3209
4214
|
"[spotpatch:bridge] The SpotPatch development session ended or changed. Keep the dev server running, then rerun the same connect command.\n"
|
|
3210
4215
|
);
|
|
@@ -3212,9 +4217,724 @@ ${backup}${plan.content}`
|
|
|
3212
4217
|
return exitCode(error);
|
|
3213
4218
|
}
|
|
3214
4219
|
}
|
|
4220
|
+
|
|
4221
|
+
// src/supervisor/supervisor.ts
|
|
4222
|
+
import { createInterface } from "readline/promises";
|
|
4223
|
+
import {
|
|
4224
|
+
ERROR_CODES as ERROR_CODES12,
|
|
4225
|
+
EXTERNAL_AGENT_CONTROL_SCHEMA_VERSION,
|
|
4226
|
+
EXTERNAL_AGENT_MANAGED_PROFILE as EXTERNAL_AGENT_MANAGED_PROFILE2,
|
|
4227
|
+
SpotPatchError as SpotPatchError12,
|
|
4228
|
+
externalAgentControlStatusSchema,
|
|
4229
|
+
externalAgentManagedResultSchema
|
|
4230
|
+
} from "@spotpatch/shared";
|
|
4231
|
+
import {
|
|
4232
|
+
createManagedExecutionRunner
|
|
4233
|
+
} from "@spotpatch/agent";
|
|
4234
|
+
|
|
4235
|
+
// src/supervisor/grant-store.ts
|
|
4236
|
+
import { rm as rm3 } from "fs/promises";
|
|
4237
|
+
import path10 from "path";
|
|
4238
|
+
import { EXTERNAL_AGENT_MANAGED_PROFILE } from "@spotpatch/shared";
|
|
4239
|
+
import { computeExternalHandoffProjectKey as computeExternalHandoffProjectKey2 } from "@spotpatch/shared/external-agent-node";
|
|
4240
|
+
import { z as z4 } from "zod";
|
|
4241
|
+
var GRANT_SCHEMA_VERSION = 1;
|
|
4242
|
+
var GRANT_POLICY_VERSION = 1;
|
|
4243
|
+
var grantSchema = z4.strictObject({
|
|
4244
|
+
schemaVersion: z4.literal(GRANT_SCHEMA_VERSION),
|
|
4245
|
+
projectKey: z4.string().regex(/^[a-f0-9]{64}$/u),
|
|
4246
|
+
adapterKind: z4.literal("codex"),
|
|
4247
|
+
profile: z4.literal(EXTERNAL_AGENT_MANAGED_PROFILE),
|
|
4248
|
+
policyVersion: z4.literal(GRANT_POLICY_VERSION),
|
|
4249
|
+
createdAt: z4.iso.datetime(),
|
|
4250
|
+
lastUsedAt: z4.iso.datetime()
|
|
4251
|
+
});
|
|
4252
|
+
async function readSecureGrant(filePath) {
|
|
4253
|
+
return readPrivateJson(filePath, 4096);
|
|
4254
|
+
}
|
|
4255
|
+
async function writeAtomicGrant(filePath, value) {
|
|
4256
|
+
await writePrivateJsonAtomic(filePath, "grant", value);
|
|
4257
|
+
}
|
|
4258
|
+
async function createManagedGrantStore(options) {
|
|
4259
|
+
const projectKey = await computeExternalHandoffProjectKey2(options.root);
|
|
4260
|
+
const canonicalBase = await resolvePrivateConfigBase(options.configBase);
|
|
4261
|
+
const directory = path10.join(canonicalBase, "external-agent-grants");
|
|
4262
|
+
await ensurePrivateDirectory(directory);
|
|
4263
|
+
const filePath = path10.join(directory, `${projectKey}.json`);
|
|
4264
|
+
const now = options.now ?? (() => /* @__PURE__ */ new Date());
|
|
4265
|
+
const record2 = (createdAt) => Object.freeze({
|
|
4266
|
+
schemaVersion: GRANT_SCHEMA_VERSION,
|
|
4267
|
+
projectKey,
|
|
4268
|
+
adapterKind: "codex",
|
|
4269
|
+
profile: EXTERNAL_AGENT_MANAGED_PROFILE,
|
|
4270
|
+
policyVersion: GRANT_POLICY_VERSION,
|
|
4271
|
+
createdAt,
|
|
4272
|
+
lastUsedAt: now().toISOString()
|
|
4273
|
+
});
|
|
4274
|
+
return Object.freeze({
|
|
4275
|
+
projectKey,
|
|
4276
|
+
async read() {
|
|
4277
|
+
try {
|
|
4278
|
+
const value = await readSecureGrant(filePath);
|
|
4279
|
+
if (value === void 0) return "missing";
|
|
4280
|
+
const parsed = grantSchema.safeParse(value);
|
|
4281
|
+
return parsed.success && parsed.data.projectKey === projectKey ? "valid" : "invalid";
|
|
4282
|
+
} catch {
|
|
4283
|
+
return "invalid";
|
|
4284
|
+
}
|
|
4285
|
+
},
|
|
4286
|
+
async grant() {
|
|
4287
|
+
const timestamp = now().toISOString();
|
|
4288
|
+
await writeAtomicGrant(filePath, record2(timestamp));
|
|
4289
|
+
},
|
|
4290
|
+
async touch() {
|
|
4291
|
+
const value = await readSecureGrant(filePath);
|
|
4292
|
+
const parsed = grantSchema.safeParse(value);
|
|
4293
|
+
if (!parsed.success || parsed.data.projectKey !== projectKey) {
|
|
4294
|
+
throw new Error("Managed grant is invalid.");
|
|
4295
|
+
}
|
|
4296
|
+
await writeAtomicGrant(filePath, record2(parsed.data.createdAt));
|
|
4297
|
+
},
|
|
4298
|
+
async revoke() {
|
|
4299
|
+
await rm3(filePath, { force: true });
|
|
4300
|
+
}
|
|
4301
|
+
});
|
|
4302
|
+
}
|
|
4303
|
+
|
|
4304
|
+
// src/supervisor/thread-cleanup-journal.ts
|
|
4305
|
+
import { rm as rm4 } from "fs/promises";
|
|
4306
|
+
import path11 from "path";
|
|
4307
|
+
import { computeExternalHandoffProjectKey as computeExternalHandoffProjectKey3 } from "@spotpatch/shared/external-agent-node";
|
|
4308
|
+
import { z as z5 } from "zod";
|
|
4309
|
+
var CLEANUP_JOURNAL_SCHEMA_VERSION = 1;
|
|
4310
|
+
var MAXIMUM_THREAD_RECORDS = 32;
|
|
4311
|
+
var MAXIMUM_JOURNAL_BYTES = 16 * 1024;
|
|
4312
|
+
function hasOnlyPrintableCharacters(value) {
|
|
4313
|
+
for (const character of value) {
|
|
4314
|
+
const code = character.codePointAt(0) ?? 0;
|
|
4315
|
+
if (code < 32 || code === 127) return false;
|
|
4316
|
+
}
|
|
4317
|
+
return true;
|
|
4318
|
+
}
|
|
4319
|
+
var threadIdSchema = z5.string().min(1).max(256).refine(hasOnlyPrintableCharacters);
|
|
4320
|
+
var journalSchema = z5.strictObject({
|
|
4321
|
+
schemaVersion: z5.literal(CLEANUP_JOURNAL_SCHEMA_VERSION),
|
|
4322
|
+
projectKey: z5.string().regex(/^[a-f0-9]{64}$/u),
|
|
4323
|
+
threads: z5.array(
|
|
4324
|
+
z5.strictObject({
|
|
4325
|
+
threadId: threadIdSchema,
|
|
4326
|
+
createdAt: z5.iso.datetime()
|
|
4327
|
+
})
|
|
4328
|
+
).max(MAXIMUM_THREAD_RECORDS)
|
|
4329
|
+
});
|
|
4330
|
+
async function createManagedThreadCleanupJournal(options) {
|
|
4331
|
+
const projectKey = await computeExternalHandoffProjectKey3(options.root);
|
|
4332
|
+
const canonicalBase = await resolvePrivateConfigBase(options.configBase);
|
|
4333
|
+
const directory = path11.join(canonicalBase, "external-agent-cleanup");
|
|
4334
|
+
await ensurePrivateDirectory(directory);
|
|
4335
|
+
const filePath = path11.join(directory, `${projectKey}.json`);
|
|
4336
|
+
const now = options.now ?? (() => /* @__PURE__ */ new Date());
|
|
4337
|
+
let operationTail = Promise.resolve();
|
|
4338
|
+
const readEntries = async () => {
|
|
4339
|
+
const value = await readPrivateJson(filePath, MAXIMUM_JOURNAL_BYTES);
|
|
4340
|
+
if (value === void 0) return Object.freeze([]);
|
|
4341
|
+
const parsed = journalSchema.safeParse(value);
|
|
4342
|
+
if (!parsed.success || parsed.data.projectKey !== projectKey) {
|
|
4343
|
+
throw new Error("Managed thread cleanup journal is invalid.");
|
|
4344
|
+
}
|
|
4345
|
+
return Object.freeze(
|
|
4346
|
+
parsed.data.threads.map((entry) => Object.freeze({ ...entry }))
|
|
4347
|
+
);
|
|
4348
|
+
};
|
|
4349
|
+
const writeEntries = async (entries) => {
|
|
4350
|
+
if (entries.length === 0) {
|
|
4351
|
+
await rm4(filePath, { force: true });
|
|
4352
|
+
return;
|
|
4353
|
+
}
|
|
4354
|
+
await writePrivateJsonAtomic(filePath, "cleanup", {
|
|
4355
|
+
schemaVersion: CLEANUP_JOURNAL_SCHEMA_VERSION,
|
|
4356
|
+
projectKey,
|
|
4357
|
+
threads: entries
|
|
4358
|
+
});
|
|
4359
|
+
};
|
|
4360
|
+
const serialize = (operation) => {
|
|
4361
|
+
const result = operationTail.then(operation, operation);
|
|
4362
|
+
operationTail = result.then(
|
|
4363
|
+
() => void 0,
|
|
4364
|
+
() => void 0
|
|
4365
|
+
);
|
|
4366
|
+
return result;
|
|
4367
|
+
};
|
|
4368
|
+
return Object.freeze({
|
|
4369
|
+
list: () => serialize(readEntries),
|
|
4370
|
+
add(threadId) {
|
|
4371
|
+
return serialize(async () => {
|
|
4372
|
+
const parsedThreadId = threadIdSchema.parse(threadId);
|
|
4373
|
+
const entries = await readEntries();
|
|
4374
|
+
if (entries.some((entry) => entry.threadId === parsedThreadId)) return;
|
|
4375
|
+
if (entries.length >= MAXIMUM_THREAD_RECORDS) {
|
|
4376
|
+
throw new Error("Managed thread cleanup journal is full.");
|
|
4377
|
+
}
|
|
4378
|
+
await writeEntries([
|
|
4379
|
+
...entries,
|
|
4380
|
+
Object.freeze({
|
|
4381
|
+
threadId: parsedThreadId,
|
|
4382
|
+
createdAt: now().toISOString()
|
|
4383
|
+
})
|
|
4384
|
+
]);
|
|
4385
|
+
});
|
|
4386
|
+
},
|
|
4387
|
+
remove(threadId) {
|
|
4388
|
+
return serialize(async () => {
|
|
4389
|
+
const parsedThreadId = threadIdSchema.parse(threadId);
|
|
4390
|
+
const entries = await readEntries();
|
|
4391
|
+
await writeEntries(
|
|
4392
|
+
entries.filter((entry) => entry.threadId !== parsedThreadId)
|
|
4393
|
+
);
|
|
4394
|
+
});
|
|
4395
|
+
}
|
|
4396
|
+
});
|
|
4397
|
+
}
|
|
4398
|
+
|
|
4399
|
+
// src/supervisor/supervisor.ts
|
|
4400
|
+
var MAXIMUM_IDEMPOTENCY_RECORDS = 64;
|
|
4401
|
+
var MAXIMUM_RESULT_RECORDS = 16;
|
|
4402
|
+
function managedError(code, stage, recoverability, action) {
|
|
4403
|
+
return Object.freeze({ code, stage, recoverability, action });
|
|
4404
|
+
}
|
|
4405
|
+
function classifyConnectionError(error) {
|
|
4406
|
+
if (error instanceof SpotPatchError12) {
|
|
4407
|
+
if (error.code === ERROR_CODES12.WORKTREE_NOT_REPOSITORY) {
|
|
4408
|
+
return managedError(
|
|
4409
|
+
"MANAGED_GIT_REQUIRED",
|
|
4410
|
+
"snapshot",
|
|
4411
|
+
"reconfigure",
|
|
4412
|
+
"use-inbox"
|
|
4413
|
+
);
|
|
4414
|
+
}
|
|
4415
|
+
if (error.code === ERROR_CODES12.WORKTREE_DIRTY || error.code === ERROR_CODES12.WORKTREE_CONFLICTED || error.code === ERROR_CODES12.WORKTREE_OPERATION_IN_PROGRESS) {
|
|
4416
|
+
return managedError(
|
|
4417
|
+
"MANAGED_SNAPSHOT_FAILED",
|
|
4418
|
+
"snapshot",
|
|
4419
|
+
"user-action",
|
|
4420
|
+
"review-workspace-conflict"
|
|
4421
|
+
);
|
|
4422
|
+
}
|
|
4423
|
+
}
|
|
4424
|
+
if (typeof error === "object" && error !== null && "code" in error && typeof error.code === "string") {
|
|
4425
|
+
if (error.code === "CODEX_EXECUTABLE_NOT_FOUND") {
|
|
4426
|
+
return managedError(
|
|
4427
|
+
"AGENT_BINARY_NOT_FOUND",
|
|
4428
|
+
"binary",
|
|
4429
|
+
"user-action",
|
|
4430
|
+
"install-agent"
|
|
4431
|
+
);
|
|
4432
|
+
}
|
|
4433
|
+
if (error.code === "CODEX_EXECUTABLE_UNTRUSTED") {
|
|
4434
|
+
return managedError(
|
|
4435
|
+
"AGENT_BINARY_UNTRUSTED",
|
|
4436
|
+
"binary",
|
|
4437
|
+
"reconfigure",
|
|
4438
|
+
"use-inbox"
|
|
4439
|
+
);
|
|
4440
|
+
}
|
|
4441
|
+
if (error.code === "CODEX_UNSUPPORTED_VERSION") {
|
|
4442
|
+
return managedError(
|
|
4443
|
+
"AGENT_VERSION_UNSUPPORTED",
|
|
4444
|
+
"protocol",
|
|
4445
|
+
"user-action",
|
|
4446
|
+
"use-supported-version"
|
|
4447
|
+
);
|
|
4448
|
+
}
|
|
4449
|
+
if (error.code === "CODEX_AUTH_REQUIRED") {
|
|
4450
|
+
return managedError("AGENT_AUTH_REQUIRED", "auth", "user-action", "sign-in");
|
|
4451
|
+
}
|
|
4452
|
+
if (error.code === "CODEX_MODEL_UNAVAILABLE") {
|
|
4453
|
+
return managedError(
|
|
4454
|
+
"AGENT_MODEL_UNAVAILABLE",
|
|
4455
|
+
"model",
|
|
4456
|
+
"user-action",
|
|
4457
|
+
"choose-available-model"
|
|
4458
|
+
);
|
|
4459
|
+
}
|
|
4460
|
+
if (error.code === "CODEX_CONFIG_ISOLATION_UNSUPPORTED") {
|
|
4461
|
+
return managedError(
|
|
4462
|
+
"CODEX_CONFIG_ISOLATION_UNSUPPORTED",
|
|
4463
|
+
"protocol",
|
|
4464
|
+
"reconfigure",
|
|
4465
|
+
"use-inbox"
|
|
4466
|
+
);
|
|
4467
|
+
}
|
|
4468
|
+
if (error.code === "CODEX_APP_SERVER_PROTOCOL_ERROR") {
|
|
4469
|
+
return managedError(
|
|
4470
|
+
"AGENT_PROTOCOL_INCOMPATIBLE",
|
|
4471
|
+
"protocol",
|
|
4472
|
+
"reconfigure",
|
|
4473
|
+
"use-supported-version"
|
|
4474
|
+
);
|
|
4475
|
+
}
|
|
4476
|
+
if (error.code === "CODEX_THREAD_CLEANUP_INCOMPLETE") {
|
|
4477
|
+
return managedError(
|
|
4478
|
+
"MANAGED_CLEANUP_INCOMPLETE",
|
|
4479
|
+
"cleanup",
|
|
4480
|
+
"user-action",
|
|
4481
|
+
"inspect-cleanup-warning"
|
|
4482
|
+
);
|
|
4483
|
+
}
|
|
4484
|
+
}
|
|
4485
|
+
return managedError("APP_SERVER_HANDSHAKE_FAILED", "handshake", "retry", "retry");
|
|
4486
|
+
}
|
|
4487
|
+
function managedExecutionError(reason) {
|
|
4488
|
+
switch (reason) {
|
|
4489
|
+
case "config-isolation":
|
|
4490
|
+
return managedError(
|
|
4491
|
+
"CODEX_CONFIG_ISOLATION_UNSUPPORTED",
|
|
4492
|
+
"protocol",
|
|
4493
|
+
"reconfigure",
|
|
4494
|
+
"use-inbox"
|
|
4495
|
+
);
|
|
4496
|
+
case "protocol":
|
|
4497
|
+
return managedError(
|
|
4498
|
+
"AGENT_PROTOCOL_INCOMPATIBLE",
|
|
4499
|
+
"protocol",
|
|
4500
|
+
"reconfigure",
|
|
4501
|
+
"use-supported-version"
|
|
4502
|
+
);
|
|
4503
|
+
case "snapshot":
|
|
4504
|
+
return managedError(
|
|
4505
|
+
"MANAGED_SNAPSHOT_FAILED",
|
|
4506
|
+
"snapshot",
|
|
4507
|
+
"user-action",
|
|
4508
|
+
"review-workspace-conflict"
|
|
4509
|
+
);
|
|
4510
|
+
case "scope":
|
|
4511
|
+
return managedError(
|
|
4512
|
+
"MANAGED_SCOPE_VIOLATION",
|
|
4513
|
+
"audit",
|
|
4514
|
+
"reconfigure",
|
|
4515
|
+
"use-inbox"
|
|
4516
|
+
);
|
|
4517
|
+
case "change-limit":
|
|
4518
|
+
return managedError(
|
|
4519
|
+
"MANAGED_CHANGE_LIMIT_EXCEEDED",
|
|
4520
|
+
"audit",
|
|
4521
|
+
"reconfigure",
|
|
4522
|
+
"use-inbox"
|
|
4523
|
+
);
|
|
4524
|
+
case "validation":
|
|
4525
|
+
return managedError(
|
|
4526
|
+
"MANAGED_VALIDATION_FAILED",
|
|
4527
|
+
"validation",
|
|
4528
|
+
"user-action",
|
|
4529
|
+
"review-candidate-diff"
|
|
4530
|
+
);
|
|
4531
|
+
case "workspace-conflict":
|
|
4532
|
+
return managedError(
|
|
4533
|
+
"MANAGED_WORKSPACE_CONFLICT",
|
|
4534
|
+
"apply",
|
|
4535
|
+
"user-action",
|
|
4536
|
+
"review-workspace-conflict"
|
|
4537
|
+
);
|
|
4538
|
+
case "apply":
|
|
4539
|
+
return managedError(
|
|
4540
|
+
"MANAGED_APPLY_FAILED",
|
|
4541
|
+
"apply",
|
|
4542
|
+
"user-action",
|
|
4543
|
+
"review-candidate-diff"
|
|
4544
|
+
);
|
|
4545
|
+
}
|
|
4546
|
+
}
|
|
4547
|
+
async function defaultTerminalConfirmation(projectLabel) {
|
|
4548
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) return false;
|
|
4549
|
+
const reader = createInterface({ input: process.stdin, output: process.stdout });
|
|
4550
|
+
try {
|
|
4551
|
+
process.stdout.write(
|
|
4552
|
+
[
|
|
4553
|
+
"\nSpotPatch managed Agent access request",
|
|
4554
|
+
`Project: ${projectLabel}`,
|
|
4555
|
+
"Adapter: Codex",
|
|
4556
|
+
`Profile: ${EXTERNAL_AGENT_MANAGED_PROFILE2}`,
|
|
4557
|
+
"Codex may write only an independent temporary snapshot. SpotPatch audits, validates, and applies eligible changes.",
|
|
4558
|
+
"You can revoke this grant from the SpotPatch panel."
|
|
4559
|
+
].join("\n") + "\n"
|
|
4560
|
+
);
|
|
4561
|
+
const answer = (await reader.question('Type "yes" to grant access: ')).trim().toLowerCase();
|
|
4562
|
+
return answer === "yes";
|
|
4563
|
+
} finally {
|
|
4564
|
+
reader.close();
|
|
4565
|
+
}
|
|
4566
|
+
}
|
|
4567
|
+
function requestFingerprint(value) {
|
|
4568
|
+
return JSON.stringify(value);
|
|
4569
|
+
}
|
|
4570
|
+
function taskStatus(revision, phase, current) {
|
|
4571
|
+
return Object.freeze({
|
|
4572
|
+
revision,
|
|
4573
|
+
deliveryStatus: current?.deliveryStatus ?? "queued",
|
|
4574
|
+
executionStatus: current?.executionStatus ?? "not-observable",
|
|
4575
|
+
managedPhase: phase,
|
|
4576
|
+
...current?.validationOutcome === void 0 ? {} : { validationOutcome: current.validationOutcome },
|
|
4577
|
+
files: [...current?.files ?? []],
|
|
4578
|
+
checks: [...current?.checks ?? []],
|
|
4579
|
+
timings: current?.timings ?? Object.freeze({}),
|
|
4580
|
+
...current?.resultExpiresAt === void 0 ? {} : { resultExpiresAt: current.resultExpiresAt }
|
|
4581
|
+
});
|
|
4582
|
+
}
|
|
4583
|
+
async function createExternalAgentSupervisor(options) {
|
|
4584
|
+
const now = options.now ?? (() => /* @__PURE__ */ new Date());
|
|
4585
|
+
const grantStore = await createManagedGrantStore({
|
|
4586
|
+
root: options.root,
|
|
4587
|
+
...options.configBase === void 0 ? {} : { configBase: options.configBase },
|
|
4588
|
+
now
|
|
4589
|
+
});
|
|
4590
|
+
const cleanupJournal = await createManagedThreadCleanupJournal({
|
|
4591
|
+
root: options.root,
|
|
4592
|
+
...options.configBase === void 0 ? {} : { configBase: options.configBase },
|
|
4593
|
+
now
|
|
4594
|
+
});
|
|
4595
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
4596
|
+
const idempotency = /* @__PURE__ */ new Map();
|
|
4597
|
+
const results = /* @__PURE__ */ new Map();
|
|
4598
|
+
let connection;
|
|
4599
|
+
let disposed = false;
|
|
4600
|
+
let operationTail = Promise.resolve();
|
|
4601
|
+
let status = externalAgentControlStatusSchema.parse({
|
|
4602
|
+
schemaVersion: EXTERNAL_AGENT_CONTROL_SCHEMA_VERSION,
|
|
4603
|
+
sequence: 0,
|
|
4604
|
+
mode: "inbox",
|
|
4605
|
+
adapter: {
|
|
4606
|
+
kind: "codex",
|
|
4607
|
+
maturity: "experimental",
|
|
4608
|
+
availability: "unavailable"
|
|
4609
|
+
},
|
|
4610
|
+
connectionState: "disconnected",
|
|
4611
|
+
authReadiness: "unknown",
|
|
4612
|
+
grantState: await grantStore.read(),
|
|
4613
|
+
updatedAt: now().toISOString()
|
|
4614
|
+
});
|
|
4615
|
+
const publish = (update) => {
|
|
4616
|
+
status = externalAgentControlStatusSchema.parse({
|
|
4617
|
+
...status,
|
|
4618
|
+
...update,
|
|
4619
|
+
schemaVersion: EXTERNAL_AGENT_CONTROL_SCHEMA_VERSION,
|
|
4620
|
+
sequence: status.sequence + 1,
|
|
4621
|
+
updatedAt: now().toISOString()
|
|
4622
|
+
});
|
|
4623
|
+
for (const listener of listeners) {
|
|
4624
|
+
try {
|
|
4625
|
+
listener(status);
|
|
4626
|
+
} catch {
|
|
4627
|
+
}
|
|
4628
|
+
}
|
|
4629
|
+
return status;
|
|
4630
|
+
};
|
|
4631
|
+
const rememberResult = (result) => {
|
|
4632
|
+
const managedResult = externalAgentManagedResultSchema.parse({
|
|
4633
|
+
revision: result.revision,
|
|
4634
|
+
diff: result.diff,
|
|
4635
|
+
files: result.files,
|
|
4636
|
+
checks: result.checks,
|
|
4637
|
+
timings: result.timings,
|
|
4638
|
+
validationOutcome: result.validationOutcome,
|
|
4639
|
+
expiresAt: result.expiresAt
|
|
4640
|
+
});
|
|
4641
|
+
results.set(result.revision, managedResult);
|
|
4642
|
+
while (results.size > MAXIMUM_RESULT_RECORDS) {
|
|
4643
|
+
const oldest = results.keys().next().value;
|
|
4644
|
+
if (oldest === void 0) break;
|
|
4645
|
+
results.delete(oldest);
|
|
4646
|
+
}
|
|
4647
|
+
publish({
|
|
4648
|
+
task: Object.freeze({
|
|
4649
|
+
revision: result.revision,
|
|
4650
|
+
deliveryStatus: "accepted",
|
|
4651
|
+
executionStatus: "terminal-succeeded",
|
|
4652
|
+
managedPhase: result.applied ? "completed" : "review-required",
|
|
4653
|
+
validationOutcome: result.validationOutcome,
|
|
4654
|
+
files: [...result.files],
|
|
4655
|
+
checks: [...result.checks],
|
|
4656
|
+
timings: result.timings,
|
|
4657
|
+
resultExpiresAt: result.expiresAt
|
|
4658
|
+
})
|
|
4659
|
+
});
|
|
4660
|
+
};
|
|
4661
|
+
const stopConnection = async () => {
|
|
4662
|
+
const active = connection;
|
|
4663
|
+
connection = void 0;
|
|
4664
|
+
if (active === void 0) return;
|
|
4665
|
+
active.controller.abort("supervisor-disconnect");
|
|
4666
|
+
await active.pump.close().catch(() => void 0);
|
|
4667
|
+
await active.run.catch(() => void 0);
|
|
4668
|
+
await active.execution.dispose().catch(() => void 0);
|
|
4669
|
+
};
|
|
4670
|
+
const connectInternal = async (allowPrompt) => {
|
|
4671
|
+
if (disposed) throw new SpotPatchError12(ERROR_CODES12.SESSION_CLOSED);
|
|
4672
|
+
if (connection !== void 0) return status;
|
|
4673
|
+
if (process.platform === "win32") {
|
|
4674
|
+
return publish({
|
|
4675
|
+
mode: "inbox",
|
|
4676
|
+
connectionState: "degraded",
|
|
4677
|
+
error: managedError(
|
|
4678
|
+
"MANAGED_PLATFORM_UNSUPPORTED",
|
|
4679
|
+
"integration",
|
|
4680
|
+
"none",
|
|
4681
|
+
"use-inbox"
|
|
4682
|
+
)
|
|
4683
|
+
});
|
|
4684
|
+
}
|
|
4685
|
+
publish({ connectionState: "diagnosing", error: void 0 });
|
|
4686
|
+
let grantState = await grantStore.read();
|
|
4687
|
+
if (grantState === "invalid") {
|
|
4688
|
+
return publish({
|
|
4689
|
+
grantState,
|
|
4690
|
+
connectionState: "error",
|
|
4691
|
+
error: managedError(
|
|
4692
|
+
"MANAGED_GRANT_INVALID",
|
|
4693
|
+
"integration",
|
|
4694
|
+
"user-action",
|
|
4695
|
+
"confirm-managed-access"
|
|
4696
|
+
)
|
|
4697
|
+
});
|
|
4698
|
+
}
|
|
4699
|
+
if (grantState === "missing") {
|
|
4700
|
+
publish({ grantState, connectionState: "awaiting-consent", mode: "inbox" });
|
|
4701
|
+
if (!allowPrompt) return status;
|
|
4702
|
+
const confirmed = await (options.confirmManagedAccess ?? defaultTerminalConfirmation)(options.projectLabel ?? grantStore.projectKey.slice(0, 12));
|
|
4703
|
+
if (!confirmed) return status;
|
|
4704
|
+
await grantStore.grant();
|
|
4705
|
+
grantState = "valid";
|
|
4706
|
+
publish({ grantState });
|
|
4707
|
+
}
|
|
4708
|
+
publish({ connectionState: "connecting", mode: "inbox", error: void 0 });
|
|
4709
|
+
const controller = new AbortController();
|
|
4710
|
+
const execution = createManagedExecutionRunner({
|
|
4711
|
+
root: options.root,
|
|
4712
|
+
...options.checks === void 0 ? {} : { checks: options.checks },
|
|
4713
|
+
...options.limits === void 0 ? {} : { limits: options.limits }
|
|
4714
|
+
});
|
|
4715
|
+
try {
|
|
4716
|
+
const adapterConnection = await (options.connectManagedAdapter ?? connectManagedCodexAppServer)({
|
|
4717
|
+
bridgeAdapter: options.bridgeAdapter,
|
|
4718
|
+
execution,
|
|
4719
|
+
cleanupJournal,
|
|
4720
|
+
onEvent(event) {
|
|
4721
|
+
if (event.type === "cleanup-warning") {
|
|
4722
|
+
const current2 = status.task?.revision === event.revision ? status.task : void 0;
|
|
4723
|
+
publish({
|
|
4724
|
+
mode: "inbox",
|
|
4725
|
+
connectionState: "degraded",
|
|
4726
|
+
task: taskStatus(event.revision, "cleanup-warning", current2),
|
|
4727
|
+
error: managedError(
|
|
4728
|
+
"MANAGED_CLEANUP_INCOMPLETE",
|
|
4729
|
+
"cleanup",
|
|
4730
|
+
"user-action",
|
|
4731
|
+
"inspect-cleanup-warning"
|
|
4732
|
+
)
|
|
4733
|
+
});
|
|
4734
|
+
controller.abort("managed-cleanup-incomplete");
|
|
4735
|
+
return;
|
|
4736
|
+
}
|
|
4737
|
+
if (event.type === "failure") {
|
|
4738
|
+
const current2 = status.task?.revision === event.revision ? status.task : void 0;
|
|
4739
|
+
const fatal = event.reason === "config-isolation" || event.reason === "protocol";
|
|
4740
|
+
publish({
|
|
4741
|
+
...fatal ? { mode: "inbox", connectionState: "degraded" } : {},
|
|
4742
|
+
task: taskStatus(event.revision, "failed", current2),
|
|
4743
|
+
error: managedExecutionError(event.reason)
|
|
4744
|
+
});
|
|
4745
|
+
if (fatal) controller.abort("managed-config-isolation-unsupported");
|
|
4746
|
+
return;
|
|
4747
|
+
}
|
|
4748
|
+
if (event.type === "result") {
|
|
4749
|
+
rememberResult(event.result);
|
|
4750
|
+
return;
|
|
4751
|
+
}
|
|
4752
|
+
const current = status.task?.revision === event.revision ? status.task : void 0;
|
|
4753
|
+
publish({
|
|
4754
|
+
task: taskStatus(event.revision, event.phase, current)
|
|
4755
|
+
});
|
|
4756
|
+
},
|
|
4757
|
+
projectRoot: options.root,
|
|
4758
|
+
...options.configBase === void 0 ? {} : { privateRuntimeBase: options.configBase },
|
|
4759
|
+
runtimeKey: grantStore.projectKey,
|
|
4760
|
+
sessionId: options.sessionId,
|
|
4761
|
+
signal: controller.signal
|
|
4762
|
+
});
|
|
4763
|
+
await grantStore.touch();
|
|
4764
|
+
const pump = createActiveEventPump({
|
|
4765
|
+
adapter: adapterConnection.adapter,
|
|
4766
|
+
client: createSpotPatchBridgeClient(options.root),
|
|
4767
|
+
sessionId: options.sessionId,
|
|
4768
|
+
onEvent(event) {
|
|
4769
|
+
if (event.type === "ready") {
|
|
4770
|
+
publish({ connectionState: "ready", mode: "managed", error: void 0 });
|
|
4771
|
+
return;
|
|
4772
|
+
}
|
|
4773
|
+
const current = status.task?.revision === event.revision ? status.task : void 0;
|
|
4774
|
+
const task = taskStatus(
|
|
4775
|
+
event.revision,
|
|
4776
|
+
event.phase === "working" ? "running" : event.phase === "completed" ? current?.managedPhase ?? "completed" : event.phase === "failed" || event.phase === "delivery-unknown" ? "failed" : current?.managedPhase ?? "preparing",
|
|
4777
|
+
current
|
|
4778
|
+
);
|
|
4779
|
+
publish({
|
|
4780
|
+
connectionState: event.phase === "completed" || event.phase === "failed" ? "ready" : "busy",
|
|
4781
|
+
task: Object.freeze({
|
|
4782
|
+
...task,
|
|
4783
|
+
deliveryStatus: event.phase === "dispatching" ? "dispatching" : event.phase === "delivery-unknown" ? "unknown" : "accepted",
|
|
4784
|
+
executionStatus: event.phase === "working" ? "started" : event.phase === "completed" ? "terminal-succeeded" : event.phase === "failed" ? "terminal-failed" : event.phase === "delivery-unknown" ? "unknown" : task.executionStatus
|
|
4785
|
+
})
|
|
4786
|
+
});
|
|
4787
|
+
}
|
|
4788
|
+
});
|
|
4789
|
+
const run = pump.run(controller.signal).catch((error) => {
|
|
4790
|
+
if (!controller.signal.aborted && !disposed) {
|
|
4791
|
+
publish({
|
|
4792
|
+
mode: "inbox",
|
|
4793
|
+
connectionState: "degraded",
|
|
4794
|
+
error: classifyConnectionError(error)
|
|
4795
|
+
});
|
|
4796
|
+
}
|
|
4797
|
+
}).finally(async () => {
|
|
4798
|
+
if (connection?.controller === controller) connection = void 0;
|
|
4799
|
+
await execution.dispose().catch(() => void 0);
|
|
4800
|
+
});
|
|
4801
|
+
const activeConnection = {
|
|
4802
|
+
controller,
|
|
4803
|
+
execution,
|
|
4804
|
+
pump,
|
|
4805
|
+
run
|
|
4806
|
+
};
|
|
4807
|
+
connection = activeConnection;
|
|
4808
|
+
publish({
|
|
4809
|
+
adapter: {
|
|
4810
|
+
kind: "codex",
|
|
4811
|
+
maturity: "experimental",
|
|
4812
|
+
availability: "available"
|
|
4813
|
+
},
|
|
4814
|
+
authReadiness: adapterConnection.authReadiness,
|
|
4815
|
+
...adapterConnection.requestedModel === void 0 ? {} : { requestedModel: adapterConnection.requestedModel },
|
|
4816
|
+
...adapterConnection.effectiveModel === void 0 ? {} : { effectiveModel: adapterConnection.effectiveModel }
|
|
4817
|
+
});
|
|
4818
|
+
return status;
|
|
4819
|
+
} catch (error) {
|
|
4820
|
+
controller.abort("supervisor-connect-failed");
|
|
4821
|
+
await execution.dispose().catch(() => void 0);
|
|
4822
|
+
const classified = classifyConnectionError(error);
|
|
4823
|
+
return publish({
|
|
4824
|
+
mode: "inbox",
|
|
4825
|
+
connectionState: "degraded",
|
|
4826
|
+
...classified.code === "AGENT_AUTH_REQUIRED" ? { authReadiness: "signed-out" } : {},
|
|
4827
|
+
error: classified
|
|
4828
|
+
});
|
|
4829
|
+
}
|
|
4830
|
+
};
|
|
4831
|
+
const serialize = (operation) => {
|
|
4832
|
+
const result = operationTail.then(operation, operation);
|
|
4833
|
+
operationTail = result.then(
|
|
4834
|
+
() => void 0,
|
|
4835
|
+
() => void 0
|
|
4836
|
+
);
|
|
4837
|
+
return result;
|
|
4838
|
+
};
|
|
4839
|
+
const idempotent = (requestId, fingerprint, operation) => {
|
|
4840
|
+
const prior = idempotency.get(requestId);
|
|
4841
|
+
if (prior !== void 0) {
|
|
4842
|
+
if (prior.fingerprint !== fingerprint) {
|
|
4843
|
+
return Promise.reject(new SpotPatchError12(ERROR_CODES12.INVALID_REQUEST));
|
|
4844
|
+
}
|
|
4845
|
+
return prior.result;
|
|
4846
|
+
}
|
|
4847
|
+
const result = serialize(operation);
|
|
4848
|
+
idempotency.set(requestId, { fingerprint, result });
|
|
4849
|
+
while (idempotency.size > MAXIMUM_IDEMPOTENCY_RECORDS) {
|
|
4850
|
+
const oldest = idempotency.keys().next().value;
|
|
4851
|
+
if (oldest === void 0) break;
|
|
4852
|
+
idempotency.delete(oldest);
|
|
4853
|
+
}
|
|
4854
|
+
return result;
|
|
4855
|
+
};
|
|
4856
|
+
const supervisor = Object.freeze({
|
|
4857
|
+
getStatus: () => status,
|
|
4858
|
+
connect(request, signal) {
|
|
4859
|
+
return idempotent(request.requestId, requestFingerprint(request), async () => {
|
|
4860
|
+
if (signal.aborted) throw new SpotPatchError12(ERROR_CODES12.AGENT_CANCELLED);
|
|
4861
|
+
return connectInternal(true);
|
|
4862
|
+
});
|
|
4863
|
+
},
|
|
4864
|
+
disconnect(request) {
|
|
4865
|
+
return idempotent(request.requestId, requestFingerprint(request), async () => {
|
|
4866
|
+
publish({ connectionState: "disconnecting" });
|
|
4867
|
+
await stopConnection();
|
|
4868
|
+
if (request.revokeGrant) {
|
|
4869
|
+
await removeManagedCodexRuntimeHome({
|
|
4870
|
+
...options.configBase === void 0 ? {} : { runtimeBase: options.configBase },
|
|
4871
|
+
runtimeKey: grantStore.projectKey
|
|
4872
|
+
});
|
|
4873
|
+
await grantStore.revoke();
|
|
4874
|
+
}
|
|
4875
|
+
return publish({
|
|
4876
|
+
mode: "inbox",
|
|
4877
|
+
connectionState: "disconnected",
|
|
4878
|
+
grantState: await grantStore.read(),
|
|
4879
|
+
authReadiness: "unknown",
|
|
4880
|
+
requestedModel: void 0,
|
|
4881
|
+
effectiveModel: void 0,
|
|
4882
|
+
error: void 0
|
|
4883
|
+
});
|
|
4884
|
+
});
|
|
4885
|
+
},
|
|
4886
|
+
cancel(request) {
|
|
4887
|
+
return idempotent(request.requestId, requestFingerprint(request), async () => {
|
|
4888
|
+
if (status.task?.revision !== request.revision) {
|
|
4889
|
+
throw new SpotPatchError12(ERROR_CODES12.HANDOFF_NOT_FOUND);
|
|
4890
|
+
}
|
|
4891
|
+
await stopConnection();
|
|
4892
|
+
return publish({
|
|
4893
|
+
mode: "inbox",
|
|
4894
|
+
connectionState: "disconnected",
|
|
4895
|
+
task: Object.freeze({
|
|
4896
|
+
...taskStatus(request.revision, "cancelled", status.task),
|
|
4897
|
+
executionStatus: "interrupted"
|
|
4898
|
+
})
|
|
4899
|
+
});
|
|
4900
|
+
});
|
|
4901
|
+
},
|
|
4902
|
+
getResult(revision) {
|
|
4903
|
+
const result = results.get(revision);
|
|
4904
|
+
if (result === void 0) return void 0;
|
|
4905
|
+
if (Date.parse(result.expiresAt) <= now().getTime()) {
|
|
4906
|
+
results.delete(revision);
|
|
4907
|
+
return void 0;
|
|
4908
|
+
}
|
|
4909
|
+
return result;
|
|
4910
|
+
},
|
|
4911
|
+
subscribe(listener) {
|
|
4912
|
+
if (disposed) return () => void 0;
|
|
4913
|
+
listeners.add(listener);
|
|
4914
|
+
return () => listeners.delete(listener);
|
|
4915
|
+
},
|
|
4916
|
+
async dispose() {
|
|
4917
|
+
if (disposed) return;
|
|
4918
|
+
disposed = true;
|
|
4919
|
+
listeners.clear();
|
|
4920
|
+
await serialize(stopConnection);
|
|
4921
|
+
idempotency.clear();
|
|
4922
|
+
results.clear();
|
|
4923
|
+
}
|
|
4924
|
+
});
|
|
4925
|
+
if (status.grantState === "valid") {
|
|
4926
|
+
void serialize(async () => {
|
|
4927
|
+
await connectInternal(false);
|
|
4928
|
+
});
|
|
4929
|
+
}
|
|
4930
|
+
return supervisor;
|
|
4931
|
+
}
|
|
3215
4932
|
export {
|
|
4933
|
+
ManagedCodexAppServerAdapter,
|
|
3216
4934
|
applyBridgeSetupPlan,
|
|
4935
|
+
connectManagedCodexAppServer,
|
|
3217
4936
|
createBridgeSetupPlan,
|
|
4937
|
+
createExternalAgentSupervisor,
|
|
3218
4938
|
createSpotPatchBridgeClient,
|
|
3219
4939
|
createSpotPatchMcpServer,
|
|
3220
4940
|
runSpotPatchBridgeCli,
|