@cocorograph/hub-agent 0.8.49 → 0.8.50
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/package.json
CHANGED
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import path from "node:path"
|
|
2
|
+
import { lstat } from "node:fs/promises"
|
|
3
|
+
|
|
4
|
+
import { resolveCodexWorkspaceDependencies } from "./codex-workspace-dependencies.mjs"
|
|
5
|
+
|
|
6
|
+
export const ARTIFACT_TEMPLATE_CATALOG_MAX_ITEMS = 100
|
|
7
|
+
export const ARTIFACT_TEMPLATE_CATALOG_MAX_BYTES = 24_000
|
|
8
|
+
export const ARTIFACT_TEMPLATE_DESCRIPTION_MAX_CODEPOINTS = 600
|
|
9
|
+
|
|
10
|
+
const TEMPLATE_NAME_RE = /^(?:[^:]+:)?artifact-template-[a-z0-9][a-z0-9._-]*$/
|
|
11
|
+
const PICKER_CONFIG_KEY = "mcp_servers.openai_artifact_template_picker"
|
|
12
|
+
const DARWIN_RESOURCES_PATH = "/Applications/ChatGPT.app/Contents/Resources"
|
|
13
|
+
|
|
14
|
+
export class CodexArtifactTemplatePickerError extends Error {
|
|
15
|
+
constructor(code, message, options = {}) {
|
|
16
|
+
super(message, options)
|
|
17
|
+
this.name = "CodexArtifactTemplatePickerError"
|
|
18
|
+
this.code = code
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function fail(code, message, cause) {
|
|
23
|
+
throw new CodexArtifactTemplatePickerError(code, message, cause ? { cause } : undefined)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function diagnostic(code, detail = {}) {
|
|
27
|
+
return { code, ...detail }
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function priorityOf(skillName) {
|
|
31
|
+
if (skillName.startsWith("openai-templates:")) return 2
|
|
32
|
+
return skillName.includes(":") ? 1 : 0
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function normalizedPathKey(skillPath, platform) {
|
|
36
|
+
// Desktop does not establish case-folding as part of its contract. Normalize separators and
|
|
37
|
+
// dot segments only, preserving case and the original path carried in the catalog.
|
|
38
|
+
return platform === "win32" ? path.win32.normalize(skillPath) : path.posix.normalize(skillPath)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function codepointPrefix(value, limit) {
|
|
42
|
+
return Array.from(value).slice(0, limit).join("")
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function optionalTrimmed(value) {
|
|
46
|
+
return typeof value === "string" ? value.trim() : ""
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function catalogCandidate(record) {
|
|
50
|
+
if (
|
|
51
|
+
!record ||
|
|
52
|
+
typeof record !== "object" ||
|
|
53
|
+
Array.isArray(record) ||
|
|
54
|
+
typeof record.name !== "string"
|
|
55
|
+
) {
|
|
56
|
+
return { error: diagnostic("ARTIFACT_TEMPLATE_SKILL_METADATA_INVALID") }
|
|
57
|
+
}
|
|
58
|
+
if (!TEMPLATE_NAME_RE.test(record.name)) return { unrelated: true }
|
|
59
|
+
if (typeof record.path !== "string" || typeof record.enabled !== "boolean") {
|
|
60
|
+
return { error: diagnostic("ARTIFACT_TEMPLATE_SKILL_METADATA_INVALID") }
|
|
61
|
+
}
|
|
62
|
+
if (record.enabled !== true) return { disabled: true }
|
|
63
|
+
// skills/list is authoritative. Do not reinterpret, normalize, or derive this SKILL.md path.
|
|
64
|
+
if (!path.isAbsolute(record.path)) {
|
|
65
|
+
return { error: diagnostic("ARTIFACT_TEMPLATE_SKILL_PATH_INVALID", { skillName: record.name }) }
|
|
66
|
+
}
|
|
67
|
+
const description = optionalTrimmed(record.description)
|
|
68
|
+
|| optionalTrimmed(record.interface?.shortDescription)
|
|
69
|
+
|| optionalTrimmed(record.interface?.displayName)
|
|
70
|
+
|| record.name
|
|
71
|
+
return {
|
|
72
|
+
candidate: {
|
|
73
|
+
skillName: record.name,
|
|
74
|
+
skillPath: record.path,
|
|
75
|
+
description: codepointPrefix(description, ARTIFACT_TEMPLATE_DESCRIPTION_MAX_CODEPOINTS),
|
|
76
|
+
priority: priorityOf(record.name),
|
|
77
|
+
},
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Convert app-server skills/list SkillMetadata records to the Desktop-compatible picker catalog.
|
|
83
|
+
* The caller supplies the exact response records; this module deliberately does not scan plugin
|
|
84
|
+
* caches, personal skill directories, or config.toml to second-guess Codex enablement.
|
|
85
|
+
*/
|
|
86
|
+
export function collectCodexArtifactTemplateCatalog({ skills = [], platform = process.platform } = {}) {
|
|
87
|
+
const diagnostics = []
|
|
88
|
+
if (!Array.isArray(skills)) {
|
|
89
|
+
return {
|
|
90
|
+
catalog: [],
|
|
91
|
+
diagnostics: [diagnostic("ARTIFACT_TEMPLATE_SKILLS_LIST_INVALID")],
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const rawCandidates = []
|
|
96
|
+
for (const record of skills) {
|
|
97
|
+
const result = catalogCandidate(record)
|
|
98
|
+
if (result.error) diagnostics.push(result.error)
|
|
99
|
+
else if (result.candidate) rawCandidates.push(result.candidate)
|
|
100
|
+
}
|
|
101
|
+
rawCandidates.sort((a, b) => b.priority - a.priority)
|
|
102
|
+
|
|
103
|
+
const catalog = []
|
|
104
|
+
let catalogBytes = 2 // JSON array brackets: []
|
|
105
|
+
const names = new Set()
|
|
106
|
+
const paths = new Set()
|
|
107
|
+
for (const { priority: _priority, ...candidate } of rawCandidates) {
|
|
108
|
+
const pathKey = normalizedPathKey(candidate.skillPath, platform)
|
|
109
|
+
if (names.has(candidate.skillName) || paths.has(pathKey)) {
|
|
110
|
+
diagnostics.push(diagnostic("ARTIFACT_TEMPLATE_SKILL_DUPLICATE", {
|
|
111
|
+
skillName: candidate.skillName,
|
|
112
|
+
skillPath: candidate.skillPath,
|
|
113
|
+
}))
|
|
114
|
+
continue
|
|
115
|
+
}
|
|
116
|
+
if (catalog.length >= ARTIFACT_TEMPLATE_CATALOG_MAX_ITEMS) {
|
|
117
|
+
diagnostics.push(diagnostic("ARTIFACT_TEMPLATE_CATALOG_ITEM_LIMIT", { skillName: candidate.skillName }))
|
|
118
|
+
continue
|
|
119
|
+
}
|
|
120
|
+
const candidateBytes = Buffer.byteLength(JSON.stringify(candidate), "utf8")
|
|
121
|
+
const nextBytes = catalogBytes + (catalog.length > 0 ? 1 : 0) + candidateBytes
|
|
122
|
+
if (nextBytes > ARTIFACT_TEMPLATE_CATALOG_MAX_BYTES) {
|
|
123
|
+
diagnostics.push(diagnostic("ARTIFACT_TEMPLATE_CATALOG_BYTE_LIMIT", { skillName: candidate.skillName }))
|
|
124
|
+
continue
|
|
125
|
+
}
|
|
126
|
+
catalog.push(candidate)
|
|
127
|
+
catalogBytes = nextBytes
|
|
128
|
+
names.add(candidate.skillName)
|
|
129
|
+
paths.add(pathKey)
|
|
130
|
+
}
|
|
131
|
+
return { catalog, diagnostics }
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
async function isFile(candidate, lstatImpl) {
|
|
135
|
+
try {
|
|
136
|
+
const value = await lstatImpl(candidate)
|
|
137
|
+
return value.isFile() && !value.isSymbolicLink()
|
|
138
|
+
} catch {
|
|
139
|
+
return false
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
async function resolveResourcesPath({ env, platform, lstatImpl }) {
|
|
144
|
+
const explicit = env.CODEX_ELECTRON_RESOURCES_PATH
|
|
145
|
+
if (explicit) {
|
|
146
|
+
if (!path.isAbsolute(explicit)) {
|
|
147
|
+
fail("CODEX_ARTIFACT_RESOURCES_PATH_INVALID", "CODEX_ELECTRON_RESOURCES_PATH must be absolute")
|
|
148
|
+
}
|
|
149
|
+
return path.normalize(explicit)
|
|
150
|
+
}
|
|
151
|
+
if (platform === "darwin") {
|
|
152
|
+
const server = path.join(DARWIN_RESOURCES_PATH, "artifact-template-picker", "server.mjs")
|
|
153
|
+
if (await isFile(server, lstatImpl)) return DARWIN_RESOURCES_PATH
|
|
154
|
+
}
|
|
155
|
+
fail("CODEX_ARTIFACT_RESOURCES_PATH_UNAVAILABLE", "Codex Electron resources path is unavailable")
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export async function preflightCodexArtifactTemplatePicker({
|
|
159
|
+
env = process.env,
|
|
160
|
+
platform = process.platform,
|
|
161
|
+
lstatImpl = lstat,
|
|
162
|
+
} = {}) {
|
|
163
|
+
const resourcesPath = await resolveResourcesPath({ env, platform, lstatImpl })
|
|
164
|
+
const serverPath = path.join(resourcesPath, "artifact-template-picker", "server.mjs")
|
|
165
|
+
if (!await isFile(serverPath, lstatImpl)) {
|
|
166
|
+
fail("CODEX_ARTIFACT_TEMPLATE_PICKER_MISSING", "Codex artifact template picker server is missing")
|
|
167
|
+
}
|
|
168
|
+
return { resourcesPath, serverPath }
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/** Return the dotted thread/start config shape consumed by Codex app-server. */
|
|
172
|
+
export async function buildCodexArtifactTemplatePickerConfig({
|
|
173
|
+
env = process.env,
|
|
174
|
+
platform = process.platform,
|
|
175
|
+
skills = [],
|
|
176
|
+
resolveWorkspaceDependencies = resolveCodexWorkspaceDependencies,
|
|
177
|
+
lstatImpl = lstat,
|
|
178
|
+
preflight,
|
|
179
|
+
onDiagnostic,
|
|
180
|
+
} = {}) {
|
|
181
|
+
const verified = preflight || await preflightCodexArtifactTemplatePicker({ env, platform, lstatImpl })
|
|
182
|
+
const { serverPath } = verified
|
|
183
|
+
const runtime = await resolveWorkspaceDependencies()
|
|
184
|
+
if (typeof runtime?.nodePath !== "string" || !path.isAbsolute(runtime.nodePath)) {
|
|
185
|
+
fail("CODEX_ARTIFACT_RUNTIME_NODE_INVALID", "Codex primary runtime did not return an absolute nodePath")
|
|
186
|
+
}
|
|
187
|
+
const { catalog, diagnostics } = collectCodexArtifactTemplateCatalog({ skills, platform })
|
|
188
|
+
for (const item of diagnostics) onDiagnostic?.(item)
|
|
189
|
+
return {
|
|
190
|
+
[PICKER_CONFIG_KEY]: {
|
|
191
|
+
command: runtime.nodePath,
|
|
192
|
+
args: [serverPath],
|
|
193
|
+
env: {
|
|
194
|
+
CODEX_ARTIFACT_TEMPLATE_SKILLS: JSON.stringify(catalog),
|
|
195
|
+
},
|
|
196
|
+
},
|
|
197
|
+
}
|
|
198
|
+
}
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import path from "node:path"
|
|
2
|
+
|
|
3
|
+
import { resolveCodexWorkspaceDependencies } from "./codex-workspace-dependencies.mjs"
|
|
4
|
+
|
|
5
|
+
const NAMESPACE = "codex_app"
|
|
6
|
+
const LOAD_WORKSPACE_DEPENDENCIES = "load_workspace_dependencies"
|
|
7
|
+
const COMPLETED_CALL_LEDGER_MAX = 256
|
|
8
|
+
|
|
9
|
+
const EMPTY_OBJECT_SCHEMA = Object.freeze({
|
|
10
|
+
type: "object",
|
|
11
|
+
properties: {},
|
|
12
|
+
additionalProperties: false,
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
export const CODEX_DYNAMIC_TOOLS = Object.freeze([
|
|
16
|
+
Object.freeze({
|
|
17
|
+
type: "namespace",
|
|
18
|
+
name: NAMESPACE,
|
|
19
|
+
description: "Codex standard runtime tools provided by the Cockpit host.",
|
|
20
|
+
tools: Object.freeze([
|
|
21
|
+
Object.freeze({
|
|
22
|
+
type: "function",
|
|
23
|
+
name: LOAD_WORKSPACE_DEPENDENCIES,
|
|
24
|
+
description: "Resolve the verified Codex primary runtime dependencies for artifact skills.",
|
|
25
|
+
inputSchema: EMPTY_OBJECT_SCHEMA,
|
|
26
|
+
}),
|
|
27
|
+
]),
|
|
28
|
+
}),
|
|
29
|
+
])
|
|
30
|
+
|
|
31
|
+
function inputText(text) {
|
|
32
|
+
return { contentItems: [{ type: "inputText", text }], success: true }
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const REQUIRED_INVENTORY_FIELDS = Object.freeze([
|
|
36
|
+
["bundleVersion", "Bundle version", false],
|
|
37
|
+
["gitPath", "Git executable", true],
|
|
38
|
+
["nodePath", "Node.js executable", false],
|
|
39
|
+
["nodeModulesPath", "Node.js packages", false],
|
|
40
|
+
["pnpmPath", "pnpm executable", true],
|
|
41
|
+
["pythonPath", "Python executable", false],
|
|
42
|
+
["pythonLibrariesPath", "Python packages", false],
|
|
43
|
+
["overrideBinPath", "Override binaries", false],
|
|
44
|
+
["fallbackBinPath", "Fallback binaries", false],
|
|
45
|
+
])
|
|
46
|
+
|
|
47
|
+
function invalidInventory() {
|
|
48
|
+
const error = new Error("Codex workspace dependency inventory is invalid")
|
|
49
|
+
error.code = "CODEX_DYNAMIC_TOOL_INVENTORY_INVALID"
|
|
50
|
+
throw error
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function inventoryValue(inventory, field, optional) {
|
|
54
|
+
const value = inventory[field]
|
|
55
|
+
if (optional && (value === null || value === undefined)) return null
|
|
56
|
+
if (!isNonEmptyString(value) || /[\r\n`]/.test(value)) invalidInventory()
|
|
57
|
+
if (field !== "bundleVersion" && !path.isAbsolute(value)) invalidInventory()
|
|
58
|
+
return value
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function inlineCode(value) {
|
|
62
|
+
return `\`${value}\``
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Format only the canonical resolver inventory; compatibility aliases are never substituted. */
|
|
66
|
+
export function formatCodexWorkspaceDependencies(inventory) {
|
|
67
|
+
if (!inventory || typeof inventory !== "object" || Array.isArray(inventory)) invalidInventory()
|
|
68
|
+
const lines = [
|
|
69
|
+
"Workspace dependencies are available for this local desktop thread.",
|
|
70
|
+
"",
|
|
71
|
+
"### Workspace Dependencies",
|
|
72
|
+
"",
|
|
73
|
+
"Use these bundled paths for sheets, slides, documents, PDFs, images, or browser automation:",
|
|
74
|
+
"",
|
|
75
|
+
]
|
|
76
|
+
for (const [field, label, optional] of REQUIRED_INVENTORY_FIELDS) {
|
|
77
|
+
const value = inventoryValue(inventory, field, optional)
|
|
78
|
+
if (value !== null) lines.push(`- ${label}: ${inlineCode(value)}`)
|
|
79
|
+
}
|
|
80
|
+
return lines.join("\n")
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function failure(text) {
|
|
84
|
+
return { contentItems: [{ type: "inputText", text }], success: false }
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function codexDynamicToolFailure(code) {
|
|
88
|
+
const safeCode = sanitizeCodexDynamicErrorCode(code)
|
|
89
|
+
return failure(`Dynamic tool call failed (${safeCode})`)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function sanitizeCodexDynamicErrorCode(value, fallback = "CODEX_DYNAMIC_TOOL_ERROR") {
|
|
93
|
+
return typeof value === "string" && value.length <= 128 && /^[A-Z0-9_]+$/.test(value)
|
|
94
|
+
? value
|
|
95
|
+
: fallback
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function sanitizeCodexDynamicToolName(value) {
|
|
99
|
+
return typeof value === "string"
|
|
100
|
+
&& value.length >= 1
|
|
101
|
+
&& value.length <= 128
|
|
102
|
+
&& /^[A-Za-z0-9_.:-]+$/.test(value)
|
|
103
|
+
? value
|
|
104
|
+
: "unknown"
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function isNonEmptyString(value) {
|
|
108
|
+
return typeof value === "string" && value.trim().length > 0
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function validateCall(params) {
|
|
112
|
+
if (!params || typeof params !== "object" || Array.isArray(params)) {
|
|
113
|
+
return { ok: false, code: "INVALID_CALL", tool: "unknown" }
|
|
114
|
+
}
|
|
115
|
+
const tool = sanitizeCodexDynamicToolName(params.tool)
|
|
116
|
+
if (
|
|
117
|
+
!isNonEmptyString(params.threadId) ||
|
|
118
|
+
!isNonEmptyString(params.callId) ||
|
|
119
|
+
!isNonEmptyString(params.turnId)
|
|
120
|
+
) {
|
|
121
|
+
return { ok: false, code: "INVALID_CALL", tool }
|
|
122
|
+
}
|
|
123
|
+
if (params.namespace !== NAMESPACE) {
|
|
124
|
+
return { ok: false, code: "UNKNOWN_NAMESPACE", tool }
|
|
125
|
+
}
|
|
126
|
+
if (params.tool !== LOAD_WORKSPACE_DEPENDENCIES) {
|
|
127
|
+
return { ok: false, code: "UNKNOWN_TOOL", tool }
|
|
128
|
+
}
|
|
129
|
+
if (
|
|
130
|
+
!params.arguments ||
|
|
131
|
+
typeof params.arguments !== "object" ||
|
|
132
|
+
Array.isArray(params.arguments) ||
|
|
133
|
+
Object.keys(params.arguments).length !== 0
|
|
134
|
+
) {
|
|
135
|
+
return { ok: false, code: "INVALID_ARGUMENTS", tool }
|
|
136
|
+
}
|
|
137
|
+
return { ok: true, tool }
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* A per-session executor for host-provided Codex dynamic tools.
|
|
142
|
+
* Recent results are retained in a bounded replay window so transport retries cannot execute a
|
|
143
|
+
* resolver twice without allowing an unbounded session-lifetime ledger.
|
|
144
|
+
*/
|
|
145
|
+
export function createCodexDynamicToolExecutor({
|
|
146
|
+
resolveWorkspaceDependencies = resolveCodexWorkspaceDependencies,
|
|
147
|
+
logger,
|
|
148
|
+
} = {}) {
|
|
149
|
+
const calls = new Map()
|
|
150
|
+
|
|
151
|
+
const executeOnce = async (params) => {
|
|
152
|
+
const validation = validateCall(params)
|
|
153
|
+
if (!validation.ok) {
|
|
154
|
+
logger?.warn?.(
|
|
155
|
+
{ tool: validation.tool, error_code: validation.code },
|
|
156
|
+
"codex dynamic tool call rejected",
|
|
157
|
+
)
|
|
158
|
+
return failure(`Dynamic tool call rejected (${validation.code})`)
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
try {
|
|
162
|
+
const dependencies = await resolveWorkspaceDependencies()
|
|
163
|
+
return inputText(formatCodexWorkspaceDependencies(dependencies))
|
|
164
|
+
} catch (error) {
|
|
165
|
+
const code = sanitizeCodexDynamicErrorCode(error?.code)
|
|
166
|
+
logger?.warn?.(
|
|
167
|
+
{ tool: validation.tool, error_code: code },
|
|
168
|
+
"codex dynamic tool call failed",
|
|
169
|
+
)
|
|
170
|
+
return failure(`Workspace dependency resolution failed (${code})`)
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return {
|
|
175
|
+
definitions: CODEX_DYNAMIC_TOOLS,
|
|
176
|
+
execute(params) {
|
|
177
|
+
const threadId = isNonEmptyString(params?.threadId) ? params.threadId : null
|
|
178
|
+
const callId = isNonEmptyString(params?.callId) ? params.callId : null
|
|
179
|
+
if (!threadId || !callId) return executeOnce(params)
|
|
180
|
+
const key = JSON.stringify([threadId, callId])
|
|
181
|
+
const existing = calls.get(key)
|
|
182
|
+
if (existing) return existing
|
|
183
|
+
const pending = executeOnce(params)
|
|
184
|
+
calls.set(key, pending)
|
|
185
|
+
if (calls.size > COMPLETED_CALL_LEDGER_MAX) {
|
|
186
|
+
calls.delete(calls.keys().next().value)
|
|
187
|
+
}
|
|
188
|
+
return pending
|
|
189
|
+
},
|
|
190
|
+
}
|
|
191
|
+
}
|
|
@@ -47,6 +47,17 @@ import { recordInjectSourcePersistent } from "./inject-source-store.mjs"
|
|
|
47
47
|
|
|
48
48
|
import { AsyncTtlCache } from "./async-ttl-cache.mjs"
|
|
49
49
|
import { CodexAppServerClient } from "./codex-appserver-client.mjs"
|
|
50
|
+
import {
|
|
51
|
+
buildCodexArtifactTemplatePickerConfig,
|
|
52
|
+
preflightCodexArtifactTemplatePicker,
|
|
53
|
+
} from "./codex-artifact-template-picker.mjs"
|
|
54
|
+
import {
|
|
55
|
+
codexDynamicToolFailure,
|
|
56
|
+
createCodexDynamicToolExecutor,
|
|
57
|
+
sanitizeCodexDynamicErrorCode,
|
|
58
|
+
sanitizeCodexDynamicToolName,
|
|
59
|
+
} from "./codex-dynamic-tools.mjs"
|
|
60
|
+
import { resolveCodexWorkspaceDependencies } from "./codex-workspace-dependencies.mjs"
|
|
50
61
|
import {
|
|
51
62
|
snapshotCodexDeliveryAudit,
|
|
52
63
|
verifyCodexDeliveryAudit,
|
|
@@ -61,6 +72,120 @@ function parseTurnMismatch(reason) {
|
|
|
61
72
|
return mismatch ? { expected: mismatch[1], found: mismatch[2] } : null
|
|
62
73
|
}
|
|
63
74
|
|
|
75
|
+
function codexHostCapabilityErrorCode(error, fallback) {
|
|
76
|
+
return sanitizeCodexDynamicErrorCode(error?.code, fallback)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function flattenCodexSkillsList(result) {
|
|
80
|
+
if (!result || !Array.isArray(result.data)) {
|
|
81
|
+
const error = new Error("invalid skills/list response")
|
|
82
|
+
error.code = "CODEX_SKILLS_LIST_RESPONSE_INVALID"
|
|
83
|
+
throw error
|
|
84
|
+
}
|
|
85
|
+
const skills = []
|
|
86
|
+
for (const entry of result.data) {
|
|
87
|
+
if (!entry || !Array.isArray(entry.skills)) {
|
|
88
|
+
const error = new Error("invalid skills/list response")
|
|
89
|
+
error.code = "CODEX_SKILLS_LIST_RESPONSE_INVALID"
|
|
90
|
+
throw error
|
|
91
|
+
}
|
|
92
|
+
// SkillMetadata は Codex が正本。未知 field を選別・再構築せず、そのまま picker へ渡す。
|
|
93
|
+
skills.push(...entry.skills)
|
|
94
|
+
}
|
|
95
|
+
return skills
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function deadlineError(code) {
|
|
99
|
+
const error = new Error(code)
|
|
100
|
+
error.code = code
|
|
101
|
+
return error
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async function withDeadline(promise, timeoutMs, code) {
|
|
105
|
+
let timer
|
|
106
|
+
const timeout = new Promise((_, reject) => {
|
|
107
|
+
timer = setTimeout(() => reject(deadlineError(code)), timeoutMs)
|
|
108
|
+
timer.unref?.()
|
|
109
|
+
})
|
|
110
|
+
try {
|
|
111
|
+
return await Promise.race([Promise.resolve(promise), timeout])
|
|
112
|
+
} finally {
|
|
113
|
+
clearTimeout(timer)
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
async function buildCodexHostCapabilities({
|
|
118
|
+
client,
|
|
119
|
+
cwd,
|
|
120
|
+
artifactTemplatePickerConfigBuilder,
|
|
121
|
+
artifactTemplatePickerPreflight,
|
|
122
|
+
resolveWorkspaceDependencies,
|
|
123
|
+
}) {
|
|
124
|
+
let preflight
|
|
125
|
+
try {
|
|
126
|
+
preflight = await artifactTemplatePickerPreflight()
|
|
127
|
+
} catch (error) {
|
|
128
|
+
return {
|
|
129
|
+
config: null,
|
|
130
|
+
warning: {
|
|
131
|
+
error_code: codexHostCapabilityErrorCode(
|
|
132
|
+
error,
|
|
133
|
+
"CODEX_ARTIFACT_TEMPLATE_PICKER_PREFLIGHT_FAILED",
|
|
134
|
+
),
|
|
135
|
+
skill_count: 0,
|
|
136
|
+
},
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
let skills
|
|
140
|
+
try {
|
|
141
|
+
skills = flattenCodexSkillsList(await client.request("skills/list", { cwds: [cwd] }))
|
|
142
|
+
} catch (error) {
|
|
143
|
+
return {
|
|
144
|
+
config: null,
|
|
145
|
+
warning: {
|
|
146
|
+
error_code: codexHostCapabilityErrorCode(error, "CODEX_SKILLS_LIST_FAILED"),
|
|
147
|
+
skill_count: 0,
|
|
148
|
+
},
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const diagnosticCounts = new Map()
|
|
153
|
+
try {
|
|
154
|
+
const config = await artifactTemplatePickerConfigBuilder({
|
|
155
|
+
skills,
|
|
156
|
+
preflight,
|
|
157
|
+
resolveWorkspaceDependencies,
|
|
158
|
+
onDiagnostic: (item) => {
|
|
159
|
+
const code = codexHostCapabilityErrorCode(
|
|
160
|
+
item,
|
|
161
|
+
"CODEX_ARTIFACT_TEMPLATE_PICKER_DIAGNOSTIC",
|
|
162
|
+
)
|
|
163
|
+
diagnosticCounts.set(code, (diagnosticCounts.get(code) || 0) + 1)
|
|
164
|
+
},
|
|
165
|
+
})
|
|
166
|
+
return {
|
|
167
|
+
config: config && typeof config === "object" && !Array.isArray(config) ? config : null,
|
|
168
|
+
warning: diagnosticCounts.size > 0
|
|
169
|
+
? { diagnostic_counts: Object.fromEntries(diagnosticCounts), skill_count: skills.length }
|
|
170
|
+
: null,
|
|
171
|
+
}
|
|
172
|
+
} catch (error) {
|
|
173
|
+
return {
|
|
174
|
+
config: null,
|
|
175
|
+
warning: {
|
|
176
|
+
error_code: codexHostCapabilityErrorCode(
|
|
177
|
+
error,
|
|
178
|
+
"CODEX_ARTIFACT_TEMPLATE_PICKER_CONFIG_FAILED",
|
|
179
|
+
),
|
|
180
|
+
...(diagnosticCounts.size > 0
|
|
181
|
+
? { diagnostic_counts: Object.fromEntries(diagnosticCounts) }
|
|
182
|
+
: {}),
|
|
183
|
+
skill_count: skills.length,
|
|
184
|
+
},
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
64
189
|
/**
|
|
65
190
|
* sibling-inject (2026-07-04) 用: spawn する codex app-server プロセス自身の
|
|
66
191
|
* 環境変数に、この cwd の capability トークンを乗せる。ディスクには書かず、
|
|
@@ -209,6 +334,24 @@ const DELIVERY_ECHO_TIMEOUT_MS = readPositiveIntEnv(
|
|
|
209
334
|
)
|
|
210
335
|
|
|
211
336
|
const CLIENT_INFO = { name: "cockpit-hub-agent", title: "Cockpit", version: "1.0.0" }
|
|
337
|
+
const WORKSPACE_DEPENDENCIES_CACHE_TTL_MS = 30_000
|
|
338
|
+
const WORKSPACE_DEPENDENCIES_FAILURE_TTL_MS = readPositiveIntEnv(
|
|
339
|
+
"HUB_AGENT_CODEX_WORKSPACE_DEPENDENCIES_FAILURE_TTL_MS",
|
|
340
|
+
30_000,
|
|
341
|
+
)
|
|
342
|
+
const WORKSPACE_DEPENDENCIES_DEADLINE_MS = readPositiveIntEnv(
|
|
343
|
+
"HUB_AGENT_CODEX_WORKSPACE_DEPENDENCIES_DEADLINE_MS",
|
|
344
|
+
45_000,
|
|
345
|
+
)
|
|
346
|
+
const HOST_CAPABILITIES_DEADLINE_MS = readPositiveIntEnv(
|
|
347
|
+
"HUB_AGENT_CODEX_HOST_CAPABILITIES_DEADLINE_MS",
|
|
348
|
+
10_000,
|
|
349
|
+
)
|
|
350
|
+
const FORM_ELICITATION_TIMEOUT_MS = readPositiveIntEnv(
|
|
351
|
+
"HUB_AGENT_CODEX_FORM_ELICITATION_TIMEOUT_MS",
|
|
352
|
+
10 * 60_000,
|
|
353
|
+
)
|
|
354
|
+
const DYNAMIC_TOOL_REQUEST_LEDGER_MAX = 256
|
|
212
355
|
|
|
213
356
|
// T06499 (2026-07-18): 巨大スレッドの resume ガード。長期運用したスレッドの rollout は
|
|
214
357
|
// 20MB 超まで肥大し得る (実測: メイン repo 540KB が正常なのに対し 20MB のスレッドあり)。
|
|
@@ -466,11 +609,80 @@ export class CodexStreamBridge extends EventEmitter {
|
|
|
466
609
|
historyMaxBytes,
|
|
467
610
|
listThreadsCacheTtlMs,
|
|
468
611
|
accountRateLimitsCacheTtlMs,
|
|
612
|
+
workspaceDependenciesCacheTtlMs,
|
|
613
|
+
workspaceDependenciesFailureTtlMs,
|
|
614
|
+
workspaceDependenciesDeadlineMs,
|
|
615
|
+
hostCapabilitiesDeadlineMs,
|
|
616
|
+
formElicitationTimeoutMs,
|
|
617
|
+
resolveWorkspaceDependencies,
|
|
618
|
+
dynamicToolExecutorFactory,
|
|
619
|
+
artifactTemplatePickerConfigBuilder,
|
|
620
|
+
artifactTemplatePickerPreflight,
|
|
469
621
|
} = {}) {
|
|
470
622
|
super()
|
|
471
623
|
this.codexCmd = codexCmd
|
|
472
624
|
this.logger = logger
|
|
473
625
|
this._clientFactory = clientFactory || ((opts) => new CodexAppServerClient(opts))
|
|
626
|
+
const uncachedWorkspaceDependencies = resolveWorkspaceDependencies || resolveCodexWorkspaceDependencies
|
|
627
|
+
this._workspaceDependenciesCacheTtlMs = sanitizePositiveInt(
|
|
628
|
+
workspaceDependenciesCacheTtlMs,
|
|
629
|
+
WORKSPACE_DEPENDENCIES_CACHE_TTL_MS,
|
|
630
|
+
{ logger: this.logger, label: "codex.workspaceDependenciesCacheTtlMs" },
|
|
631
|
+
)
|
|
632
|
+
this._workspaceDependenciesFailureTtlMs = sanitizePositiveInt(
|
|
633
|
+
workspaceDependenciesFailureTtlMs,
|
|
634
|
+
WORKSPACE_DEPENDENCIES_FAILURE_TTL_MS,
|
|
635
|
+
{ logger: this.logger, label: "codex.workspaceDependenciesFailureTtlMs" },
|
|
636
|
+
)
|
|
637
|
+
this._workspaceDependenciesDeadlineMs = sanitizePositiveInt(
|
|
638
|
+
workspaceDependenciesDeadlineMs,
|
|
639
|
+
WORKSPACE_DEPENDENCIES_DEADLINE_MS,
|
|
640
|
+
{ logger: this.logger, label: "codex.workspaceDependenciesDeadlineMs" },
|
|
641
|
+
)
|
|
642
|
+
this._hostCapabilitiesDeadlineMs = sanitizePositiveInt(
|
|
643
|
+
hostCapabilitiesDeadlineMs,
|
|
644
|
+
HOST_CAPABILITIES_DEADLINE_MS,
|
|
645
|
+
{ logger: this.logger, label: "codex.hostCapabilitiesDeadlineMs" },
|
|
646
|
+
)
|
|
647
|
+
this._formElicitationTimeoutMs = sanitizePositiveInt(
|
|
648
|
+
formElicitationTimeoutMs,
|
|
649
|
+
FORM_ELICITATION_TIMEOUT_MS,
|
|
650
|
+
{ logger: this.logger, label: "codex.formElicitationTimeoutMs" },
|
|
651
|
+
)
|
|
652
|
+
this._workspaceDependenciesCache = new AsyncTtlCache({
|
|
653
|
+
ttlMs: this._workspaceDependenciesCacheTtlMs,
|
|
654
|
+
})
|
|
655
|
+
this._workspaceDependenciesFailure = null
|
|
656
|
+
this._resolveWorkspaceDependencies = async () => {
|
|
657
|
+
const cachedFailure = this._workspaceDependenciesFailure
|
|
658
|
+
if (cachedFailure && cachedFailure.expiresAt > Date.now()) {
|
|
659
|
+
throw deadlineError(cachedFailure.code)
|
|
660
|
+
}
|
|
661
|
+
this._workspaceDependenciesFailure = null
|
|
662
|
+
try {
|
|
663
|
+
const dependencies = await this._workspaceDependenciesCache.get(
|
|
664
|
+
() => withDeadline(
|
|
665
|
+
Promise.resolve().then(uncachedWorkspaceDependencies),
|
|
666
|
+
this._workspaceDependenciesDeadlineMs,
|
|
667
|
+
"CODEX_WORKSPACE_DEPENDENCIES_TIMEOUT",
|
|
668
|
+
),
|
|
669
|
+
)
|
|
670
|
+
this._workspaceDependenciesFailure = null
|
|
671
|
+
return dependencies
|
|
672
|
+
} catch (error) {
|
|
673
|
+
// Negative cacheには安全なcodeと期限だけを保持する。cause/messageは秘密を含み得る。
|
|
674
|
+
this._workspaceDependenciesFailure = {
|
|
675
|
+
code: codexHostCapabilityErrorCode(error, "CODEX_WORKSPACE_DEPENDENCIES_FAILED"),
|
|
676
|
+
expiresAt: Date.now() + this._workspaceDependenciesFailureTtlMs,
|
|
677
|
+
}
|
|
678
|
+
throw deadlineError(this._workspaceDependenciesFailure.code)
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
this._dynamicToolExecutorFactory = dynamicToolExecutorFactory || createCodexDynamicToolExecutor
|
|
682
|
+
this._artifactTemplatePickerConfigBuilder = artifactTemplatePickerConfigBuilder
|
|
683
|
+
|| buildCodexArtifactTemplatePickerConfig
|
|
684
|
+
this._artifactTemplatePickerPreflight = artifactTemplatePickerPreflight
|
|
685
|
+
|| preflightCodexArtifactTemplatePicker
|
|
474
686
|
// Stage 5: warm 保持の idle TTL(既定 7 日)。テストから短縮できるよう注入可能。
|
|
475
687
|
this._detachTtlMs = detachTtlMs ?? IDLE_DETACH_TTL_MS
|
|
476
688
|
// 子⑤ (T06544): 同時 warm 保持する cwd 数の上限 N(既定 20)。各 cwd は K 本に畳む。
|
|
@@ -741,6 +953,14 @@ export class CodexStreamBridge extends EventEmitter {
|
|
|
741
953
|
// request_id -> { turnId, method, params }。major#1: reattach で新 stream へ未応答の承認要求を
|
|
742
954
|
// 再送できるよう、元の method/params も保持する (Claude の _permissionResolvers と同型)。
|
|
743
955
|
pendingApprovals: new Map(),
|
|
956
|
+
// host dynamic tool は permission request と別経路で即時応答する。
|
|
957
|
+
dynamicToolExecutor: this._dynamicToolExecutorFactory({
|
|
958
|
+
resolveWorkspaceDependencies: this._resolveWorkspaceDependencies,
|
|
959
|
+
logger: this.logger,
|
|
960
|
+
}),
|
|
961
|
+
// JSON-RPC request id ごとの exactly-once 応答台帳。callId の resolver single-flight は
|
|
962
|
+
// executor が別に担い、同じ callId でも request id が違えば各 request へ応答する。
|
|
963
|
+
handledDynamicToolRequestIds: new Set(),
|
|
744
964
|
// ブラウザから codex.detach を受け取った (= 画面の持ち主が居なくなった) か。診断・ログ用。
|
|
745
965
|
// Stage 5: 回収の可否は detachRequested ではなく lastActivityMs(LRU)と activeTurnId
|
|
746
966
|
// (busy/stuck)で決める。detach してもすぐには殺さず warm 保持する。
|
|
@@ -827,7 +1047,13 @@ export class CodexStreamBridge extends EventEmitter {
|
|
|
827
1047
|
{ stream_id, cwd, resuming, lifecycle_stage: "initialize_started" },
|
|
828
1048
|
"codex app-server session initialization",
|
|
829
1049
|
)
|
|
830
|
-
await client.request("initialize", {
|
|
1050
|
+
await client.request("initialize", {
|
|
1051
|
+
clientInfo: CLIENT_INFO,
|
|
1052
|
+
capabilities: {
|
|
1053
|
+
experimentalApi: true,
|
|
1054
|
+
mcpServerOpenaiFormElicitation: true,
|
|
1055
|
+
},
|
|
1056
|
+
})
|
|
831
1057
|
// ここまでが「app-server プロセスが起動して JSON-RPC に応答できるようになるまで」。
|
|
832
1058
|
timing.initialize_ms = lap()
|
|
833
1059
|
// 公式 App Server handshake: initialize 応答後、同じ transport 上で initialized を
|
|
@@ -837,10 +1063,43 @@ export class CodexStreamBridge extends EventEmitter {
|
|
|
837
1063
|
{ stream_id, cwd, resuming, lifecycle_stage: "initialized_sent" },
|
|
838
1064
|
"codex app-server session initialization",
|
|
839
1065
|
)
|
|
840
|
-
reachedThreadOp = true
|
|
841
1066
|
const startParams = { cwd, model: model || undefined, sandbox: sandbox || undefined }
|
|
842
1067
|
if (effort) startParams.effort = effort
|
|
843
1068
|
if (approvalPolicy) startParams.approvalPolicy = approvalPolicy
|
|
1069
|
+
// app-serverを跨ぐcold resumeではthreadのhost側MCP/dynamic tool登録が復元されない実測が
|
|
1070
|
+
// あるため、fresh startと同じcapabilityを毎回再注入する。in-memory reattachはこのcold経路へ
|
|
1071
|
+
// 入らないので、既存process上での再buildは発生しない。
|
|
1072
|
+
try {
|
|
1073
|
+
const hostCapabilities = await withDeadline(
|
|
1074
|
+
buildCodexHostCapabilities({
|
|
1075
|
+
client,
|
|
1076
|
+
cwd,
|
|
1077
|
+
artifactTemplatePickerConfigBuilder: this._artifactTemplatePickerConfigBuilder,
|
|
1078
|
+
artifactTemplatePickerPreflight: this._artifactTemplatePickerPreflight,
|
|
1079
|
+
resolveWorkspaceDependencies: this._resolveWorkspaceDependencies,
|
|
1080
|
+
}),
|
|
1081
|
+
this._hostCapabilitiesDeadlineMs,
|
|
1082
|
+
"CODEX_HOST_CAPABILITIES_TIMEOUT",
|
|
1083
|
+
)
|
|
1084
|
+
if (hostCapabilities.config) startParams.config = { ...hostCapabilities.config }
|
|
1085
|
+
if (hostCapabilities.warning) {
|
|
1086
|
+
this.logger?.warn?.(
|
|
1087
|
+
hostCapabilities.warning,
|
|
1088
|
+
"codex artifact template picker degraded",
|
|
1089
|
+
)
|
|
1090
|
+
}
|
|
1091
|
+
} catch (error) {
|
|
1092
|
+
this.logger?.warn?.(
|
|
1093
|
+
{
|
|
1094
|
+
error_code: codexHostCapabilityErrorCode(error, "CODEX_HOST_CAPABILITIES_FAILED"),
|
|
1095
|
+
skill_count: 0,
|
|
1096
|
+
},
|
|
1097
|
+
"codex artifact template picker degraded",
|
|
1098
|
+
)
|
|
1099
|
+
}
|
|
1100
|
+
startParams.dynamicTools = session.dynamicToolExecutor.definitions
|
|
1101
|
+
startParams.runtimeWorkspaceRoots = [cwd]
|
|
1102
|
+
timing.host_capabilities_ms = lap()
|
|
844
1103
|
this.logger?.info?.(
|
|
845
1104
|
{
|
|
846
1105
|
stream_id,
|
|
@@ -851,6 +1110,7 @@ export class CodexStreamBridge extends EventEmitter {
|
|
|
851
1110
|
},
|
|
852
1111
|
"codex app-server session initialization",
|
|
853
1112
|
)
|
|
1113
|
+
reachedThreadOp = true
|
|
854
1114
|
const result = resuming
|
|
855
1115
|
? await client.request("thread/resume", { ...startParams, threadId: resumeThreadId })
|
|
856
1116
|
: await client.request("thread/start", startParams)
|
|
@@ -951,6 +1211,7 @@ export class CodexStreamBridge extends EventEmitter {
|
|
|
951
1211
|
}
|
|
952
1212
|
} catch (err) {
|
|
953
1213
|
this.sessions.delete(stream_id)
|
|
1214
|
+
this._settleAllPendingApprovals(session, { respond: false })
|
|
954
1215
|
client.stop()
|
|
955
1216
|
settleReady(null) // reattach 待ちへ「失敗」を通知 (reject しない)
|
|
956
1217
|
// 失敗も 1 行に落とす。**_classifyAttachFailure より前**に出す (分類は rollout の disk 走査を
|
|
@@ -987,6 +1248,7 @@ export class CodexStreamBridge extends EventEmitter {
|
|
|
987
1248
|
* ## 読み方
|
|
988
1249
|
* - `warm=true` / `mode=reattach_*` … app-server を立てていない (所要は `reattach_wait_ms` だけ)
|
|
989
1250
|
* - `warm=false` / `mode=cold_*` … `spawn_ms`(同期 spawn) + `initialize_ms`(プロセス起動待ち)
|
|
1251
|
+
* + `host_capabilities_ms`(cold start/resume の skills/runtime 構築)
|
|
990
1252
|
* + `thread_op_ms`(thread/start|resume。resume は rollout 読み込み込み) + `history_flatten_ms`
|
|
991
1253
|
*
|
|
992
1254
|
* ログ量: attach 時のみ (ターン毎には出ない)。既存の "codex session attached" と同オーダー。
|
|
@@ -1000,6 +1262,7 @@ export class CodexStreamBridge extends EventEmitter {
|
|
|
1000
1262
|
reattach_wait_ms: timing.reattach_wait_ms ?? null,
|
|
1001
1263
|
spawn_ms: timing.spawn_ms ?? null,
|
|
1002
1264
|
initialize_ms: timing.initialize_ms ?? null,
|
|
1265
|
+
host_capabilities_ms: timing.host_capabilities_ms ?? null,
|
|
1003
1266
|
thread_op_ms: timing.thread_op_ms ?? null,
|
|
1004
1267
|
history_flatten_ms: timing.history_flatten_ms ?? null,
|
|
1005
1268
|
},
|
|
@@ -1249,9 +1512,22 @@ export class CodexStreamBridge extends EventEmitter {
|
|
|
1249
1512
|
/** app-server を 1 本 spawn して account/rateLimits/read を取る実処理 (キャッシュ miss 時のみ)。 */
|
|
1250
1513
|
async _fetchAccountRateLimits({ cwd, profileId }) {
|
|
1251
1514
|
const client = this._clientFactory({ cwd: cwd || process.cwd(), codexCmd: this.codexCmd, logger: this.logger })
|
|
1515
|
+
client.on("error", (error) => {
|
|
1516
|
+
this.logger?.warn?.(
|
|
1517
|
+
{
|
|
1518
|
+
error_code: codexHostCapabilityErrorCode(
|
|
1519
|
+
error,
|
|
1520
|
+
"CODEX_ACCOUNT_RATE_LIMITS_CLIENT_ERROR",
|
|
1521
|
+
),
|
|
1522
|
+
},
|
|
1523
|
+
"codex account rate limits client error",
|
|
1524
|
+
)
|
|
1525
|
+
})
|
|
1252
1526
|
client.start()
|
|
1253
1527
|
try {
|
|
1254
|
-
await client.request("initialize", {
|
|
1528
|
+
await client.request("initialize", {
|
|
1529
|
+
clientInfo: CLIENT_INFO,
|
|
1530
|
+
})
|
|
1255
1531
|
client.notify("initialized", {})
|
|
1256
1532
|
const result = await client.request("account/rateLimits/read", {})
|
|
1257
1533
|
this._accountRateLimitsByProfile.set(profileId, result)
|
|
@@ -2540,13 +2816,27 @@ export class CodexStreamBridge extends EventEmitter {
|
|
|
2540
2816
|
}
|
|
2541
2817
|
|
|
2542
2818
|
_onServerRequest(session, msg) {
|
|
2819
|
+
if (msg?.method === "item/tool/call") {
|
|
2820
|
+
this._onDynamicToolCall(session, msg)
|
|
2821
|
+
return
|
|
2822
|
+
}
|
|
2543
2823
|
const { stream_id } = session
|
|
2824
|
+
const previous = session.pendingApprovals.get(msg.id)
|
|
2825
|
+
if (previous?.timer) clearTimeout(previous.timer)
|
|
2544
2826
|
// major#1: reattach 時に新 stream へ再送できるよう method/params も保持する。
|
|
2545
|
-
|
|
2827
|
+
const pending = {
|
|
2546
2828
|
turnId: session.activeTurnId,
|
|
2547
2829
|
method: msg.method,
|
|
2548
2830
|
params: msg.params,
|
|
2549
|
-
|
|
2831
|
+
timer: null,
|
|
2832
|
+
}
|
|
2833
|
+
if (msg.method === "mcpServer/elicitation/request") {
|
|
2834
|
+
pending.timer = setTimeout(() => {
|
|
2835
|
+
this._expireFormElicitation(session, msg.id, pending)
|
|
2836
|
+
}, this._formElicitationTimeoutMs)
|
|
2837
|
+
pending.timer.unref?.()
|
|
2838
|
+
}
|
|
2839
|
+
session.pendingApprovals.set(msg.id, pending)
|
|
2550
2840
|
this.emit("permission", {
|
|
2551
2841
|
stream_id,
|
|
2552
2842
|
thread_id: session.threadId,
|
|
@@ -2557,6 +2847,49 @@ export class CodexStreamBridge extends EventEmitter {
|
|
|
2557
2847
|
})
|
|
2558
2848
|
}
|
|
2559
2849
|
|
|
2850
|
+
_onDynamicToolCall(session, msg) {
|
|
2851
|
+
if (session.handledDynamicToolRequestIds.has(msg?.id)) return
|
|
2852
|
+
session.handledDynamicToolRequestIds.add(msg?.id)
|
|
2853
|
+
if (session.handledDynamicToolRequestIds.size > DYNAMIC_TOOL_REQUEST_LEDGER_MAX) {
|
|
2854
|
+
session.handledDynamicToolRequestIds.delete(
|
|
2855
|
+
session.handledDynamicToolRequestIds.values().next().value,
|
|
2856
|
+
)
|
|
2857
|
+
}
|
|
2858
|
+
|
|
2859
|
+
const tool = sanitizeCodexDynamicToolName(msg?.params?.tool)
|
|
2860
|
+
let execution
|
|
2861
|
+
const requestThreadId = msg?.params?.threadId
|
|
2862
|
+
const allowedThread = requestThreadId === session.threadId
|
|
2863
|
+
// 表示台帳と停止台帳は観測経路・寿命が異なる。どちらか一方で既知なら、このsessionが
|
|
2864
|
+
// 所有するsubagent threadとして許可する意図的なOR結合(積集合へ狭めない)。
|
|
2865
|
+
|| session.subagents.has(requestThreadId)
|
|
2866
|
+
|| session.subagentInterruptRegistry.has(requestThreadId)
|
|
2867
|
+
if (!allowedThread) {
|
|
2868
|
+
this.logger?.warn?.(
|
|
2869
|
+
{ tool, error_code: "THREAD_MISMATCH" },
|
|
2870
|
+
"codex dynamic tool call rejected",
|
|
2871
|
+
)
|
|
2872
|
+
execution = Promise.resolve(codexDynamicToolFailure("THREAD_MISMATCH"))
|
|
2873
|
+
} else {
|
|
2874
|
+
execution = Promise.resolve().then(() => session.dynamicToolExecutor.execute(msg?.params))
|
|
2875
|
+
}
|
|
2876
|
+
|
|
2877
|
+
void execution.catch(() => {
|
|
2878
|
+
this.logger?.warn?.(
|
|
2879
|
+
{ tool, error_code: "DYNAMIC_TOOL_EXECUTOR_REJECTED" },
|
|
2880
|
+
"codex dynamic tool call failed",
|
|
2881
|
+
)
|
|
2882
|
+
return codexDynamicToolFailure("DYNAMIC_TOOL_EXECUTOR_REJECTED")
|
|
2883
|
+
}).then((result) => {
|
|
2884
|
+
session.client.respond(msg.id, result)
|
|
2885
|
+
}).catch(() => {
|
|
2886
|
+
this.logger?.warn?.(
|
|
2887
|
+
{ tool, error_code: "DYNAMIC_TOOL_RESPONSE_FAILED" },
|
|
2888
|
+
"codex dynamic tool response failed",
|
|
2889
|
+
)
|
|
2890
|
+
})
|
|
2891
|
+
}
|
|
2892
|
+
|
|
2560
2893
|
/**
|
|
2561
2894
|
* major#1: 未応答の承認要求を現在の stream へ再送する (reattach から呼ぶ)。Claude の
|
|
2562
2895
|
* _replayPendingPermissions と同型。frontend は request_id で重複排除するため、既に表示済みの
|
|
@@ -2576,14 +2909,63 @@ export class CodexStreamBridge extends EventEmitter {
|
|
|
2576
2909
|
}
|
|
2577
2910
|
}
|
|
2578
2911
|
|
|
2912
|
+
_pendingApprovalResult(info) {
|
|
2913
|
+
return info?.method === "mcpServer/elicitation/request"
|
|
2914
|
+
? { action: "cancel", content: null }
|
|
2915
|
+
: { decision: "decline" }
|
|
2916
|
+
}
|
|
2917
|
+
|
|
2918
|
+
_settlePendingApproval(session, requestId, result, { respond = true } = {}) {
|
|
2919
|
+
const info = session?.pendingApprovals?.get(requestId)
|
|
2920
|
+
if (!info) return false
|
|
2921
|
+
session.pendingApprovals.delete(requestId)
|
|
2922
|
+
if (info.timer) clearTimeout(info.timer)
|
|
2923
|
+
if (respond) session.client.respond(requestId, result)
|
|
2924
|
+
return true
|
|
2925
|
+
}
|
|
2926
|
+
|
|
2927
|
+
_settleAllPendingApprovals(session, { respond = true } = {}) {
|
|
2928
|
+
for (const [requestId, info] of session?.pendingApprovals || []) {
|
|
2929
|
+
this._settlePendingApproval(
|
|
2930
|
+
session,
|
|
2931
|
+
requestId,
|
|
2932
|
+
this._pendingApprovalResult(info),
|
|
2933
|
+
{ respond },
|
|
2934
|
+
)
|
|
2935
|
+
}
|
|
2936
|
+
}
|
|
2937
|
+
|
|
2938
|
+
_expireFormElicitation(session, requestId, expected) {
|
|
2939
|
+
if (session?.pendingApprovals?.get(requestId) !== expected) return
|
|
2940
|
+
if (!this._settlePendingApproval(
|
|
2941
|
+
session,
|
|
2942
|
+
requestId,
|
|
2943
|
+
{ action: "cancel", content: null },
|
|
2944
|
+
)) return
|
|
2945
|
+
this.emit("event", {
|
|
2946
|
+
stream_id: session.stream_id,
|
|
2947
|
+
producer_seq: session.seq,
|
|
2948
|
+
thread_id: session.threadId,
|
|
2949
|
+
cwd: session.cwd,
|
|
2950
|
+
notification: {
|
|
2951
|
+
method: "cockpit/permission_expired",
|
|
2952
|
+
params: { request_id: requestId, method: expected.method },
|
|
2953
|
+
},
|
|
2954
|
+
})
|
|
2955
|
+
}
|
|
2956
|
+
|
|
2579
2957
|
// ターン完了時、そのターンに紐づく未解決の承認要求は自動 decline する
|
|
2580
2958
|
// (browser が応答する前にターンが打ち切られた/クラッシュ相当のケース。
|
|
2581
2959
|
// claude-stream-bridge.mjs の「ターン終了時に未解決分は自動 deny」と同じ設計)。
|
|
2582
2960
|
_autoResolveStalePendingApprovals(session, completedTurnId) {
|
|
2583
2961
|
for (const [requestId, info] of session.pendingApprovals) {
|
|
2584
|
-
|
|
2585
|
-
|
|
2586
|
-
|
|
2962
|
+
const belongsToCompletedTurn = info.turnId !== null && info.turnId === completedTurnId
|
|
2963
|
+
if (!belongsToCompletedTurn) continue
|
|
2964
|
+
this._settlePendingApproval(
|
|
2965
|
+
session,
|
|
2966
|
+
requestId,
|
|
2967
|
+
this._pendingApprovalResult(info),
|
|
2968
|
+
)
|
|
2587
2969
|
}
|
|
2588
2970
|
}
|
|
2589
2971
|
|
|
@@ -2595,6 +2977,7 @@ export class CodexStreamBridge extends EventEmitter {
|
|
|
2595
2977
|
this._settleTurnActiveWaiter(session)
|
|
2596
2978
|
this._drainRejectQueue(session, { reason: "queue_dead" })
|
|
2597
2979
|
this._settlePendingInjectDeliveries(session, "session_dead")
|
|
2980
|
+
this._settleAllPendingApprovals(session, { respond: false })
|
|
2598
2981
|
// 案B: app-server が死んだら全購読 stream_id キーを Map から外す(1 端末分だけでは
|
|
2599
2982
|
// 他の購読キーが dangling で残る)。
|
|
2600
2983
|
this._dropAllStreamKeys(session)
|
|
@@ -2947,8 +3330,7 @@ export class CodexStreamBridge extends EventEmitter {
|
|
|
2947
3330
|
this.logger?.warn?.({ stream_id, request_id }, "codex permissionReply: unknown request_id (ignored)")
|
|
2948
3331
|
return
|
|
2949
3332
|
}
|
|
2950
|
-
|
|
2951
|
-
session.client.respond(request_id, result)
|
|
3333
|
+
this._settlePendingApproval(session, request_id, result)
|
|
2952
3334
|
}
|
|
2953
3335
|
|
|
2954
3336
|
/**
|
|
@@ -3629,6 +4011,7 @@ export class CodexStreamBridge extends EventEmitter {
|
|
|
3629
4011
|
this._settleTurnActiveWaiter(session)
|
|
3630
4012
|
this._drainRejectQueue(session, { reason: "queue_dead" })
|
|
3631
4013
|
this._settlePendingInjectDeliveries(session, "session_dead")
|
|
4014
|
+
this._settleAllPendingApprovals(session)
|
|
3632
4015
|
// 案B: 全購読 stream_id キーを Map から外す(primary 1 個だけ消すと dangling が残る)。
|
|
3633
4016
|
this._dropAllStreamKeys(session)
|
|
3634
4017
|
try {
|
|
@@ -3817,6 +4200,7 @@ export class CodexStreamBridge extends EventEmitter {
|
|
|
3817
4200
|
this._settleTurnActiveWaiter(session)
|
|
3818
4201
|
this._drainRejectQueue(session, { reason: "queue_dead" })
|
|
3819
4202
|
this._settlePendingInjectDeliveries(session, "session_dead")
|
|
4203
|
+
this._settleAllPendingApprovals(session)
|
|
3820
4204
|
// hub-agent 再起動時にターン実行中のセッションを落とすと thread が汚染されうる
|
|
3821
4205
|
// (exec 中の kill → output 欠落)。因果を後から辿れるよう必ず記録する。
|
|
3822
4206
|
this.logger?.[session.activeTurnId ? "warn" : "info"]?.(
|
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
import { constants as fsConstants } from "node:fs"
|
|
2
|
+
import { access, readFile, realpath, stat } from "node:fs/promises"
|
|
3
|
+
import { execFile } from "node:child_process"
|
|
4
|
+
import { promisify } from "node:util"
|
|
5
|
+
import os from "node:os"
|
|
6
|
+
import path from "node:path"
|
|
7
|
+
|
|
8
|
+
export const CODEX_PRIMARY_RUNTIME_BUNDLE_FORMAT = 2
|
|
9
|
+
const execFileAsync = promisify(execFile)
|
|
10
|
+
|
|
11
|
+
export class CodexWorkspaceDependenciesError extends Error {
|
|
12
|
+
constructor(code, message, options = {}) {
|
|
13
|
+
super(message, options)
|
|
14
|
+
this.name = "CodexWorkspaceDependenciesError"
|
|
15
|
+
this.code = code
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function fail(code, message, cause) {
|
|
20
|
+
throw new CodexWorkspaceDependenciesError(code, message, cause ? { cause } : undefined)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function resolveRuntimeRoot(runtimeRootOverride, homedirImpl) {
|
|
24
|
+
if (runtimeRootOverride !== undefined) {
|
|
25
|
+
if (typeof runtimeRootOverride !== "string" || !path.isAbsolute(runtimeRootOverride)) {
|
|
26
|
+
fail("CODEX_RUNTIME_ROOT_INVALID", "runtimeRootOverride must be an absolute path")
|
|
27
|
+
}
|
|
28
|
+
return runtimeRootOverride
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const home = homedirImpl()
|
|
32
|
+
if (typeof home !== "string" || !path.isAbsolute(home)) {
|
|
33
|
+
fail("CODEX_RUNTIME_ROOT_INVALID", "OS home directory must be an absolute path")
|
|
34
|
+
}
|
|
35
|
+
return path.join(home, ".cache", "codex-runtimes", "codex-primary-runtime")
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function requireNonemptyString(manifest, field, { optional = false } = {}) {
|
|
39
|
+
const value = manifest[field]
|
|
40
|
+
if (optional && value === undefined) return null
|
|
41
|
+
if (typeof value !== "string" || value.trim() === "") {
|
|
42
|
+
fail("CODEX_RUNTIME_MANIFEST_INVALID", `Codex primary runtime manifest field ${field} must be a nonempty string`)
|
|
43
|
+
}
|
|
44
|
+
return value
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function requireNativeDependencies(manifest) {
|
|
48
|
+
if (manifest.nativeDependencies === undefined) return []
|
|
49
|
+
if (
|
|
50
|
+
!Array.isArray(manifest.nativeDependencies)
|
|
51
|
+
|| manifest.nativeDependencies.some((item) => typeof item !== "string" || item.trim() === "")
|
|
52
|
+
) {
|
|
53
|
+
fail(
|
|
54
|
+
"CODEX_RUNTIME_MANIFEST_INVALID",
|
|
55
|
+
"Codex primary runtime manifest field nativeDependencies must be an array of nonempty strings",
|
|
56
|
+
)
|
|
57
|
+
}
|
|
58
|
+
return manifest.nativeDependencies
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async function readManifest(manifestPath, readFileImpl) {
|
|
62
|
+
let raw
|
|
63
|
+
try {
|
|
64
|
+
raw = await readFileImpl(manifestPath, "utf8")
|
|
65
|
+
} catch (error) {
|
|
66
|
+
const code = error?.code === "ENOENT"
|
|
67
|
+
? "CODEX_RUNTIME_MANIFEST_MISSING"
|
|
68
|
+
: "CODEX_RUNTIME_MANIFEST_UNREADABLE"
|
|
69
|
+
fail(code, "Codex primary runtime manifest is not readable", error)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
try {
|
|
73
|
+
const manifest = JSON.parse(raw)
|
|
74
|
+
if (!manifest || typeof manifest !== "object" || Array.isArray(manifest)) {
|
|
75
|
+
fail("CODEX_RUNTIME_MANIFEST_INVALID", "Codex primary runtime manifest must be a JSON object")
|
|
76
|
+
}
|
|
77
|
+
return manifest
|
|
78
|
+
} catch (error) {
|
|
79
|
+
if (error instanceof CodexWorkspaceDependenciesError) throw error
|
|
80
|
+
fail("CODEX_RUNTIME_MANIFEST_INVALID_JSON", "Codex primary runtime manifest is not valid JSON", error)
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async function requireFile(candidate, label, statImpl) {
|
|
85
|
+
try {
|
|
86
|
+
if ((await statImpl(candidate)).isFile()) return
|
|
87
|
+
} catch (error) {
|
|
88
|
+
fail("CODEX_RUNTIME_PATH_MISSING", `${label} is missing or inaccessible`, error)
|
|
89
|
+
}
|
|
90
|
+
fail("CODEX_RUNTIME_PATH_INVALID", `${label} must be a file`)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async function requireDirectory(candidate, label, statImpl) {
|
|
94
|
+
try {
|
|
95
|
+
if ((await statImpl(candidate)).isDirectory()) return
|
|
96
|
+
} catch (error) {
|
|
97
|
+
fail("CODEX_RUNTIME_PATH_MISSING", `${label} is missing or inaccessible`, error)
|
|
98
|
+
}
|
|
99
|
+
fail("CODEX_RUNTIME_PATH_INVALID", `${label} must be a directory`)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async function requireExecutableFile(candidate, label, executableErrorCode, statImpl, accessImpl) {
|
|
103
|
+
await requireFile(candidate, label, statImpl)
|
|
104
|
+
try {
|
|
105
|
+
await accessImpl(candidate, fsConstants.X_OK)
|
|
106
|
+
} catch (error) {
|
|
107
|
+
fail(executableErrorCode, `${label} is not executable`, error)
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function isWithinRoot(candidate, root) {
|
|
112
|
+
const relative = path.relative(root, candidate)
|
|
113
|
+
return relative === "" || (!relative.startsWith(`..${path.sep}`) && relative !== ".." && !path.isAbsolute(relative))
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
async function requireContainedRealPath(candidate, label, realRuntimeRoot, realpathImpl) {
|
|
117
|
+
let resolved
|
|
118
|
+
try {
|
|
119
|
+
resolved = await realpathImpl(candidate)
|
|
120
|
+
} catch (error) {
|
|
121
|
+
fail("CODEX_RUNTIME_PATH_MISSING", `${label} is missing or inaccessible`, error)
|
|
122
|
+
}
|
|
123
|
+
if (typeof resolved !== "string" || !path.isAbsolute(resolved)) {
|
|
124
|
+
fail("CODEX_RUNTIME_PATH_INVALID", `${label} real path must be absolute`)
|
|
125
|
+
}
|
|
126
|
+
if (!isWithinRoot(resolved, realRuntimeRoot)) {
|
|
127
|
+
fail("CODEX_RUNTIME_PATH_OUTSIDE_ROOT", `${label} resolves outside the Codex primary runtime root`)
|
|
128
|
+
}
|
|
129
|
+
return resolved
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
async function requireContainedManifestPath(manifestPath, realRuntimeRoot, realpathImpl) {
|
|
133
|
+
let resolved
|
|
134
|
+
try {
|
|
135
|
+
resolved = await realpathImpl(manifestPath)
|
|
136
|
+
} catch (error) {
|
|
137
|
+
const code = error?.code === "ENOENT"
|
|
138
|
+
? "CODEX_RUNTIME_MANIFEST_MISSING"
|
|
139
|
+
: "CODEX_RUNTIME_MANIFEST_UNREADABLE"
|
|
140
|
+
fail(code, "Codex primary runtime manifest is not accessible", error)
|
|
141
|
+
}
|
|
142
|
+
if (typeof resolved !== "string" || !path.isAbsolute(resolved)) {
|
|
143
|
+
fail("CODEX_RUNTIME_PATH_INVALID", "Codex primary runtime manifest real path must be absolute")
|
|
144
|
+
}
|
|
145
|
+
if (!isWithinRoot(resolved, realRuntimeRoot)) {
|
|
146
|
+
fail(
|
|
147
|
+
"CODEX_RUNTIME_PATH_OUTSIDE_ROOT",
|
|
148
|
+
"Codex primary runtime manifest resolves outside the Codex primary runtime root",
|
|
149
|
+
)
|
|
150
|
+
}
|
|
151
|
+
return resolved
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function childEnvWithoutExternalLanguagePaths(env) {
|
|
155
|
+
const childEnv = { ...env }
|
|
156
|
+
delete childEnv.NODE_OPTIONS
|
|
157
|
+
delete childEnv.NODE_PATH
|
|
158
|
+
delete childEnv.PYTHONHOME
|
|
159
|
+
delete childEnv.PYTHONPATH
|
|
160
|
+
return childEnv
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Codex standard primary runtime の絶対パスを検証して返す。
|
|
165
|
+
*
|
|
166
|
+
* runtime.json 以外から版や配置を推測せず、ローカル npm・system Node・PATH へ
|
|
167
|
+
* fallback しない。runtimeRoot 自体のsymlinkは追従し、そのrealpathをcontainment rootとする。
|
|
168
|
+
* host側は成功・失敗をそれぞれ短期cacheするが、cache miss時はここで実体を再検証する。
|
|
169
|
+
*/
|
|
170
|
+
export async function resolveCodexWorkspaceDependencies({
|
|
171
|
+
env = process.env,
|
|
172
|
+
platform = process.platform,
|
|
173
|
+
arch = process.arch,
|
|
174
|
+
runtimeRootOverride,
|
|
175
|
+
homedirImpl = os.homedir,
|
|
176
|
+
readFileImpl = readFile,
|
|
177
|
+
realpathImpl = realpath,
|
|
178
|
+
statImpl = stat,
|
|
179
|
+
accessImpl = access,
|
|
180
|
+
execFileImpl = execFileAsync,
|
|
181
|
+
} = {}) {
|
|
182
|
+
const runtimeRoot = resolveRuntimeRoot(runtimeRootOverride, homedirImpl)
|
|
183
|
+
const manifestPath = path.join(runtimeRoot, "runtime.json")
|
|
184
|
+
let realRuntimeRoot
|
|
185
|
+
try {
|
|
186
|
+
realRuntimeRoot = await realpathImpl(runtimeRoot)
|
|
187
|
+
} catch (error) {
|
|
188
|
+
fail("CODEX_RUNTIME_ROOT_UNREADABLE", "Codex primary runtime root is not accessible", error)
|
|
189
|
+
}
|
|
190
|
+
if (!path.isAbsolute(realRuntimeRoot)) {
|
|
191
|
+
fail("CODEX_RUNTIME_ROOT_INVALID", "Codex primary runtime real path must be absolute")
|
|
192
|
+
}
|
|
193
|
+
// manifestの内容を読む前に、symlinkを含む実体がruntime root内にあることを確認する。
|
|
194
|
+
await requireContainedManifestPath(manifestPath, realRuntimeRoot, realpathImpl)
|
|
195
|
+
const manifest = await readManifest(manifestPath, readFileImpl)
|
|
196
|
+
|
|
197
|
+
if (manifest.bundleFormatVersion !== CODEX_PRIMARY_RUNTIME_BUNDLE_FORMAT) {
|
|
198
|
+
fail(
|
|
199
|
+
"CODEX_RUNTIME_BUNDLE_FORMAT_UNSUPPORTED",
|
|
200
|
+
`Unsupported Codex primary runtime bundle format: ${String(manifest.bundleFormatVersion)}`,
|
|
201
|
+
)
|
|
202
|
+
}
|
|
203
|
+
if (manifest.targetPlatform !== platform) {
|
|
204
|
+
fail("CODEX_RUNTIME_PLATFORM_MISMATCH", "Codex primary runtime target platform does not match this process")
|
|
205
|
+
}
|
|
206
|
+
if (manifest.targetArch !== arch) {
|
|
207
|
+
fail("CODEX_RUNTIME_ARCH_MISMATCH", "Codex primary runtime target architecture does not match this process")
|
|
208
|
+
}
|
|
209
|
+
if (platform === "win32") {
|
|
210
|
+
fail(
|
|
211
|
+
"CODEX_RUNTIME_PLATFORM_UNSUPPORTED",
|
|
212
|
+
"Codex primary runtime layout for Windows is not supported by this resolver",
|
|
213
|
+
)
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const bundleVersion = requireNonemptyString(manifest, "bundleVersion")
|
|
217
|
+
requireNonemptyString(manifest, "nodeVersion")
|
|
218
|
+
requireNonemptyString(manifest, "pythonVersion")
|
|
219
|
+
requireNonemptyString(manifest, "artifactToolVersion")
|
|
220
|
+
const pnpmVersion = requireNonemptyString(manifest, "pnpmVersion", { optional: true })
|
|
221
|
+
const nativeDependencies = requireNativeDependencies(manifest)
|
|
222
|
+
|
|
223
|
+
const runtimeNode = path.join(runtimeRoot, "dependencies", "node", "bin", "node")
|
|
224
|
+
const runtimeNodeModules = path.join(runtimeRoot, "dependencies", "node", "node_modules")
|
|
225
|
+
const runtimeBinDir = path.join(runtimeRoot, "dependencies", "bin", "override")
|
|
226
|
+
const runtimePython = path.join(runtimeRoot, "dependencies", "python", "bin", "python3")
|
|
227
|
+
const runtimePythonPackages = path.join(runtimeRoot, "dependencies", "python")
|
|
228
|
+
const runtimeFallbackBinDir = path.join(runtimeRoot, "dependencies", "bin", "fallback")
|
|
229
|
+
const runtimePnpm = pnpmVersion ? path.join(runtimeFallbackBinDir, "pnpm") : null
|
|
230
|
+
const runtimeGit = nativeDependencies.includes("git") ? path.join(runtimeFallbackBinDir, "git") : null
|
|
231
|
+
|
|
232
|
+
for (const [candidate, label] of [
|
|
233
|
+
[runtimeNode, "Bundled runtime Node"],
|
|
234
|
+
[runtimeNodeModules, "Bundled runtime node_modules"],
|
|
235
|
+
[runtimeBinDir, "Bundled runtime binary directory"],
|
|
236
|
+
[runtimePython, "Bundled runtime Python"],
|
|
237
|
+
[runtimePythonPackages, "Bundled runtime Python packages"],
|
|
238
|
+
[runtimeFallbackBinDir, "Bundled runtime fallback binary directory"],
|
|
239
|
+
[runtimePnpm, "Bundled runtime pnpm"],
|
|
240
|
+
[runtimeGit, "Bundled runtime git"],
|
|
241
|
+
]) {
|
|
242
|
+
if (candidate) await requireContainedRealPath(candidate, label, realRuntimeRoot, realpathImpl)
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
await requireExecutableFile(
|
|
246
|
+
runtimeNode,
|
|
247
|
+
"Bundled runtime Node",
|
|
248
|
+
"CODEX_RUNTIME_NODE_NOT_EXECUTABLE",
|
|
249
|
+
statImpl,
|
|
250
|
+
accessImpl,
|
|
251
|
+
)
|
|
252
|
+
await requireDirectory(runtimeNodeModules, "Bundled runtime node_modules", statImpl)
|
|
253
|
+
await requireDirectory(runtimeBinDir, "Bundled runtime binary directory", statImpl)
|
|
254
|
+
await requireExecutableFile(
|
|
255
|
+
runtimePython,
|
|
256
|
+
"Bundled runtime Python",
|
|
257
|
+
"CODEX_RUNTIME_PYTHON_NOT_EXECUTABLE",
|
|
258
|
+
statImpl,
|
|
259
|
+
accessImpl,
|
|
260
|
+
)
|
|
261
|
+
await requireDirectory(runtimePythonPackages, "Bundled runtime Python packages", statImpl)
|
|
262
|
+
await requireDirectory(runtimeFallbackBinDir, "Bundled runtime fallback binary directory", statImpl)
|
|
263
|
+
if (runtimePnpm) {
|
|
264
|
+
await requireExecutableFile(
|
|
265
|
+
runtimePnpm,
|
|
266
|
+
"Bundled runtime pnpm",
|
|
267
|
+
"CODEX_RUNTIME_PNPM_NOT_EXECUTABLE",
|
|
268
|
+
statImpl,
|
|
269
|
+
accessImpl,
|
|
270
|
+
)
|
|
271
|
+
}
|
|
272
|
+
if (runtimeGit) {
|
|
273
|
+
await requireExecutableFile(
|
|
274
|
+
runtimeGit,
|
|
275
|
+
"Bundled runtime git",
|
|
276
|
+
"CODEX_RUNTIME_GIT_NOT_EXECUTABLE",
|
|
277
|
+
statImpl,
|
|
278
|
+
accessImpl,
|
|
279
|
+
)
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
const runtimeChildEnv = childEnvWithoutExternalLanguagePaths(env)
|
|
283
|
+
|
|
284
|
+
try {
|
|
285
|
+
await execFileImpl(
|
|
286
|
+
runtimeNode,
|
|
287
|
+
["--input-type=module", "--eval", "await import('@oai/artifact-tool')"],
|
|
288
|
+
{
|
|
289
|
+
cwd: path.dirname(runtimeNodeModules),
|
|
290
|
+
env: runtimeChildEnv,
|
|
291
|
+
encoding: "utf8",
|
|
292
|
+
timeout: 15_000,
|
|
293
|
+
maxBuffer: 64 * 1024,
|
|
294
|
+
killSignal: "SIGKILL",
|
|
295
|
+
windowsHide: true,
|
|
296
|
+
},
|
|
297
|
+
)
|
|
298
|
+
} catch (error) {
|
|
299
|
+
fail("CODEX_RUNTIME_ARTIFACT_TOOL_UNAVAILABLE", "Bundled @oai/artifact-tool could not be imported", error)
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
try {
|
|
303
|
+
await execFileImpl(
|
|
304
|
+
runtimePython,
|
|
305
|
+
["-I", "-c", "import docx"],
|
|
306
|
+
{
|
|
307
|
+
cwd: runtimePythonPackages,
|
|
308
|
+
env: runtimeChildEnv,
|
|
309
|
+
encoding: "utf8",
|
|
310
|
+
timeout: 15_000,
|
|
311
|
+
maxBuffer: 64 * 1024,
|
|
312
|
+
killSignal: "SIGKILL",
|
|
313
|
+
windowsHide: true,
|
|
314
|
+
},
|
|
315
|
+
)
|
|
316
|
+
} catch (error) {
|
|
317
|
+
fail("CODEX_RUNTIME_DOCUMENTS_PYTHON_UNAVAILABLE", "Bundled Python could not import docx", error)
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
return {
|
|
321
|
+
bundleVersion,
|
|
322
|
+
gitPath: runtimeGit,
|
|
323
|
+
nodePath: runtimeNode,
|
|
324
|
+
nodeModulesPath: runtimeNodeModules,
|
|
325
|
+
pnpmPath: runtimePnpm,
|
|
326
|
+
pythonPath: runtimePython,
|
|
327
|
+
pythonLibrariesPath: runtimePythonPackages,
|
|
328
|
+
overrideBinPath: runtimeBinDir,
|
|
329
|
+
fallbackBinPath: runtimeFallbackBinDir,
|
|
330
|
+
}
|
|
331
|
+
}
|