@roamcode.ai/server 1.0.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/LICENSE +21 -0
- package/dist/auth.d.ts +63 -0
- package/dist/auth.js +133 -0
- package/dist/claude-auth-service.d.ts +76 -0
- package/dist/claude-auth-service.js +217 -0
- package/dist/claude-latest-service.d.ts +34 -0
- package/dist/claude-latest-service.js +61 -0
- package/dist/config.d.ts +78 -0
- package/dist/config.js +59 -0
- package/dist/data-dir.d.ts +42 -0
- package/dist/data-dir.js +70 -0
- package/dist/diag.d.ts +43 -0
- package/dist/diag.js +83 -0
- package/dist/fs-service.d.ts +90 -0
- package/dist/fs-service.js +290 -0
- package/dist/index.d.ts +82 -0
- package/dist/index.js +44 -0
- package/dist/managed-runtime.d.ts +51 -0
- package/dist/managed-runtime.js +411 -0
- package/dist/managed-update-helper.d.ts +2 -0
- package/dist/managed-update-helper.js +34 -0
- package/dist/mcp-send.d.ts +33 -0
- package/dist/mcp-send.js +107 -0
- package/dist/origin-check.d.ts +37 -0
- package/dist/origin-check.js +101 -0
- package/dist/pane-status.d.ts +61 -0
- package/dist/pane-status.js +145 -0
- package/dist/providers/claude-metadata-service.d.ts +58 -0
- package/dist/providers/claude-metadata-service.js +352 -0
- package/dist/providers/claude-provider.d.ts +11 -0
- package/dist/providers/claude-provider.js +166 -0
- package/dist/providers/codex-activity.d.ts +21 -0
- package/dist/providers/codex-activity.js +122 -0
- package/dist/providers/codex-app-server-client.d.ts +90 -0
- package/dist/providers/codex-app-server-client.js +485 -0
- package/dist/providers/codex-latest-service.d.ts +50 -0
- package/dist/providers/codex-latest-service.js +174 -0
- package/dist/providers/codex-metadata-service.d.ts +161 -0
- package/dist/providers/codex-metadata-service.js +686 -0
- package/dist/providers/codex-profile-client.d.ts +16 -0
- package/dist/providers/codex-profile-client.js +52 -0
- package/dist/providers/codex-profile-security.d.ts +23 -0
- package/dist/providers/codex-profile-security.js +161 -0
- package/dist/providers/codex-provider.d.ts +15 -0
- package/dist/providers/codex-provider.js +174 -0
- package/dist/providers/codex-thread-coordinator.d.ts +18 -0
- package/dist/providers/codex-thread-coordinator.js +93 -0
- package/dist/providers/codex-thread-persistence.d.ts +9 -0
- package/dist/providers/codex-thread-persistence.js +45 -0
- package/dist/providers/codex-thread-resolver.d.ts +59 -0
- package/dist/providers/codex-thread-resolver.js +322 -0
- package/dist/providers/options.d.ts +7 -0
- package/dist/providers/options.js +155 -0
- package/dist/providers/provider-artifacts.d.ts +3 -0
- package/dist/providers/provider-artifacts.js +30 -0
- package/dist/providers/registry.d.ts +7 -0
- package/dist/providers/registry.js +23 -0
- package/dist/providers/types.d.ts +95 -0
- package/dist/providers/types.js +8 -0
- package/dist/push-dispatch.d.ts +81 -0
- package/dist/push-dispatch.js +100 -0
- package/dist/push-store.d.ts +25 -0
- package/dist/push-store.js +79 -0
- package/dist/rate-limit.d.ts +52 -0
- package/dist/rate-limit.js +72 -0
- package/dist/server-config.d.ts +60 -0
- package/dist/server-config.js +77 -0
- package/dist/service-install.d.ts +60 -0
- package/dist/service-install.js +221 -0
- package/dist/session-defaults.d.ts +26 -0
- package/dist/session-defaults.js +60 -0
- package/dist/session-store.d.ts +81 -0
- package/dist/session-store.js +654 -0
- package/dist/start.d.ts +31 -0
- package/dist/start.js +372 -0
- package/dist/static-routes.d.ts +101 -0
- package/dist/static-routes.js +188 -0
- package/dist/terminal-capability.d.ts +5 -0
- package/dist/terminal-capability.js +27 -0
- package/dist/terminal-manager.d.ts +224 -0
- package/dist/terminal-manager.js +917 -0
- package/dist/terminal-process.d.ts +85 -0
- package/dist/terminal-process.js +238 -0
- package/dist/terminal-shared.d.ts +36 -0
- package/dist/terminal-shared.js +43 -0
- package/dist/tmux-list.d.ts +11 -0
- package/dist/tmux-list.js +39 -0
- package/dist/transport.d.ts +123 -0
- package/dist/transport.js +1559 -0
- package/dist/updater.d.ts +161 -0
- package/dist/updater.js +451 -0
- package/dist/usage-service.d.ts +118 -0
- package/dist/usage-service.js +173 -0
- package/dist/vapid.d.ts +17 -0
- package/dist/vapid.js +31 -0
- package/dist/web-push-send.d.ts +20 -0
- package/dist/web-push-send.js +21 -0
- package/dist/ws-ticket.d.ts +47 -0
- package/dist/ws-ticket.js +62 -0
- package/package.json +55 -0
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { ZodType } from "zod";
|
|
2
|
+
import { type SpawnCodexAppServerTransport } from "./codex-app-server-client.js";
|
|
3
|
+
export interface CodexProfileClientLifecycle {
|
|
4
|
+
readConfig<T>(profile: string, codexHome: string, cwd: string, schema: ZodType<T>): Promise<T>;
|
|
5
|
+
}
|
|
6
|
+
export interface CreateCodexProfileClientLifecycleOptions {
|
|
7
|
+
readonly codexBin: string;
|
|
8
|
+
readonly env?: NodeJS.ProcessEnv;
|
|
9
|
+
readonly spawnTransport?: SpawnCodexAppServerTransport;
|
|
10
|
+
readonly timeoutMs?: number;
|
|
11
|
+
}
|
|
12
|
+
/** Required selected-profile lifecycle. It owns Task 6 client construction, canonical env, start, and stop. */
|
|
13
|
+
export declare function createCodexProfileClientLifecycle(options: CreateCodexProfileClientLifecycleOptions): CodexProfileClientLifecycle;
|
|
14
|
+
/** Isolated protocol seam for service tests; unavailable outside the test process. */
|
|
15
|
+
export declare function createCodexProfileClientLifecycleForTests(readConfig: CodexProfileClientLifecycle["readConfig"]): CodexProfileClientLifecycle;
|
|
16
|
+
export declare function isCodexProfileClientLifecycle(value: unknown): value is CodexProfileClientLifecycle;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { CodexAppServerClient, } from "./codex-app-server-client.js";
|
|
2
|
+
import { ProviderError } from "./types.js";
|
|
3
|
+
const lifecycles = new WeakSet();
|
|
4
|
+
function unavailable() {
|
|
5
|
+
return new ProviderError("OSS_PROVIDER_DEFERRED", "Codex profile capability proof is unavailable");
|
|
6
|
+
}
|
|
7
|
+
function brand(lifecycle) {
|
|
8
|
+
lifecycles.add(lifecycle);
|
|
9
|
+
return lifecycle;
|
|
10
|
+
}
|
|
11
|
+
/** Required selected-profile lifecycle. It owns Task 6 client construction, canonical env, start, and stop. */
|
|
12
|
+
export function createCodexProfileClientLifecycle(options) {
|
|
13
|
+
return brand({
|
|
14
|
+
async readConfig(profile, codexHome, cwd, schema) {
|
|
15
|
+
const clientOptions = {
|
|
16
|
+
codexBin: options.codexBin,
|
|
17
|
+
profile,
|
|
18
|
+
env: { ...(options.env ?? process.env), CODEX_HOME: codexHome },
|
|
19
|
+
...(options.spawnTransport ? { spawnTransport: options.spawnTransport } : {}),
|
|
20
|
+
...(options.timeoutMs === undefined ? {} : { timeoutMs: options.timeoutMs }),
|
|
21
|
+
};
|
|
22
|
+
const client = new CodexAppServerClient(clientOptions);
|
|
23
|
+
let result;
|
|
24
|
+
let failed = false;
|
|
25
|
+
try {
|
|
26
|
+
await client.start();
|
|
27
|
+
result = await client.request("config/read", { cwd, includeLayers: false }, schema);
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
failed = true;
|
|
31
|
+
}
|
|
32
|
+
try {
|
|
33
|
+
await client.stop();
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
failed = true;
|
|
37
|
+
}
|
|
38
|
+
if (failed || result === undefined)
|
|
39
|
+
throw unavailable();
|
|
40
|
+
return result;
|
|
41
|
+
},
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
/** Isolated protocol seam for service tests; unavailable outside the test process. */
|
|
45
|
+
export function createCodexProfileClientLifecycleForTests(readConfig) {
|
|
46
|
+
if (process.env.NODE_ENV !== "test")
|
|
47
|
+
throw unavailable();
|
|
48
|
+
return brand({ readConfig });
|
|
49
|
+
}
|
|
50
|
+
export function isCodexProfileClientLifecycle(value) {
|
|
51
|
+
return typeof value === "object" && value !== null && lifecycles.has(value);
|
|
52
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export interface CodexProfileFingerprint {
|
|
2
|
+
readonly path: string;
|
|
3
|
+
readonly realPath: string;
|
|
4
|
+
readonly dev: bigint;
|
|
5
|
+
readonly ino: bigint;
|
|
6
|
+
readonly size: bigint;
|
|
7
|
+
readonly mtimeNs: bigint;
|
|
8
|
+
readonly digest: string;
|
|
9
|
+
}
|
|
10
|
+
export interface CodexLaunchConfigFingerprint {
|
|
11
|
+
readonly homeDev: bigint;
|
|
12
|
+
readonly homeIno: bigint;
|
|
13
|
+
readonly homeSize: bigint;
|
|
14
|
+
readonly homeMtimeNs: bigint;
|
|
15
|
+
readonly base: CodexProfileFingerprint | null;
|
|
16
|
+
readonly profile: CodexProfileFingerprint;
|
|
17
|
+
}
|
|
18
|
+
export declare function resolveSecureCodexHome(configured: string | undefined): Promise<string | undefined>;
|
|
19
|
+
export declare function listSecureProfileNames(codexHome: string, validName: (name: string) => boolean): Promise<string[]>;
|
|
20
|
+
export declare function captureProfileFingerprint(codexHome: string, profile: string): Promise<CodexProfileFingerprint | undefined>;
|
|
21
|
+
export declare function sameProfileFingerprint(left: CodexProfileFingerprint, right: CodexProfileFingerprint | undefined): boolean;
|
|
22
|
+
export declare function captureLaunchConfigFingerprint(codexHome: string, profile: string): Promise<CodexLaunchConfigFingerprint | undefined>;
|
|
23
|
+
export declare function sameLaunchConfigFingerprint(left: CodexLaunchConfigFingerprint, right: CodexLaunchConfigFingerprint | undefined): boolean;
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { lstat, readFile, readdir, realpath } from "node:fs/promises";
|
|
3
|
+
import { isAbsolute, join, parse, relative, resolve, sep } from "node:path";
|
|
4
|
+
const PROFILE_SUFFIX = ".config.toml";
|
|
5
|
+
const MAX_DIRECTORY_ENTRIES = 1_024;
|
|
6
|
+
const MAX_CONFIG_BYTES = 1024 * 1024;
|
|
7
|
+
async function hasSymlinkComponent(path) {
|
|
8
|
+
const absolute = resolve(path);
|
|
9
|
+
const root = parse(absolute).root;
|
|
10
|
+
const segments = absolute.slice(root.length).split(sep).filter(Boolean);
|
|
11
|
+
let current = root;
|
|
12
|
+
for (const segment of segments) {
|
|
13
|
+
current = join(current, segment);
|
|
14
|
+
if ((await lstat(current)).isSymbolicLink())
|
|
15
|
+
return true;
|
|
16
|
+
}
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
export async function resolveSecureCodexHome(configured) {
|
|
20
|
+
if (!configured || !isAbsolute(configured))
|
|
21
|
+
return undefined;
|
|
22
|
+
try {
|
|
23
|
+
if (await hasSymlinkComponent(configured))
|
|
24
|
+
return undefined;
|
|
25
|
+
const stat = await lstat(configured, { bigint: true });
|
|
26
|
+
if (!stat.isDirectory() || stat.isSymbolicLink())
|
|
27
|
+
return undefined;
|
|
28
|
+
const canonical = await realpath(configured);
|
|
29
|
+
const after = await lstat(configured, { bigint: true });
|
|
30
|
+
return canonical === resolve(configured) &&
|
|
31
|
+
after.isDirectory() &&
|
|
32
|
+
!after.isSymbolicLink() &&
|
|
33
|
+
stat.dev === after.dev &&
|
|
34
|
+
stat.ino === after.ino
|
|
35
|
+
? canonical
|
|
36
|
+
: undefined;
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
return undefined;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
export async function listSecureProfileNames(codexHome, validName) {
|
|
43
|
+
if ((await resolveSecureCodexHome(codexHome)) !== codexHome)
|
|
44
|
+
return [];
|
|
45
|
+
const before = await lstat(codexHome, { bigint: true });
|
|
46
|
+
const entries = await readdir(codexHome, { withFileTypes: true });
|
|
47
|
+
const profiles = [];
|
|
48
|
+
for (const entry of entries.slice(0, MAX_DIRECTORY_ENTRIES)) {
|
|
49
|
+
if (!entry.isFile() || entry.isSymbolicLink() || !entry.name.endsWith(PROFILE_SUFFIX))
|
|
50
|
+
continue;
|
|
51
|
+
const profile = entry.name.slice(0, -PROFILE_SUFFIX.length);
|
|
52
|
+
if (!validName(profile))
|
|
53
|
+
continue;
|
|
54
|
+
const fingerprint = await captureProfileFingerprint(codexHome, profile);
|
|
55
|
+
if (fingerprint)
|
|
56
|
+
profiles.push(profile);
|
|
57
|
+
}
|
|
58
|
+
const after = await lstat(codexHome, { bigint: true });
|
|
59
|
+
if ((await resolveSecureCodexHome(codexHome)) !== codexHome ||
|
|
60
|
+
before.dev !== after.dev ||
|
|
61
|
+
before.ino !== after.ino ||
|
|
62
|
+
before.size !== after.size ||
|
|
63
|
+
before.mtimeNs !== after.mtimeNs) {
|
|
64
|
+
return [];
|
|
65
|
+
}
|
|
66
|
+
return [...new Set(profiles)].sort((left, right) => left.localeCompare(right));
|
|
67
|
+
}
|
|
68
|
+
export async function captureProfileFingerprint(codexHome, profile) {
|
|
69
|
+
return captureConfigFileFingerprint(codexHome, join(codexHome, `${profile}${PROFILE_SUFFIX}`));
|
|
70
|
+
}
|
|
71
|
+
async function captureConfigFileFingerprint(codexHome, path) {
|
|
72
|
+
try {
|
|
73
|
+
if (relative(codexHome, path).startsWith(".."))
|
|
74
|
+
return undefined;
|
|
75
|
+
const stat = await lstat(path, { bigint: true });
|
|
76
|
+
if (!stat.isFile() || stat.isSymbolicLink())
|
|
77
|
+
return undefined;
|
|
78
|
+
if (stat.size > BigInt(MAX_CONFIG_BYTES))
|
|
79
|
+
return undefined;
|
|
80
|
+
const realPath = await realpath(path);
|
|
81
|
+
if (realPath !== path)
|
|
82
|
+
return undefined;
|
|
83
|
+
const contents = await readFile(path);
|
|
84
|
+
const after = await lstat(path, { bigint: true });
|
|
85
|
+
if (!after.isFile() ||
|
|
86
|
+
after.isSymbolicLink() ||
|
|
87
|
+
stat.dev !== after.dev ||
|
|
88
|
+
stat.ino !== after.ino ||
|
|
89
|
+
stat.size !== after.size ||
|
|
90
|
+
stat.mtimeNs !== after.mtimeNs) {
|
|
91
|
+
return undefined;
|
|
92
|
+
}
|
|
93
|
+
return {
|
|
94
|
+
path,
|
|
95
|
+
realPath,
|
|
96
|
+
dev: stat.dev,
|
|
97
|
+
ino: stat.ino,
|
|
98
|
+
size: stat.size,
|
|
99
|
+
mtimeNs: stat.mtimeNs,
|
|
100
|
+
digest: createHash("sha256").update(contents).digest("hex"),
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
return undefined;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
export function sameProfileFingerprint(left, right) {
|
|
108
|
+
return Boolean(right &&
|
|
109
|
+
left.path === right.path &&
|
|
110
|
+
left.realPath === right.realPath &&
|
|
111
|
+
left.dev === right.dev &&
|
|
112
|
+
left.ino === right.ino &&
|
|
113
|
+
left.size === right.size &&
|
|
114
|
+
left.mtimeNs === right.mtimeNs &&
|
|
115
|
+
left.digest === right.digest);
|
|
116
|
+
}
|
|
117
|
+
export async function captureLaunchConfigFingerprint(codexHome, profile) {
|
|
118
|
+
try {
|
|
119
|
+
if ((await resolveSecureCodexHome(codexHome)) !== codexHome)
|
|
120
|
+
return undefined;
|
|
121
|
+
const home = await lstat(codexHome, { bigint: true });
|
|
122
|
+
const selected = await captureProfileFingerprint(codexHome, profile);
|
|
123
|
+
if (!selected)
|
|
124
|
+
return undefined;
|
|
125
|
+
const basePath = join(codexHome, "config.toml");
|
|
126
|
+
let base = null;
|
|
127
|
+
try {
|
|
128
|
+
const baseStat = await lstat(basePath);
|
|
129
|
+
if (!baseStat.isFile() || baseStat.isSymbolicLink())
|
|
130
|
+
return undefined;
|
|
131
|
+
const capturedBase = await captureConfigFileFingerprint(codexHome, basePath);
|
|
132
|
+
if (!capturedBase)
|
|
133
|
+
return undefined;
|
|
134
|
+
base = capturedBase;
|
|
135
|
+
}
|
|
136
|
+
catch (error) {
|
|
137
|
+
if (error.code !== "ENOENT")
|
|
138
|
+
return undefined;
|
|
139
|
+
}
|
|
140
|
+
return {
|
|
141
|
+
homeDev: home.dev,
|
|
142
|
+
homeIno: home.ino,
|
|
143
|
+
homeSize: home.size,
|
|
144
|
+
homeMtimeNs: home.mtimeNs,
|
|
145
|
+
base,
|
|
146
|
+
profile: selected,
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
catch {
|
|
150
|
+
return undefined;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
export function sameLaunchConfigFingerprint(left, right) {
|
|
154
|
+
return Boolean(right &&
|
|
155
|
+
left.homeDev === right.homeDev &&
|
|
156
|
+
left.homeIno === right.homeIno &&
|
|
157
|
+
left.homeSize === right.homeSize &&
|
|
158
|
+
left.homeMtimeNs === right.homeMtimeNs &&
|
|
159
|
+
(left.base === null ? right.base === null : right.base !== null && sameProfileFingerprint(left.base, right.base)) &&
|
|
160
|
+
sameProfileFingerprint(left.profile, right.profile));
|
|
161
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { type AttachSpawnOptions } from "../config.js";
|
|
2
|
+
import { type AgentProvider, type CodexProfileLaunchProof, type ProviderProcessContext, type ProviderAvailability } from "./types.js";
|
|
3
|
+
export interface CreateCodexProviderOptions {
|
|
4
|
+
codexBin: string;
|
|
5
|
+
env?: NodeJS.ProcessEnv;
|
|
6
|
+
attach?: AttachSpawnOptions;
|
|
7
|
+
getAttach?: () => AttachSpawnOptions | undefined;
|
|
8
|
+
probe?: () => Promise<ProviderAvailability>;
|
|
9
|
+
/** Task 7 supplies the effective-provider capability check; this adapter never reads profile files. */
|
|
10
|
+
validateProfile?(profile: string, cwd: string): CodexProfileLaunchProof | Promise<CodexProfileLaunchProof>;
|
|
11
|
+
}
|
|
12
|
+
type CodexAttachContext = Pick<AttachSpawnOptions, "baseUrl" | "token" | "mcpScriptPath">;
|
|
13
|
+
export declare function buildCodexArgs(context: ProviderProcessContext, attach?: CodexAttachContext): string[];
|
|
14
|
+
export declare function createCodexProvider(options: CreateCodexProviderOptions): AgentProvider;
|
|
15
|
+
export {};
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import { codexMcpTokenPathFor } from "../config.js";
|
|
2
|
+
import { isAbsolute } from "node:path";
|
|
3
|
+
import { classifyCodexPane, createCodexOscParser, parseCodexOscNotifications, parseCodexRuntimeMetadata, } from "./codex-activity.js";
|
|
4
|
+
import { assertExactCodexResumeArgs } from "./codex-thread-resolver.js";
|
|
5
|
+
import { cleanupProviderArtifacts, writeProviderArtifact0600 } from "./provider-artifacts.js";
|
|
6
|
+
import { ProviderError, } from "./types.js";
|
|
7
|
+
function configArg(key, value) {
|
|
8
|
+
return ["-c", `${key}=${JSON.stringify(value)}`];
|
|
9
|
+
}
|
|
10
|
+
function validResumeId(id) {
|
|
11
|
+
return (id !== undefined &&
|
|
12
|
+
id.trim().length > 0 &&
|
|
13
|
+
id.length <= 2048 &&
|
|
14
|
+
!/[\p{Cc}\p{Zl}\p{Zp}]/u.test(id) &&
|
|
15
|
+
!id.trimStart().startsWith("-"));
|
|
16
|
+
}
|
|
17
|
+
function usableAttach(attach) {
|
|
18
|
+
return Boolean(attach?.baseUrl &&
|
|
19
|
+
attach.token &&
|
|
20
|
+
Buffer.byteLength(attach.token, "utf8") <= 4096 &&
|
|
21
|
+
!/[\p{Cc}\p{Zl}\p{Zp}]/u.test(attach.token) &&
|
|
22
|
+
attach.mcpScriptPath &&
|
|
23
|
+
attach.dataDir);
|
|
24
|
+
}
|
|
25
|
+
function profileUnavailable() {
|
|
26
|
+
return new ProviderError("OSS_PROVIDER_DEFERRED", "Codex profile capability proof is unavailable");
|
|
27
|
+
}
|
|
28
|
+
export function buildCodexArgs(context, attach) {
|
|
29
|
+
if (context.options.provider !== "codex") {
|
|
30
|
+
throw new ProviderError("INVALID_PROVIDER_OPTIONS", "Codex provider received non-Codex options");
|
|
31
|
+
}
|
|
32
|
+
if (context.intent === "fresh" && context.providerSessionId !== undefined) {
|
|
33
|
+
throw new ProviderError("INVALID_PROVIDER_OPTIONS", "Fresh Codex launch cannot include a resume identity");
|
|
34
|
+
}
|
|
35
|
+
const options = context.options;
|
|
36
|
+
const args = [];
|
|
37
|
+
let resumeId;
|
|
38
|
+
if (context.intent === "resume") {
|
|
39
|
+
resumeId = context.providerSessionId;
|
|
40
|
+
if (!validResumeId(resumeId)) {
|
|
41
|
+
throw new ProviderError("RESUME_IDENTITY_UNAVAILABLE", "Codex resume requires an exact safe session id");
|
|
42
|
+
}
|
|
43
|
+
args.push("resume");
|
|
44
|
+
}
|
|
45
|
+
if (options.model)
|
|
46
|
+
args.push("--model", options.model);
|
|
47
|
+
if (options.profile)
|
|
48
|
+
args.push("--profile", options.profile);
|
|
49
|
+
if (options.dangerouslyBypassApprovalsAndSandbox) {
|
|
50
|
+
args.push("--dangerously-bypass-approvals-and-sandbox");
|
|
51
|
+
}
|
|
52
|
+
else {
|
|
53
|
+
if (options.sandbox)
|
|
54
|
+
args.push("--sandbox", options.sandbox);
|
|
55
|
+
if (options.approvalPolicy)
|
|
56
|
+
args.push("--ask-for-approval", options.approvalPolicy);
|
|
57
|
+
}
|
|
58
|
+
if (options.webSearch)
|
|
59
|
+
args.push("--search");
|
|
60
|
+
for (const dir of options.addDirs ?? [])
|
|
61
|
+
args.push("--add-dir", dir);
|
|
62
|
+
if (options.reasoningEffort)
|
|
63
|
+
args.push(...configArg("model_reasoning_effort", options.reasoningEffort));
|
|
64
|
+
if (attach) {
|
|
65
|
+
args.push(...configArg("mcp_servers.roamcode.command", process.execPath));
|
|
66
|
+
args.push(...configArg("mcp_servers.roamcode.args", [attach.mcpScriptPath]));
|
|
67
|
+
args.push(...configArg("mcp_servers.roamcode.env_vars", ["RC_BASE_URL", "RC_SESSION_ID", "RC_TOKEN_FILE"]));
|
|
68
|
+
}
|
|
69
|
+
args.push(...configArg("tui.notifications", ["agent-turn-complete", "approval-requested", "plan-mode-prompt"]));
|
|
70
|
+
args.push(...configArg("tui.notification_method", "osc9"));
|
|
71
|
+
args.push(...configArg("tui.notification_condition", "always"));
|
|
72
|
+
// Keep the conversation in tmux's ordinary pane history. With tmux mouse mode enabled, wheel/trackpad
|
|
73
|
+
// and mobile scroll can read that history in place instead of opening Codex's separate transcript UI.
|
|
74
|
+
args.push("--no-alt-screen");
|
|
75
|
+
if (resumeId)
|
|
76
|
+
args.push("--", resumeId);
|
|
77
|
+
if (context.intent === "resume")
|
|
78
|
+
assertExactCodexResumeArgs(args);
|
|
79
|
+
return args;
|
|
80
|
+
}
|
|
81
|
+
export function createCodexProvider(options) {
|
|
82
|
+
return {
|
|
83
|
+
id: "codex",
|
|
84
|
+
displayName: "Codex",
|
|
85
|
+
resumeIdentity: "required",
|
|
86
|
+
probe: options.probe ?? (() => Promise.resolve({ terminalAvailable: true, metadataAvailable: false })),
|
|
87
|
+
buildProcess: async (context) => {
|
|
88
|
+
if (context.options.provider !== "codex") {
|
|
89
|
+
throw new ProviderError("INVALID_PROVIDER_OPTIONS", "Codex provider received non-Codex options");
|
|
90
|
+
}
|
|
91
|
+
let profileProof;
|
|
92
|
+
if (context.options.profile) {
|
|
93
|
+
let validateProfile;
|
|
94
|
+
try {
|
|
95
|
+
validateProfile = options.validateProfile;
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
throw profileUnavailable();
|
|
99
|
+
}
|
|
100
|
+
if (!validateProfile)
|
|
101
|
+
throw profileUnavailable();
|
|
102
|
+
try {
|
|
103
|
+
profileProof = await validateProfile(context.options.profile, context.cwd);
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
throw profileUnavailable();
|
|
107
|
+
}
|
|
108
|
+
if (profileProof.profile !== context.options.profile ||
|
|
109
|
+
!isAbsolute(profileProof.codexHome) ||
|
|
110
|
+
/[\p{Cc}\p{Zl}\p{Zp}]/u.test(profileProof.codexHome) ||
|
|
111
|
+
typeof profileProof.assertUnchanged !== "function") {
|
|
112
|
+
throw profileUnavailable();
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
const env = { ...(options.env ?? process.env) };
|
|
116
|
+
delete env.RC_TOKEN;
|
|
117
|
+
delete env.RC_TOKEN_FILE;
|
|
118
|
+
if (profileProof)
|
|
119
|
+
env.CODEX_HOME = profileProof.codexHome;
|
|
120
|
+
const preSpawnCheck = profileProof
|
|
121
|
+
? async () => {
|
|
122
|
+
try {
|
|
123
|
+
await profileProof.assertUnchanged();
|
|
124
|
+
}
|
|
125
|
+
catch {
|
|
126
|
+
throw profileUnavailable();
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
: undefined;
|
|
130
|
+
const ownedPaths = [];
|
|
131
|
+
const candidateAttach = context.attach ?? options.getAttach?.() ?? options.attach;
|
|
132
|
+
let attach;
|
|
133
|
+
try {
|
|
134
|
+
if (usableAttach(candidateAttach)) {
|
|
135
|
+
const tokenPath = codexMcpTokenPathFor(candidateAttach.dataDir, context.roamSessionId);
|
|
136
|
+
if (writeProviderArtifact0600(tokenPath, candidateAttach.token, context, ownedPaths)) {
|
|
137
|
+
attach = candidateAttach;
|
|
138
|
+
env.RC_BASE_URL = candidateAttach.baseUrl;
|
|
139
|
+
env.RC_SESSION_ID = context.roamSessionId;
|
|
140
|
+
env.RC_TOKEN_FILE = tokenPath;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
const args = buildCodexArgs(context, attach);
|
|
144
|
+
return {
|
|
145
|
+
executable: options.codexBin,
|
|
146
|
+
args,
|
|
147
|
+
env,
|
|
148
|
+
cleanupPaths: ownedPaths,
|
|
149
|
+
...(preSpawnCheck ? { preSpawnCheck } : {}),
|
|
150
|
+
integration: attach
|
|
151
|
+
? {
|
|
152
|
+
attachments: "ready",
|
|
153
|
+
activity: "degraded",
|
|
154
|
+
detail: "Codex activity uses display-text signals with pane fallback",
|
|
155
|
+
}
|
|
156
|
+
: {
|
|
157
|
+
attachments: "degraded",
|
|
158
|
+
activity: "degraded",
|
|
159
|
+
detail: "RoamCode attachment MCP is not configured; Codex activity uses display-text signals with pane fallback",
|
|
160
|
+
},
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
catch (error) {
|
|
164
|
+
cleanupProviderArtifacts(ownedPaths);
|
|
165
|
+
throw error;
|
|
166
|
+
}
|
|
167
|
+
},
|
|
168
|
+
createRuntimeSignalParser: createCodexOscParser,
|
|
169
|
+
runtimeSignals: parseCodexOscNotifications,
|
|
170
|
+
classifyPane: classifyCodexPane,
|
|
171
|
+
runtimeMetadata: parseCodexRuntimeMetadata,
|
|
172
|
+
cleanup: cleanupProviderArtifacts,
|
|
173
|
+
};
|
|
174
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export interface CodexSpawnLease {
|
|
2
|
+
readonly started: Promise<void>;
|
|
3
|
+
cancel(): Promise<void>;
|
|
4
|
+
}
|
|
5
|
+
declare class CodexThreadResolutionCoordinator {
|
|
6
|
+
private locked;
|
|
7
|
+
private poisoned;
|
|
8
|
+
private readonly queue;
|
|
9
|
+
acquire(signal?: AbortSignal): Promise<() => void>;
|
|
10
|
+
acknowledgeCancellation(lease: CodexSpawnLease, timeoutMs: number): Promise<void>;
|
|
11
|
+
/** A launch callback ran but did not return a cancellable lease, so later discovery is permanently unsafe. */
|
|
12
|
+
poisonUnknownSpawnOutcome(): void;
|
|
13
|
+
resetForTests(): void;
|
|
14
|
+
private poison;
|
|
15
|
+
private grant;
|
|
16
|
+
}
|
|
17
|
+
export declare const codexThreadResolutionCoordinator: CodexThreadResolutionCoordinator;
|
|
18
|
+
export {};
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { ProviderError } from "./types.js";
|
|
2
|
+
function unavailable() {
|
|
3
|
+
return new ProviderError("RESUME_IDENTITY_UNAVAILABLE", "Codex resume identity is unavailable");
|
|
4
|
+
}
|
|
5
|
+
class CodexThreadResolutionCoordinator {
|
|
6
|
+
locked = false;
|
|
7
|
+
poisoned = false;
|
|
8
|
+
queue = [];
|
|
9
|
+
acquire(signal) {
|
|
10
|
+
if (this.poisoned || signal?.aborted)
|
|
11
|
+
return Promise.reject(unavailable());
|
|
12
|
+
return new Promise((resolve, reject) => {
|
|
13
|
+
const waiter = { resolve, reject, signal };
|
|
14
|
+
if (signal) {
|
|
15
|
+
waiter.abort = () => {
|
|
16
|
+
const index = this.queue.indexOf(waiter);
|
|
17
|
+
if (index >= 0)
|
|
18
|
+
this.queue.splice(index, 1);
|
|
19
|
+
reject(unavailable());
|
|
20
|
+
};
|
|
21
|
+
signal.addEventListener("abort", waiter.abort, { once: true });
|
|
22
|
+
}
|
|
23
|
+
if (!this.locked) {
|
|
24
|
+
this.locked = true;
|
|
25
|
+
this.grant(waiter);
|
|
26
|
+
}
|
|
27
|
+
else {
|
|
28
|
+
this.queue.push(waiter);
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
async acknowledgeCancellation(lease, timeoutMs) {
|
|
33
|
+
let timer;
|
|
34
|
+
try {
|
|
35
|
+
const acknowledgement = lease.cancel();
|
|
36
|
+
if (!(acknowledgement instanceof Promise))
|
|
37
|
+
throw unavailable();
|
|
38
|
+
await Promise.race([
|
|
39
|
+
acknowledgement,
|
|
40
|
+
new Promise((_, reject) => {
|
|
41
|
+
timer = setTimeout(() => reject(unavailable()), timeoutMs);
|
|
42
|
+
timer.unref?.();
|
|
43
|
+
}),
|
|
44
|
+
]);
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
this.poison();
|
|
48
|
+
throw unavailable();
|
|
49
|
+
}
|
|
50
|
+
finally {
|
|
51
|
+
if (timer)
|
|
52
|
+
clearTimeout(timer);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
/** A launch callback ran but did not return a cancellable lease, so later discovery is permanently unsafe. */
|
|
56
|
+
poisonUnknownSpawnOutcome() {
|
|
57
|
+
this.poison();
|
|
58
|
+
}
|
|
59
|
+
resetForTests() {
|
|
60
|
+
if (process.env.NODE_ENV !== "test" || this.locked || this.queue.length > 0)
|
|
61
|
+
throw unavailable();
|
|
62
|
+
this.poisoned = false;
|
|
63
|
+
}
|
|
64
|
+
poison() {
|
|
65
|
+
this.poisoned = true;
|
|
66
|
+
for (const waiter of this.queue.splice(0)) {
|
|
67
|
+
if (waiter.abort)
|
|
68
|
+
waiter.signal?.removeEventListener("abort", waiter.abort);
|
|
69
|
+
waiter.reject(unavailable());
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
grant(waiter) {
|
|
73
|
+
if (waiter.abort)
|
|
74
|
+
waiter.signal?.removeEventListener("abort", waiter.abort);
|
|
75
|
+
if (this.poisoned) {
|
|
76
|
+
waiter.reject(unavailable());
|
|
77
|
+
this.locked = false;
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
let released = false;
|
|
81
|
+
waiter.resolve(() => {
|
|
82
|
+
if (released)
|
|
83
|
+
return;
|
|
84
|
+
released = true;
|
|
85
|
+
const next = this.queue.shift();
|
|
86
|
+
if (next)
|
|
87
|
+
this.grant(next);
|
|
88
|
+
else
|
|
89
|
+
this.locked = false;
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
export const codexThreadResolutionCoordinator = new CodexThreadResolutionCoordinator();
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { type SessionStore } from "../session-store.js";
|
|
2
|
+
export interface CodexThreadPersistence {
|
|
3
|
+
markProvisional(id: string): void;
|
|
4
|
+
clear(id: string): void;
|
|
5
|
+
commit(id: string): void;
|
|
6
|
+
}
|
|
7
|
+
/** Creates the only persistence object accepted by the exact-thread resolver. */
|
|
8
|
+
export declare function createCodexThreadPersistence(store: SessionStore, roamSessionId: string): CodexThreadPersistence;
|
|
9
|
+
export declare function isCodexThreadPersistence(value: unknown): value is CodexThreadPersistence;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { isConcreteSessionStore } from "../session-store.js";
|
|
2
|
+
const capabilities = new WeakSet();
|
|
3
|
+
class StoreBoundCodexThreadPersistence {
|
|
4
|
+
store;
|
|
5
|
+
roamSessionId;
|
|
6
|
+
constructor(store, roamSessionId) {
|
|
7
|
+
this.store = store;
|
|
8
|
+
this.roamSessionId = roamSessionId;
|
|
9
|
+
}
|
|
10
|
+
markProvisional(id) {
|
|
11
|
+
this.requireCodexSession();
|
|
12
|
+
this.store.markProvisionalProviderSessionId(this.roamSessionId, id);
|
|
13
|
+
if (this.store.get(this.roamSessionId)?.providerSessionId !== undefined)
|
|
14
|
+
throw new Error("Persistence failed");
|
|
15
|
+
}
|
|
16
|
+
clear(id) {
|
|
17
|
+
this.requireCodexSession();
|
|
18
|
+
this.store.clearProvisionalProviderSessionId(this.roamSessionId, id);
|
|
19
|
+
if (this.store.get(this.roamSessionId)?.providerSessionId !== undefined)
|
|
20
|
+
throw new Error("Rollback failed");
|
|
21
|
+
}
|
|
22
|
+
commit(id) {
|
|
23
|
+
this.requireCodexSession();
|
|
24
|
+
this.store.commitProvisionalProviderSessionId(this.roamSessionId, id);
|
|
25
|
+
if (this.store.get(this.roamSessionId)?.providerSessionId !== id)
|
|
26
|
+
throw new Error("Persistence identity changed");
|
|
27
|
+
}
|
|
28
|
+
requireCodexSession() {
|
|
29
|
+
const session = this.store.get(this.roamSessionId);
|
|
30
|
+
if (!session || session.provider !== "codex")
|
|
31
|
+
throw new Error("Codex session is unavailable");
|
|
32
|
+
return session;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
/** Creates the only persistence object accepted by the exact-thread resolver. */
|
|
36
|
+
export function createCodexThreadPersistence(store, roamSessionId) {
|
|
37
|
+
if (!isConcreteSessionStore(store) || roamSessionId.length === 0)
|
|
38
|
+
throw new Error("Invalid Codex persistence");
|
|
39
|
+
const capability = new StoreBoundCodexThreadPersistence(store, roamSessionId);
|
|
40
|
+
capabilities.add(capability);
|
|
41
|
+
return capability;
|
|
42
|
+
}
|
|
43
|
+
export function isCodexThreadPersistence(value) {
|
|
44
|
+
return typeof value === "object" && value !== null && capabilities.has(value);
|
|
45
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { type ZodType } from "zod";
|
|
2
|
+
import { type CodexSpawnLease } from "./codex-thread-coordinator.js";
|
|
3
|
+
import { type CodexThreadPersistence } from "./codex-thread-persistence.js";
|
|
4
|
+
export interface CodexThreadInventoryEntry {
|
|
5
|
+
readonly id: string;
|
|
6
|
+
readonly cwd: string;
|
|
7
|
+
readonly source: "cli" | "vscode" | "exec" | "appServer" | "unknown" | Record<string, unknown>;
|
|
8
|
+
/** Current protocol uses Unix seconds; milliseconds are normalized defensively for compatible revisions. */
|
|
9
|
+
readonly createdAt: number;
|
|
10
|
+
}
|
|
11
|
+
export type ReadCodexThreadInventory = () => Promise<readonly CodexThreadInventoryEntry[]>;
|
|
12
|
+
export interface CodexThreadRpc {
|
|
13
|
+
request<T>(method: string, params: unknown, schema: ZodType<T>): Promise<T>;
|
|
14
|
+
}
|
|
15
|
+
export interface CreateCodexThreadInventoryOptions {
|
|
16
|
+
readonly cwd: string;
|
|
17
|
+
readonly maxPages?: number;
|
|
18
|
+
readonly maxItems?: number;
|
|
19
|
+
}
|
|
20
|
+
export interface CodexThreadResolverOptions {
|
|
21
|
+
readonly inventory: ReadCodexThreadInventory;
|
|
22
|
+
readonly now?: () => number;
|
|
23
|
+
readonly sleep?: (milliseconds: number, signal?: AbortSignal) => Promise<void>;
|
|
24
|
+
readonly deadlineMs?: number;
|
|
25
|
+
readonly pollIntervalMs?: number;
|
|
26
|
+
readonly maxInventoryItems?: number;
|
|
27
|
+
readonly creationSkewMs?: number;
|
|
28
|
+
readonly cancellationAckMs?: number;
|
|
29
|
+
}
|
|
30
|
+
export interface ResolveCodexThreadOptions {
|
|
31
|
+
readonly cwd: string;
|
|
32
|
+
/** The launch callback is intentionally inside the process-wide identity mutex. */
|
|
33
|
+
readonly spawn: (signal: AbortSignal) => CodexSpawnLease;
|
|
34
|
+
readonly persistence: CodexThreadPersistence;
|
|
35
|
+
readonly signal?: AbortSignal;
|
|
36
|
+
}
|
|
37
|
+
export declare function resetCodexThreadResolutionCoordinatorForTests(): void;
|
|
38
|
+
/** Shared assertion used by the provider's generated resume argv regression. */
|
|
39
|
+
export declare function assertExactCodexResumeArgs(args: readonly string[]): void;
|
|
40
|
+
/** Creates the bounded current-protocol `thread/list` reader used by resolver snapshots and polls. */
|
|
41
|
+
export declare function createCodexThreadInventory(rpc: CodexThreadRpc, options: CreateCodexThreadInventoryOptions): ReadCodexThreadInventory;
|
|
42
|
+
export declare class CodexThreadResolver {
|
|
43
|
+
private readonly inventory;
|
|
44
|
+
private readonly now;
|
|
45
|
+
private readonly sleep;
|
|
46
|
+
private readonly deadlineMs;
|
|
47
|
+
private readonly pollIntervalMs;
|
|
48
|
+
private readonly maxInventoryItems;
|
|
49
|
+
private readonly creationSkewMs;
|
|
50
|
+
private readonly cancellationAckMs;
|
|
51
|
+
private readonly maxPolls;
|
|
52
|
+
constructor(options: CodexThreadResolverOptions);
|
|
53
|
+
resolveAfterSpawn(options: ResolveCodexThreadOptions): Promise<string>;
|
|
54
|
+
private resolveLocked;
|
|
55
|
+
private readInventory;
|
|
56
|
+
private candidates;
|
|
57
|
+
private sameIdentity;
|
|
58
|
+
private throwIfCancelled;
|
|
59
|
+
}
|