@pushary/codex-app-server 0.0.0-stage → 0.1.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.
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts","../src/handler.ts","../src/protocol.ts","../src/presentation.ts","../src/partner-decisions.ts","../src/request-lifecycle.ts"],"sourcesContent":["import { createAdapterKernel } from '@pushary/server/adapters'\nexport { createPusharyCodexHandler } from './handler'\nexport type { CodexRequestId, CodexReply, CodexDispatchResult, CodexReviewSubject, PusharyCodexConfig, PusharyCodexEvent, PusharyCodexHandler } from './types'\nexport const connect = createAdapterKernel('Codex app-server').connect\n","import { randomUUID } from 'node:crypto'\nimport { createPusharyServer, isApproved, type CreateDecision } from '@pushary/server'\nimport { decisionFingerprint } from '@pushary/server/adapters'\nimport { COMMAND, FILES, QUESTIONS, certifiedThread, fileChangesOf, identityOf, isId, isRecord, isText, subjectOf } from './protocol'\nimport { presentSubject, type PhoneQuestion } from './presentation'\nimport { decisionFailureReason, decisionSnapshot, delay, isRetryable, withRetry, type DecisionSnapshot } from './partner-decisions'\nimport { transitionRequest, type RequestCompletion, type RequestState } from './request-lifecycle'\nimport type { CodexDispatchResult, CodexReviewSubject, FileChange, PusharyCodexConfig, PusharyCodexEvent, PusharyCodexHandler, RequestIdentity } from './types'\n\nconst MAX_PENDING = 8\nconst MAX_THREADS = 32\nconst MAX_ITEMS = 128\nconst MAX_CACHE_BYTES = 4 * 1024 * 1024\nconst MAX_REQUESTS = 4096\nconst CLEANUP_MS = 5000\nconst MIN_QUESTION_BUDGET_MS = 10_000\ninterface OwnedRequest {\n readonly identity: RequestIdentity\n readonly declineDecision: 'decline' | 'cancel'\n readonly digest: string\n readonly envelopeDigest: string\n readonly subject: CodexReviewSubject\n readonly controller: AbortController\n readonly deadline: number\n readonly timer: ReturnType<typeof setTimeout>\n readonly decisionIds: Set<string>\n readonly cleanupIds: Set<string>\n state: RequestState\n cleanupDeadline: number\n}\ninterface CachedPatch { readonly changes: readonly FileChange[] | null; readonly digest: string; readonly bytes: number }\nconst itemKey = (threadId: string, turnId: string, itemId: string): string => JSON.stringify([threadId, turnId, itemId])\nconst turnKey = (threadId: string, turnId: string): string => JSON.stringify([threadId, turnId])\nconst rpcKey = (id: string | number): string => JSON.stringify([typeof id, id])\n\nexport const createPusharyCodexHandler = (configuration: PusharyCodexConfig): PusharyCodexHandler => {\n const config = Object.freeze({ ...configuration })\n const timeoutMs = config.timeoutMs ?? 300_000\n if (config.codexVersion !== '0.160.1' || !Number.isInteger(timeoutMs) || timeoutMs < 60_000 || timeoutMs > 600_000 || !config.tenantId?.trim() || !config.externalId?.trim()) throw new TypeError('Invalid Codex adapter configuration')\n if ([config.sendResponse, config.requestTurnInterrupt, config.onEvent, config.maySendSubject].some(callback => typeof callback !== 'function')) throw new TypeError('All host callbacks are required')\n const decisions = createPusharyServer({ apiKey: config.apiKey, baseUrl: config.baseUrl, requestTimeoutMs: 10_000 }).decisions\n const generation = randomUUID()\n const threads = new Set<string>()\n const requests = new Map<string, OwnedRequest>()\n const closedRequestIds = new Set<string>()\n const turns = new Map<string, { readonly threadId: string; readonly turnId: string; terminal: boolean }>()\n const completedItems = new Set<string>()\n const patches = new Map<string, CachedPatch>()\n const jobs = new Set<Promise<void>>()\n const cleanupFailures: string[] = []\n const cleanupControllers = new Set<AbortController>()\n let cacheBytes = 0\n let disposed = false\n let disposal: Promise<{ readonly cleanupFailures: readonly string[] }> | undefined\n let disposalDeadline = Infinity\n const emit = (event: PusharyCodexEvent): void => {\n try { config.onEvent(event) } catch { console.error('Pushary Codex onEvent callback failed') }\n }\n const eventFor = (request: OwnedRequest, type: PusharyCodexEvent['type'], reason?: string, decisionId?: string): void => emit({ type, requestId: request.identity.id, threadId: request.identity.threadId, turnId: request.identity.turnId, subjectDigest: request.digest, decisionId, reason })\n const track = (job: Promise<void>): void => { jobs.add(job); void job.finally(() => jobs.delete(job)).catch(() => undefined) }\n const cleanupFailed = (request: OwnedRequest, id: string): void => {\n cleanupFailures.push(id)\n eventFor(request, 'cleanup_failed', 'Decision cleanup not confirmed before deadline', id)\n }\n const cleanup = async (request: OwnedRequest, id: string): Promise<void> => {\n if (request.cleanupIds.has(id)) return\n request.cleanupIds.add(id)\n const remaining = Math.min(request.cleanupDeadline, disposalDeadline) - performance.now()\n if (remaining <= 0) { cleanupFailed(request, id); return }\n const controller = new AbortController()\n cleanupControllers.add(controller)\n const timer = setTimeout(() => controller.abort(), remaining)\n try {\n for (let attempt = 0; attempt < 3; attempt += 1) {\n try {\n const result: unknown = await decisions.cancel(id, { signal: controller.signal })\n if (!isRecord(result) || result.decisionId !== id || typeof result.cancelled !== 'boolean') throw new Error('Invalid cancellation result')\n if (result.cancelled) return\n const latest: unknown = await decisions.get(id, { signal: controller.signal })\n if (isRecord(latest) && (latest.type === 'confirm' || latest.type === 'select' || latest.type === 'input')) {\n const snapshot = decisionSnapshot(latest, latest.type, id)\n if (snapshot.status !== 'pending') return\n }\n throw new TypeError('Cancellation still pending')\n } catch (error) {\n if (controller.signal.aborted || attempt === 2 || !isRetryable(error)) throw error\n await delay(attempt === 0 ? 100 : 300, controller.signal)\n }\n }\n } catch { cleanupFailed(request, id) } finally { clearTimeout(timer); cleanupControllers.delete(controller) }\n }\n const beginCleanup = (request: OwnedRequest): void => {\n clearTimeout(request.timer)\n request.cleanupDeadline = Math.min(performance.now() + CLEANUP_MS, disposalDeadline)\n request.controller.abort()\n for (const id of request.decisionIds) track(cleanup(request, id))\n }\n const completeRequest = (request: OwnedRequest, completion: RequestCompletion): void => {\n const transition = transitionRequest(request.state, completion, {\n id: request.identity.id,\n isQuestion: request.subject.kind === 'questions',\n replyAllowed: contextLive(request),\n deadlineElapsed: performance.now() >= request.deadline,\n })\n if (!transition) return\n request.state = transition.state\n beginCleanup(request)\n if (transition.reply && contextLive(request)) {\n const interrupted = transition.interruption === null || interrupt(request.identity, transition.interruption)\n if (interrupted && contextLive(request)) {\n try { config.sendResponse(transition.reply) } catch { fatal('Response enqueue failed; close connection') }\n }\n }\n if (transition.abandonment !== null) eventFor(request, 'request_abandoned', transition.abandonment)\n }\n const dispose = (): Promise<{ readonly cleanupFailures: readonly string[] }> => {\n if (disposal) return disposal\n disposed = true\n disposalDeadline = performance.now() + CLEANUP_MS\n config.signal?.removeEventListener('abort', onAbort)\n let resolveDisposal: (value: { readonly cleanupFailures: readonly string[] }) => void = () => undefined\n disposal = new Promise(resolve => { resolveDisposal = resolve })\n threads.clear()\n patches.clear()\n cacheBytes = 0\n for (const request of requests.values()) completeRequest(request, { kind: 'abandon', reason: 'Connection disposed' })\n const finish = async (): Promise<void> => {\n const controller = new AbortController()\n const drain = async (): Promise<void> => { while (jobs.size > 0 && performance.now() < disposalDeadline) await Promise.allSettled([...jobs]) }\n await Promise.race([drain(), delay(CLEANUP_MS, controller.signal)]).catch(() => undefined)\n controller.abort()\n for (const cleanupController of cleanupControllers) cleanupController.abort()\n await Promise.resolve()\n resolveDisposal({ cleanupFailures: [...cleanupFailures] })\n }\n void finish()\n return disposal\n }\n const fatal = (reason: string): void => {\n void dispose()\n emit({ type: 'reply_failed', reason })\n }\n const overflow = (): void => { void dispose(); emit({ type: 'capacity_exceeded', reason: 'Connection must close; bounded adapter capacity exceeded' }) }\n const stateCount = (): number => requests.size + closedRequestIds.size + turns.size + completedItems.size\n const rememberTurn = (threadId: string, turnId: string, terminal: boolean): boolean => {\n const key = turnKey(threadId, turnId)\n const previous = turns.get(key)\n if (previous) { previous.terminal ||= terminal; return true }\n if (stateCount() >= MAX_REQUESTS) { overflow(); return false }\n turns.set(key, { threadId, turnId, terminal })\n return true\n }\n const contextEnded = (identity: RequestIdentity): boolean => turns.get(turnKey(identity.threadId, identity.turnId))?.terminal === true || completedItems.has(itemKey(identity.threadId, identity.turnId, identity.itemId))\n const contextLive = (request: OwnedRequest): boolean => !disposed && !contextEnded(request.identity) && threads.has(request.identity.threadId)\n const live = (request: OwnedRequest): boolean => request.state === 'pending' && contextLive(request) && performance.now() < request.deadline\n const interrupt = (identity: RequestIdentity, reason: string): boolean => {\n try { config.requestTurnInterrupt(identity.threadId, identity.turnId, reason); return !disposed } catch { fatal('Turn interrupt enqueue failed; close connection'); return false }\n }\n const fenceTurn = (threadId: string, turnId: string, reason: string): void => {\n if (!rememberTurn(threadId, turnId, true)) return\n for (const request of requests.values()) if (request.identity.threadId === threadId && request.identity.turnId === turnId) completeRequest(request, { kind: 'abandon', reason })\n for (const key of patches.keys()) {\n if (key.startsWith(JSON.stringify([threadId, turnId]).slice(0, -1) + ',')) { cacheBytes -= patches.get(key)?.bytes ?? 0; patches.delete(key) }\n }\n }\n const invalidateTurn = (threadId: string, turnId: string, reason: string): void => {\n const accepted = [...requests.values()].find(request => request.identity.threadId === threadId && request.identity.turnId === turnId && request.state === 'accepted' && !contextEnded(request.identity))\n fenceTurn(threadId, turnId, reason)\n if (accepted) {\n if (!disposed) interrupt(accepted.identity, reason)\n eventFor(accepted, 'request_abandoned', 'Invalidated after acceptance enqueue; execution outcome unknown')\n }\n }\n const unregisterThread = (threadId: string): void => {\n threads.delete(threadId)\n for (const turn of turns.values()) if (turn.threadId === threadId) invalidateTurn(threadId, turn.turnId, 'Thread unregistered')\n for (const key of patches.keys()) if (key.startsWith(JSON.stringify([threadId]).slice(0, -1) + ',')) { cacheBytes -= patches.get(key)?.bytes ?? 0; patches.delete(key) }\n }\n const createAndWait = async (request: OwnedRequest, phone: PhoneQuestion, ordinal: number): Promise<DecisionSnapshot> => {\n const payload: CreateDecision = { question: phone.question, type: phone.type, options: phone.options, context: phone.context, externalId: config.externalId, agentName: config.agentName ?? 'Codex', wait: false, requireReachable: true, expiresInSeconds: Math.ceil(timeoutMs / 1000), idempotencyKey: decisionFingerprint({ tenantId: config.tenantId, externalId: config.externalId, generation, ...request.identity, subjectDigest: request.digest, ordinal, questionId: phone.questionId ?? null }) }\n let raw: unknown\n try { raw = await withRetry(() => decisions.create(payload, { signal: request.controller.signal }), request.controller.signal) } catch (error) {\n if (request.controller.signal.aborted || isRetryable(error)) eventFor(request, 'create_outcome_unknown', 'Create may have reached server; TTL bounds residual prompt')\n throw error\n }\n if (isRecord(raw) && isText(raw.decisionId)) {\n request.decisionIds.add(raw.decisionId)\n if (!live(request)) { track(cleanup(request, raw.decisionId)); throw new Error('Request no longer live') }\n }\n if (!isRecord(raw) || !isText(raw.decisionId)) eventFor(request, 'create_outcome_unknown', 'Create returned no usable decision ID')\n let snapshot = decisionSnapshot(raw, phone.type)\n eventFor(request, 'decision_created', undefined, snapshot.decisionId)\n while (snapshot.status === 'pending') {\n if (!live(request)) throw new Error('Request no longer live')\n const next: unknown = await withRetry(() => decisions.get(snapshot.decisionId, { wait: 5, signal: request.controller.signal }), request.controller.signal)\n snapshot = decisionSnapshot(next, phone.type, snapshot.decisionId)\n if (snapshot.status === 'pending') await delay(50, request.controller.signal)\n }\n request.decisionIds.delete(snapshot.decisionId)\n eventFor(request, 'decision_settled', snapshot.status, snapshot.decisionId)\n return snapshot\n }\n const runRequest = async (request: OwnedRequest, phones: readonly PhoneQuestion[]): Promise<void> => {\n try {\n const answers: Record<string, { answers: readonly string[] }> = Object.create(null)\n for (const [ordinal, phone] of phones.entries()) {\n if (!live(request)) return\n if (ordinal > 0 && request.deadline - performance.now() < MIN_QUESTION_BUDGET_MS) { completeRequest(request, { kind: 'refuse', reason: 'Insufficient time for another question' }); return }\n const result = await createAndWait(request, phone, ordinal)\n if (!live(request)) return\n if (request.subject.kind !== 'questions') {\n const approved = result.type === 'confirm' && result.status === 'answered' && isApproved({ status: result.status, type: result.type, value: result.value })\n completeRequest(request, { kind: 'reply', reply: { id: request.identity.id, result: { decision: approved ? 'accept' : request.declineDecision } } })\n return\n }\n if (!result.answered || !result.value?.trim() || result.value.length > 2000 || (phone.options && !phone.options.includes(result.value))) { completeRequest(request, { kind: 'refuse', reason: 'Question did not receive a valid answer' }); return }\n const question = request.subject.questions[ordinal]\n if (!question || !phone.questionId) throw new Error('Question identity missing')\n const selectedIndex = phone.options?.indexOf(result.value)\n const answer = selectedIndex !== undefined ? question.options?.[selectedIndex]?.label : result.value\n if (answer === undefined) throw new Error('Question option missing')\n answers[phone.questionId] = { answers: [answer] }\n }\n completeRequest(request, { kind: 'reply', reply: { id: request.identity.id, result: { answers } } })\n } catch (error) { if (live(request)) completeRequest(request, { kind: 'refuse', reason: decisionFailureReason(error) }) }\n }\n const observe = (method: string, params: Record<string, unknown>): CodexDispatchResult => {\n if (method === 'serverRequest/resolved' && isId(params.requestId)) {\n const request = requests.get(rpcKey(params.requestId))\n if (request && params.threadId === request.identity.threadId) completeRequest(request, { kind: 'abandon', reason: 'Server request resolved' })\n }\n if (isText(params.threadId)) {\n if (threads.has(params.threadId) && isRecord(params.turn) && isText(params.turn.id) && method === 'turn/started') rememberTurn(params.threadId, params.turn.id, false)\n if (['thread/closed', 'thread/archived', 'thread/settings/updated'].includes(method)) unregisterThread(params.threadId)\n if (isText(params.turnId) && ['turn/completed', 'turn/interrupted', 'turn/failed'].includes(method)) fenceTurn(params.threadId, params.turnId, method)\n if (isRecord(params.turn) && isText(params.turn.id) && method === 'turn/completed') fenceTurn(params.threadId, params.turn.id, method)\n const itemId = isRecord(params.item) && isText(params.item.id) ? params.item.id : params.itemId\n if (isText(params.turnId) && isText(itemId)) {\n const key = itemKey(params.threadId, params.turnId, itemId)\n if (method === 'item/completed') {\n if (!completedItems.has(key) && stateCount() >= MAX_REQUESTS) { overflow(); return { kind: 'invalid', reason: 'Terminal item capacity exceeded' } }\n completedItems.add(key)\n for (const request of requests.values()) if (itemKey(request.identity.threadId, request.identity.turnId, request.identity.itemId) === key) completeRequest(request, { kind: 'abandon', reason: 'Item completed' })\n cacheBytes -= patches.get(key)?.bytes ?? 0\n patches.delete(key)\n }\n if (threads.has(params.threadId) && !completedItems.has(key) && turns.get(turnKey(params.threadId, params.turnId))?.terminal !== true && (method === 'item/fileChange/patchUpdated' || (method === 'item/started' && isRecord(params.item) && params.item.type === 'fileChange'))) {\n if (!rememberTurn(params.threadId, params.turnId, false)) return { kind: 'invalid', reason: 'Turn capacity exceeded' }\n const raw = method === 'item/started' && isRecord(params.item) ? params.item.changes : params.changes\n const previous = patches.get(key)\n const parsed = fileChangesOf(raw)\n const digest = decisionFingerprint(parsed)\n const conflict = previous !== undefined && previous.digest !== digest\n const changes = (method === 'item/fileChange/patchUpdated' && !previous) || (method === 'item/started' && conflict) || previous?.changes === null ? null : parsed\n const bytes = Buffer.byteLength(JSON.stringify(changes))\n if ((!previous && patches.size >= MAX_ITEMS) || cacheBytes - (previous?.bytes ?? 0) + bytes > MAX_CACHE_BYTES) { overflow(); return { kind: 'invalid', reason: 'Patch capacity exceeded' } }\n patches.set(key, { changes, digest, bytes })\n cacheBytes += bytes - (previous?.bytes ?? 0)\n if (conflict || changes === null) for (const request of requests.values()) if (itemKey(request.identity.threadId, request.identity.turnId, request.identity.itemId) === key) {\n if (request.state === 'accepted' && !contextEnded(request.identity)) invalidateTurn(request.identity.threadId, request.identity.turnId, 'Reviewed patch changed after acceptance')\n else completeRequest(request, { kind: 'refuse', reason: 'Reviewed patch changed' })\n }\n }\n }\n }\n return { kind: 'observed' }\n }\n const delegateToNative = (id: string | number, reason: string): CodexDispatchResult => {\n if (stateCount() >= MAX_REQUESTS) { overflow(); return { kind: 'invalid', reason: 'Request capacity exceeded' } }\n closedRequestIds.add(rpcKey(id))\n emit({ type: 'native_review_required', requestId: id, reason })\n return disposed ? { kind: 'invalid', reason: 'Disposed during native handoff' } : { kind: 'unhandled', reason }\n }\n const dispatch = (message: unknown): CodexDispatchResult => {\n if (disposed) return { kind: 'invalid', reason: 'Adapter disposed' }\n if (!isRecord(message) || !isText(message.method) || !isRecord(message.params)) return { kind: 'invalid', reason: 'Invalid server envelope' }\n if (message.id === undefined) return observe(message.method, message.params)\n if (!isId(message.id)) return { kind: 'invalid', reason: 'Invalid RPC ID' }\n if (closedRequestIds.has(rpcKey(message.id))) return { kind: 'claimed' }\n const previous = requests.get(rpcKey(message.id))\n if (previous) {\n if (previous.envelopeDigest !== decisionFingerprint({ method: message.method, params: message.params })) {\n if (previous.state === 'accepted') invalidateTurn(previous.identity.threadId, previous.identity.turnId, 'Conflicting duplicate request')\n else completeRequest(previous, { kind: 'conflict', reason: 'Conflicting duplicate request' })\n }\n return { kind: 'claimed' }\n }\n if (![COMMAND, FILES, QUESTIONS].includes(message.method)) return delegateToNative(message.id, 'Unsupported server method')\n const identity = identityOf(message.id, message.method, message.params)\n if (!identity) return { kind: 'invalid', reason: 'Malformed request identity; close connection' }\n if (contextEnded(identity)) {\n if (stateCount() >= MAX_REQUESTS) { overflow(); return { kind: 'invalid', reason: 'Request capacity exceeded' } }\n closedRequestIds.add(rpcKey(identity.id))\n if (identity.method === QUESTIONS && !interrupt(identity, 'Request context has ended')) return { kind: 'invalid', reason: 'Interrupt failed' }\n if (!disposed) config.sendResponse(identity.method === QUESTIONS ? { id: identity.id, error: { code: -32000, message: 'Request context has ended' } } : { id: identity.id, result: { decision: 'cancel' } })\n return { kind: 'claimed' }\n }\n if (!threads.has(identity.threadId)) return delegateToNative(message.id, 'Unregistered request context')\n if (!rememberTurn(identity.threadId, identity.turnId, false)) return { kind: 'invalid', reason: 'Turn capacity exceeded' }\n const subject = subjectOf(message.method, message.params, patches.get(itemKey(identity.threadId, identity.turnId, identity.itemId))?.changes ?? null, config.experimentalQuestions === true)\n if (subject.kind === 'malformed') {\n if (stateCount() >= MAX_REQUESTS) { overflow(); return { kind: 'invalid', reason: 'Request capacity exceeded' } }\n closedRequestIds.add(rpcKey(message.id))\n if (message.method === QUESTIONS && !interrupt(identity, subject.reason)) return { kind: 'invalid', reason: 'Interrupt failed' }\n try { config.sendResponse({ id: message.id, error: { code: -32602, message: subject.reason } }) } catch { fatal('Invalid-params response enqueue failed') }\n return { kind: 'claimed' }\n }\n if (subject.kind === 'unsupported') return delegateToNative(message.id, subject.reason)\n const digest = decisionFingerprint(subject.subject)\n const phones = presentSubject(subject.subject)\n if (!phones) return delegateToNative(message.id, 'Subject exceeds presentation limits')\n if (stateCount() >= MAX_REQUESTS || [...requests.values()].filter(request => request.state === 'pending').length >= MAX_PENDING) { overflow(); return { kind: 'invalid', reason: 'Request capacity exceeded' } }\n const controller = new AbortController()\n const timer = setTimeout(() => { const request = requests.get(rpcKey(identity.id)); if (request) completeRequest(request, { kind: 'refuse', reason: 'Decision deadline elapsed' }) }, timeoutMs)\n const request: OwnedRequest = { identity, declineDecision: Array.isArray(message.params.availableDecisions) && !message.params.availableDecisions.includes('decline') ? 'cancel' : 'decline', digest, envelopeDigest: decisionFingerprint({ method: message.method, params: message.params }), subject: subject.subject, controller, deadline: performance.now() + timeoutMs, timer, decisionIds: new Set(), cleanupIds: new Set(), state: 'pending', cleanupDeadline: Infinity }\n requests.set(rpcKey(identity.id), request)\n let allowed = false\n try { allowed = config.maySendSubject(structuredClone(subject.subject)) === true } catch { allowed = false }\n if (disposed) return { kind: 'invalid', reason: 'Disposed during privacy review' }\n if (!live(request)) return { kind: 'claimed' }\n if (!allowed) {\n completeRequest(request, { kind: 'native' })\n requests.delete(rpcKey(identity.id))\n return delegateToNative(message.id, 'Subject rejected by privacy policy')\n }\n track(runRequest(request, phones))\n return { kind: 'claimed' }\n }\n const handle = (message: unknown): CodexDispatchResult => {\n try { return dispatch(message) } catch {\n fatal('Invalid message or host callback; close connection')\n return { kind: 'invalid', reason: 'Message could not be validated' }\n }\n }\n const registerThread = (value: unknown): void => {\n if (disposed) throw new Error('Adapter disposed')\n const id = certifiedThread(value)\n if (!id) throw new TypeError('Thread requires user approvals and a certified sandbox')\n if ([...requests.values()].some(request => request.state === 'pending' && request.identity.threadId === id)) throw new Error('Cannot replace thread policy while approval is pending')\n if (!threads.has(id) && threads.size >= MAX_THREADS) { overflow(); throw new Error('Thread capacity exceeded') }\n threads.add(id)\n }\n function onAbort(): void { void dispose() }\n config.signal?.addEventListener('abort', onAbort, { once: true })\n if (config.signal?.aborted) void dispose()\n return { registerThread, unregisterThread, handle, invalidateTurn, dispose }\n}\n","import type { CodexQuestion, CodexRequestId, CodexReviewSubject, FileChange, RequestIdentity } from './types'\n\nexport const COMMAND = 'item/commandExecution/requestApproval'\nexport const FILES = 'item/fileChange/requestApproval'\nexport const QUESTIONS = 'item/tool/requestUserInput'\nexport const isRecord = (value: unknown): value is Record<string, unknown> => typeof value === 'object' && value !== null && !Array.isArray(value)\nexport const isId = (value: unknown): value is CodexRequestId => typeof value === 'string' || (typeof value === 'number' && Number.isSafeInteger(value))\nexport const isText = (value: unknown): value is string => typeof value === 'string' && value.length > 0\nconst optionalText = (value: unknown): boolean => value === undefined || value === null || typeof value === 'string'\nexport const onlyKeys = (value: Record<string, unknown>, keys: readonly string[]): boolean => Object.keys(value).every(key => keys.includes(key))\nexport const identityOf = (id: CodexRequestId, method: string, params: Record<string, unknown>): RequestIdentity | null => {\n if (!isText(params.threadId) || !isText(params.turnId) || !isText(params.itemId) || !optionalText(params.approvalId)) return null\n return { id, method, threadId: params.threadId, turnId: params.turnId, itemId: params.itemId, approvalId: typeof params.approvalId === 'string' ? params.approvalId : null }\n}\nexport const certifiedThread = (value: unknown): string | null => {\n if (!isRecord(value) || !isRecord(value.thread) || !isText(value.thread.id) || !isRecord(value.thread.status) || value.thread.status.type !== 'idle') return null\n if ((value.approvalPolicy !== 'untrusted' && value.approvalPolicy !== 'on-request') || value.approvalsReviewer !== 'user' || !isRecord(value.sandbox)) return null\n const sandbox = value.sandbox\n if (sandbox.type !== 'readOnly' && sandbox.type !== 'workspaceWrite') return null\n if (!onlyKeys(sandbox, ['type', 'networkAccess', 'writableRoots', 'excludeSlashTmp', 'excludeTmpdirEnvVar'])) return null\n if (sandbox.networkAccess !== undefined && typeof sandbox.networkAccess !== 'boolean') return null\n if (sandbox.writableRoots !== undefined && (!Array.isArray(sandbox.writableRoots) || !sandbox.writableRoots.every(isText))) return null\n if (['excludeSlashTmp', 'excludeTmpdirEnvVar'].some(key => sandbox[key] !== undefined && typeof sandbox[key] !== 'boolean')) return null\n if (value.activePermissionProfile !== undefined && value.activePermissionProfile !== null) return null\n return value.thread.id\n}\nexport const fileChangesOf = (value: unknown): readonly FileChange[] | null => {\n if (!Array.isArray(value) || value.length === 0 || value.length > 128) return null\n const changes: FileChange[] = []\n for (const change of value) {\n if (!isRecord(change) || !onlyKeys(change, ['path', 'kind', 'diff']) || !isText(change.path) || typeof change.diff !== 'string' || !isRecord(change.kind)) return null\n const kind = change.kind\n if (!onlyKeys(kind, ['type', 'move_path']) || (kind.type !== 'add' && kind.type !== 'delete' && kind.type !== 'update')) return null\n if (!optionalText(kind.move_path) || (kind.type !== 'update' && kind.move_path != null)) return null\n changes.push({ path: change.path, operation: kind.type, movePath: typeof kind.move_path === 'string' ? kind.move_path : null, diff: change.diff })\n }\n return changes\n}\ntype SubjectResult = { readonly kind: 'supported'; readonly subject: CodexReviewSubject } | { readonly kind: 'unsupported'; readonly reason: string } | { readonly kind: 'malformed'; readonly reason: string }\nconst unsupported = (reason: string): SubjectResult => ({ kind: 'unsupported', reason })\nconst malformed = (reason: string): SubjectResult => ({ kind: 'malformed', reason })\nconst isKnownCommandDecision = (value: unknown): boolean => {\n if (typeof value === 'string') return ['accept', 'acceptForSession', 'decline', 'cancel'].includes(value)\n if (!isRecord(value) || !onlyKeys(value, ['acceptWithExecpolicyAmendment'])) return false\n const amendment = value.acceptWithExecpolicyAmendment\n return isRecord(amendment) && onlyKeys(amendment, ['execpolicy_amendment']) && Array.isArray(amendment.execpolicy_amendment) && amendment.execpolicy_amendment.every(isText)\n}\nexport const subjectOf = (method: string, params: Record<string, unknown>, changes: readonly FileChange[] | null, experimental: boolean): SubjectResult => {\n if (method === COMMAND) {\n if (!onlyKeys(params, ['threadId', 'turnId', 'itemId', 'approvalId', 'startedAtMs', 'kind', 'environmentId', 'command', 'cwd', 'reason', 'commandActions', 'networkApprovalContext', 'proposedExecpolicyAmendment', 'proposedNetworkPolicyAmendments', 'availableDecisions'])) return unsupported('Unknown command permission fields')\n if ((params.kind !== undefined && params.kind !== 'command') || (params.environmentId != null && params.environmentId !== 'local') || params.networkApprovalContext != null || params.proposedNetworkPolicyAmendments != null) return unsupported('Command needs native permission review')\n if (params.proposedExecpolicyAmendment != null && (!Array.isArray(params.proposedExecpolicyAmendment) || !params.proposedExecpolicyAmendment.every(isText))) return malformed('Invalid policy suggestion')\n if (params.availableDecisions != null && (!Array.isArray(params.availableDecisions) || !params.availableDecisions.includes('accept') || !params.availableDecisions.every(isKnownCommandDecision))) return unsupported('Unknown available approval decisions')\n if (!isText(params.command) || !isText(params.cwd) || !optionalText(params.reason) || !Number.isSafeInteger(params.startedAtMs)) return malformed('Incomplete command subject')\n return { kind: 'supported', subject: { kind: 'command', command: params.command, cwd: params.cwd, reason: typeof params.reason === 'string' ? params.reason : null } }\n }\n if (method === FILES) {\n if (!onlyKeys(params, ['threadId', 'turnId', 'itemId', 'startedAtMs', 'reason', 'grantRoot']) || params.grantRoot != null) return unsupported('File request widens permissions')\n if (!optionalText(params.reason) || !Number.isSafeInteger(params.startedAtMs)) return malformed('Malformed file request')\n if (!changes) return unsupported('Complete file patch unavailable')\n return { kind: 'supported', subject: { kind: 'files', changes, reason: typeof params.reason === 'string' ? params.reason : null } }\n }\n if (!experimental) return unsupported('Experimental questions disabled')\n if (!onlyKeys(params, ['threadId', 'turnId', 'itemId', 'questions', 'isBlocking', 'autoResolutionMs']) || params.isBlocking !== true || params.autoResolutionMs != null) return unsupported('Question requires native handling')\n if (!Array.isArray(params.questions) || params.questions.length < 1 || params.questions.length > 3) return malformed('Question count outside supported range')\n const questions: CodexQuestion[] = []\n for (const question of params.questions) {\n if (!isRecord(question) || !onlyKeys(question, ['id', 'header', 'question', 'isOther', 'isSecret', 'options'])) return malformed('Malformed question')\n if (question.isOther !== false || question.isSecret !== false) return unsupported('Secret or other-option question')\n if (!isText(question.id) || !isText(question.header) || !isText(question.question) || questions.some(entry => entry.id === question.id)) return malformed('Invalid question identity')\n let options: CodexQuestion['options'] = null\n if (question.options !== null) {\n if (!Array.isArray(question.options) || question.options.length < 2 || question.options.length > 20) return malformed('Invalid options')\n const validated: { label: string; description: string }[] = []\n for (const option of question.options) {\n if (!isRecord(option) || !onlyKeys(option, ['label', 'description']) || !isText(option.label) || option.label.length > 200 || typeof option.description !== 'string' || validated.some(entry => entry.label === option.label)) return malformed('Invalid option')\n validated.push({ label: option.label, description: option.description })\n }\n options = validated\n }\n questions.push({ id: question.id, header: question.header, question: question.question, options })\n }\n return { kind: 'supported', subject: { kind: 'questions', questions } }\n}\n","import type { CodexReviewSubject } from './types'\nimport type { DecisionType } from '@pushary/server'\n\nexport interface PhoneQuestion {\n readonly question: string\n readonly context: string\n readonly type: DecisionType\n readonly options?: readonly string[]\n readonly questionId?: string\n}\nexport const visibleText = (text: string): string => text.replace(/\\\\/g, '\\\\\\\\').replace(/[\\p{Cc}\\p{Cf}\\p{Zl}\\p{Zp}]/gu, character => `\\\\u{${character.codePointAt(0)?.toString(16)}}`)\nconst bounded = (questions: readonly PhoneQuestion[]): readonly PhoneQuestion[] | null => questions.every(question => question.question.length <= 8000 && question.context.length <= 2000 && question.options?.some(option => option.length > 200) !== true) ? questions : null\nexport const presentSubject = (subject: CodexReviewSubject): readonly PhoneQuestion[] | null => {\n if (subject.kind === 'command') return bounded([{ type: 'confirm', question: `Approve this command once?\\n\\n${visibleText(subject.command)}\\n\\nWorking directory: ${visibleText(subject.cwd)}\\nReason: ${visibleText(subject.reason ?? '(none)')}`, context: 'Codex command approval. This approval applies only to this request.' }])\n if (subject.kind === 'files') {\n const patches = subject.changes.map(change => `${change.operation}: ${visibleText(change.path)}${change.movePath === null ? '' : ` -> ${visibleText(change.movePath)}`}\\n${visibleText(change.diff)}`).join('\\n\\n')\n return bounded([{ type: 'confirm', question: `Approve these exact file changes once?\\n\\n${patches}\\n\\nReason: ${visibleText(subject.reason ?? '(none)')}`, context: 'Codex file approval. Includes every file and the complete patch.' }])\n }\n return bounded(subject.questions.map(question => ({ questionId: question.id, type: question.options ? 'select' : 'input', question: `${visibleText(question.header)}\\n${visibleText(question.question)}${question.options ? '\\n\\n' + question.options.map(option => `${visibleText(option.label)}: ${visibleText(option.description)}`).join('\\n') : ''}`, options: question.options?.map(option => visibleText(option.label)), context: 'Answer this Codex question. This answer does not grant tool permissions.' })))\n}\n","import { PusharyApiError, type DecisionType } from '@pushary/server'\nimport { isRecord, isText } from './protocol'\n\nexport interface DecisionSnapshot {\n readonly decisionId: string\n readonly status: 'pending' | 'answered' | 'expired' | 'cancelled' | 'stopped'\n readonly answered: boolean\n readonly value: string | null\n readonly type: DecisionType\n}\nexport const decisionSnapshot = (value: unknown, type: DecisionType, expectedId?: string): DecisionSnapshot => {\n if (!isRecord(value) || !isText(value.decisionId) || (expectedId !== undefined && value.decisionId !== expectedId) || value.type !== type || typeof value.answered !== 'boolean') throw new Error('Invalid decision response')\n const status = value.status\n if (status !== 'pending' && status !== 'answered' && status !== 'expired' && status !== 'cancelled' && status !== 'stopped') throw new Error('Unknown decision status')\n if ((status === 'answered') !== value.answered || (status === 'answered' && typeof value.value !== 'string') || (status !== 'answered' && value.value != null)) throw new Error('Inconsistent decision response')\n return { decisionId: value.decisionId, status, answered: value.answered, value: typeof value.value === 'string' ? value.value : null, type }\n}\nexport const isRetryable = (error: unknown): boolean => error instanceof PusharyApiError ? error.status === 429 || error.status >= 500 : error instanceof TypeError || (error instanceof Error && error.name === 'TimeoutError')\nexport const delay = (milliseconds: number, signal: AbortSignal): Promise<void> => new Promise((resolve, reject) => {\n if (signal.aborted) { reject(signal.reason); return }\n const onAbort = (): void => { clearTimeout(timer); reject(signal.reason) }\n const timer = setTimeout(() => { signal.removeEventListener('abort', onAbort); resolve() }, milliseconds)\n signal.addEventListener('abort', onAbort, { once: true })\n})\nexport const withRetry = async <T>(operation: () => Promise<T>, signal: AbortSignal): Promise<T> => {\n for (let attempt = 0; ; attempt += 1) {\n signal.throwIfAborted()\n try { return await operation() } catch (error) {\n if (signal.aborted || attempt >= 3 || !isRetryable(error)) throw error\n await delay(250 * 2 ** attempt + Math.floor(Math.random() * 101), signal)\n }\n }\n}\n\nexport const decisionFailureReason = (error: unknown): string => {\n if (error instanceof PusharyApiError) return `Partner HTTP ${error.status}`\n if (error instanceof TypeError) return 'Decision transport unavailable'\n if (error instanceof Error && error.name === 'TimeoutError') return 'Decision HTTP deadline elapsed'\n return 'Decision response invalid or unavailable'\n}\n","import type { CodexReply, CodexRequestId } from './types'\n\nexport type RequestState = 'pending' | 'accepted' | 'closed'\nexport type RequestCompletion =\n | { readonly kind: 'reply'; readonly reply: CodexReply }\n | { readonly kind: 'refuse' | 'conflict' | 'abandon'; readonly reason: string }\n | { readonly kind: 'native' }\n\ninterface RequestContext {\n readonly id: CodexRequestId\n readonly isQuestion: boolean\n readonly replyAllowed: boolean\n readonly deadlineElapsed: boolean\n}\ninterface RequestTransition {\n readonly state: Exclude<RequestState, 'pending'>\n readonly reply: CodexReply | null\n readonly interruption: string | null\n readonly abandonment: string | null\n}\n\nexport const transitionRequest = (state: RequestState, completion: RequestCompletion, context: RequestContext): RequestTransition | null => {\n if (state !== 'pending') return null\n if (completion.kind === 'reply') {\n if (!context.replyAllowed || context.deadlineElapsed) return null\n const reply = completion.reply\n const accepted = 'result' in reply && 'decision' in reply.result && reply.result.decision === 'accept'\n return { state: accepted ? 'accepted' : 'closed', reply, interruption: null, abandonment: null }\n }\n if (completion.kind === 'native') return { state: 'closed', reply: null, interruption: null, abandonment: null }\n if (completion.kind === 'abandon' || !context.replyAllowed) return { state: 'closed', reply: null, interruption: null, abandonment: completion.reason }\n const reply: CodexReply = context.isQuestion\n ? { id: context.id, error: { code: -32000, message: completion.reason } }\n : { id: context.id, result: { decision: 'cancel' } }\n return { state: 'closed', reply, interruption: context.isQuestion || completion.kind === 'conflict' ? completion.reason : null, abandonment: completion.reason }\n}\n"],"mappings":"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","names":["reply","request"]}
@@ -0,0 +1,42 @@
1
+ # Examples
2
+
3
+ `node examples/simulated-host.mjs` runs a synthetic Codex request through a local mock Partner HTTP service. It needs no credentials and does not execute a command or contact a phone.
4
+
5
+ `stdio-host.mjs` exports `startStdioHost` for a real, pinned Codex child process. Supply an explicit executable/CLI path, workspace, separate CODEX_HOME, and adapter configuration. Initialize that home with the intended model/provider configuration first. Only `OPENAI_API_KEY` or `CODEX_TEST_API_KEY` may be passed in `modelEnvironment`; the host does not inherit the Partner key or your personal Codex login. Never place the Partner credential in the workspace or Codex config.
6
+
7
+ ```js
8
+ import { startStdioHost } from './stdio-host.mjs'
9
+
10
+ const host = await startStdioHost({
11
+ executable: process.execPath,
12
+ args: ['/absolute/path/to/@openai/codex/bin/codex.js'],
13
+ cwd: '/absolute/path/to/customer-workspace',
14
+ codexHome: '/absolute/path/to/isolated-codex-home',
15
+ modelEnvironment: { OPENAI_API_KEY: modelApiKey },
16
+ adapterConfig: {
17
+ apiKey: partnerApiKey,
18
+ tenantId: authenticatedTenantId,
19
+ externalId: authenticatedCustomerId,
20
+ codexVersion: '0.160.1',
21
+ maySendSubject: subject => privacyPolicy.allows(subject),
22
+ onEvent: event => audit.write(event),
23
+ },
24
+ })
25
+ for (const signal of ['SIGINT', 'SIGTERM']) {
26
+ process.once(signal, () => { void host.close() })
27
+ }
28
+ try {
29
+ const { thread } = await host.startThread()
30
+ const { turn } = await host.startTurn(thread.id, 'Inspect the repository.')
31
+ const outcome = await host.waitForTurn(turn.id)
32
+ console.log(outcome.status)
33
+ } finally {
34
+ await host.close()
35
+ }
36
+ ```
37
+
38
+ This is an opt-in live example: Codex/model usage and Partner decisions can be billed. The customer must already be enrolled (see `connect` in the package README). Each supported question in a batch creates a separate Partner decision. Reachability confirms an available delivery channel, not receipt by a person.
39
+
40
+ The host is intentionally small: stdio, bounded frames/FIFO/RPCs, fixed user-reviewed read-only start settings, no per-turn permission overrides, native refusal for unsupported requests, and bounded graceful child shutdown. It verifies the binary's initialization version. It does not provide interactive authentication, production audit storage, WebSocket reconnect, multi-tenant worker orchestration, or restart replay. Extend those host responsibilities before production use. SIGKILL may leave pending prompts until TTL expiry.
41
+
42
+ Run `npm run test:conformance` after a build to exercise real Codex command/file side effects against mock model and Partner services. The suite uses temporary files and cleans up its own child processes.
@@ -0,0 +1,24 @@
1
+ import { createServer } from 'node:http'
2
+ import assert from 'node:assert/strict'
3
+ import { createPusharyCodexHandler } from '@pushary/codex-app-server'
4
+ const server = createServer(async (request, response) => {
5
+ let body = ''
6
+ for await (const chunk of request) body += chunk
7
+ const decision = JSON.parse(body)
8
+ assert.equal(decision.externalId, 'simulated-customer')
9
+ response.writeHead(200, { 'Content-Type': 'application/json' })
10
+ response.end(JSON.stringify({ decisionId: 'simulated-decision', status: 'answered', answered: true, value: 'yes', type: 'confirm' }))
11
+ })
12
+ let handler
13
+ try {
14
+ await new Promise((resolve, reject) => { server.once('error', reject); server.listen(0, '127.0.0.1', resolve) })
15
+ const address = server.address()
16
+ if (!address || typeof address === 'string') throw new Error('No local server address')
17
+ let resolveReply
18
+ const reply = new Promise(resolve => { resolveReply = resolve })
19
+ handler = createPusharyCodexHandler({ apiKey: 'pk_simulated.secret', tenantId: 'simulated-tenant', externalId: 'simulated-customer', baseUrl: `http://127.0.0.1:${address.port}`, codexVersion: '0.160.1', maySendSubject: () => true, onEvent: event => console.log(event.type), sendResponse: resolveReply, requestTurnInterrupt: () => {} })
20
+ handler.registerThread({ thread: { id: 'simulated-thread', status: { type: 'idle' } }, approvalPolicy: 'on-request', approvalsReviewer: 'user', sandbox: { type: 'readOnly' } })
21
+ handler.handle({ id: 1, method: 'item/commandExecution/requestApproval', params: { threadId: 'simulated-thread', turnId: 'simulated-turn', itemId: 'simulated-item', command: 'echo simulated', cwd: '/tmp', startedAtMs: Date.now() } })
22
+ assert.deepEqual(await reply, { id: 1, result: { decision: 'accept' } })
23
+ console.log('Simulated approval passed; no command was executed and no phone was contacted.')
24
+ } finally { await handler?.dispose(); server.closeAllConnections(); await new Promise(resolve => server.close(resolve)) }
@@ -0,0 +1,157 @@
1
+ import { spawn } from 'node:child_process'
2
+ import { createPusharyCodexHandler } from '@pushary/codex-app-server'
3
+
4
+ const MAX_FRAME_BYTES = 1024 * 1024
5
+ const MAX_QUEUED_BYTES = 4 * MAX_FRAME_BYTES
6
+ const TURN_COMPLETION_BUFFER_MS = 120_000
7
+ export const startStdioHost = async ({ executable, args = [], cwd, codexHome, modelEnvironment = {}, adapterConfig, onMessage = () => {} }) => {
8
+ if (!codexHome || !cwd) throw new Error('An isolated Codex home and workspace are required')
9
+ if (Object.keys(modelEnvironment).some(key => !['OPENAI_API_KEY', 'CODEX_TEST_API_KEY'].includes(key))) throw new Error('Only model credentials may enter the child environment')
10
+ let child
11
+ let childClosed = Promise.resolve()
12
+ const turnTimeoutMs = (adapterConfig.timeoutMs ?? 300_000) + TURN_COMPLETION_BUFFER_MS
13
+ let sequence = 0
14
+ let closed = false
15
+ let shutdown
16
+ let inbound = Buffer.alloc(0)
17
+ let queuedBytes = 0
18
+ let blocked = false
19
+ const queue = []
20
+ const pending = new Map()
21
+ const ownedThreads = new Set()
22
+ const finishedTurns = new Map()
23
+ const turnWaiters = new Map()
24
+ const stop = () => {
25
+ if (shutdown) return shutdown
26
+ const completion = Promise.withResolvers()
27
+ shutdown = completion.promise
28
+ closed = true
29
+ for (const request of pending.values()) { clearTimeout(request.timer); request.reject(new Error('Codex connection closed')) }
30
+ pending.clear()
31
+ for (const waiter of turnWaiters.values()) { clearTimeout(waiter.timer); waiter.reject(new Error('Codex connection closed')) }
32
+ turnWaiters.clear()
33
+ queue.length = 0
34
+ queuedBytes = 0
35
+ void adapter.dispose().then(async () => {
36
+ if (!child) return
37
+ child.stdin?.destroy()
38
+ child.kill('SIGTERM')
39
+ const timer = setTimeout(() => child.kill('SIGKILL'), 2000)
40
+ try { await childClosed } finally { clearTimeout(timer) }
41
+ }).then(completion.resolve, completion.reject)
42
+ return shutdown
43
+ }
44
+ const drain = () => {
45
+ while (!blocked && queue.length && !closed) {
46
+ const frame = queue.shift()
47
+ queuedBytes -= Buffer.byteLength(frame)
48
+ blocked = !child.stdin.write(frame)
49
+ }
50
+ }
51
+ const enqueue = message => {
52
+ if (closed) throw new Error('Codex connection closed')
53
+ const frame = JSON.stringify(message) + '\n'
54
+ const bytes = Buffer.byteLength(frame)
55
+ if (bytes > MAX_FRAME_BYTES || queuedBytes + child.stdin.writableLength + bytes > MAX_QUEUED_BYTES) { void stop(); throw new Error('Codex transport capacity exceeded') }
56
+ queue.push(frame); queuedBytes += bytes; drain()
57
+ }
58
+ const rpc = (method, params) => new Promise((resolve, reject) => {
59
+ if (closed) { reject(new Error('Codex connection closed')); return }
60
+ if (pending.size >= 128) { void stop(); reject(new Error('RPC capacity exceeded')); return }
61
+ const id = `host:${++sequence}`
62
+ const timer = setTimeout(() => { pending.delete(id); reject(new Error('Codex RPC deadline elapsed')); void stop() }, 30_000)
63
+ pending.set(id, { resolve, reject, timer })
64
+ try { enqueue({ id, method, params }) } catch (error) { clearTimeout(timer); pending.delete(id); reject(error) }
65
+ })
66
+ const adapter = createPusharyCodexHandler({ ...adapterConfig, sendResponse: enqueue, requestTurnInterrupt: (threadId, turnId) => { void rpc('turn/interrupt', { threadId, turnId }).catch(() => stop()) }, onEvent: event => {
67
+ if (event.type === 'reply_failed' || event.type === 'capacity_exceeded') void stop()
68
+ adapterConfig.onEvent(event)
69
+ } })
70
+ const fallback = message => {
71
+ const { id, method, params } = message
72
+ if (method === 'item/commandExecution/requestApproval' || method === 'item/fileChange/requestApproval') return enqueue({ id, result: { decision: 'decline' } })
73
+ if (method === 'item/permissions/requestApproval') return enqueue({ id, result: { permissions: {}, scope: 'turn' } })
74
+ if (method === 'mcpServer/elicitation/request') return enqueue({ id, result: { action: 'cancel', content: null } })
75
+ if (method === 'item/tool/call') return enqueue({ id, result: { contentItems: [], success: false } })
76
+ if (method === 'item/tool/requestUserInput') {
77
+ if (!params || !ownedThreads.has(params.threadId) || typeof params.turnId !== 'string') { void stop(); return }
78
+ void rpc('turn/interrupt', { threadId: params.threadId, turnId: params.turnId }).catch(() => stop())
79
+ return enqueue({ id, error: { code: -32000, message: 'Question requires native UI; turn interrupted' } })
80
+ }
81
+ if (method.startsWith('account/') || method.startsWith('auth/') || method === 'execCommandApproval' || method === 'applyPatchApproval') { void stop(); return }
82
+ enqueue({ id, error: { code: -32601, message: 'Unsupported server request' } })
83
+ }
84
+ const dispatch = message => {
85
+ if (closed) return
86
+ if (!message || typeof message !== 'object' || Array.isArray(message)) throw new Error('Invalid server frame')
87
+ onMessage(message)
88
+ if (message.method === undefined) {
89
+ const request = pending.get(message.id)
90
+ if (!request) throw new Error('Unmatched RPC response')
91
+ clearTimeout(request.timer); pending.delete(message.id)
92
+ if (message.error) request.reject(new Error('Codex RPC failed: ' + JSON.stringify(message.error)))
93
+ else request.resolve(message.result)
94
+ return
95
+ }
96
+ const result = adapter.handle(message)
97
+ if (result.kind === 'invalid') { void stop(); return }
98
+ if (message.method === 'turn/completed') {
99
+ const turn = message.params?.turn
100
+ if (turn?.id) {
101
+ const waiter = turnWaiters.get(turn.id)
102
+ if (waiter) { clearTimeout(waiter.timer); turnWaiters.delete(turn.id); waiter.resolve(turn) }
103
+ else { if (finishedTurns.size >= 128) finishedTurns.delete(finishedTurns.keys().next().value); finishedTurns.set(turn.id, turn) }
104
+ }
105
+ }
106
+ if (result.kind === 'unhandled' && message.id !== undefined) fallback(message)
107
+ }
108
+ try {
109
+ adapterConfig.signal?.throwIfAborted()
110
+ child = spawn(executable, [...args, 'app-server', '--stdio'], { cwd, env: { PATH: process.env.PATH, HOME: codexHome, CODEX_HOME: codexHome, TMPDIR: codexHome, ...modelEnvironment }, stdio: ['pipe', 'pipe', 'pipe'] })
111
+ } catch (error) { await stop(); throw error }
112
+ childClosed = new Promise(resolve => child.once('close', resolve))
113
+ child.on('error', () => { void stop() })
114
+ child.on('close', () => { void stop() })
115
+ child.stdout?.on('data', chunk => {
116
+ inbound = Buffer.concat([inbound, chunk])
117
+ try {
118
+ let newline
119
+ while ((newline = inbound.indexOf(10)) !== -1) {
120
+ if (newline > MAX_FRAME_BYTES) throw new Error('Codex frame too large')
121
+ const line = inbound.subarray(0, newline).toString('utf8'); inbound = inbound.subarray(newline + 1)
122
+ dispatch(JSON.parse(line))
123
+ }
124
+ if (inbound.length > MAX_FRAME_BYTES) throw new Error('Codex frame too large')
125
+ } catch { void stop() }
126
+ })
127
+ child.stderr?.resume()
128
+ child.stdin?.on('drain', () => { blocked = false; drain() })
129
+ child.stdin?.on('error', () => { void stop() })
130
+ const waitForTurn = turnId => {
131
+ if (closed) return Promise.reject(new Error('Codex connection closed'))
132
+ if (finishedTurns.has(turnId)) { const turn = finishedTurns.get(turnId); finishedTurns.delete(turnId); return Promise.resolve(turn) }
133
+ if (turnWaiters.has(turnId)) return Promise.reject(new Error('Turn already has a waiter'))
134
+ return new Promise((resolve, reject) => {
135
+ const timer = setTimeout(() => { turnWaiters.delete(turnId); reject(new Error('Turn deadline elapsed')); void stop() }, turnTimeoutMs)
136
+ turnWaiters.set(turnId, { resolve, reject, timer })
137
+ })
138
+ }
139
+ try {
140
+ const initialized = await rpc('initialize', { clientInfo: { name: 'pushary_codex_host', version: '0.1.0' }, capabilities: { experimentalApi: adapterConfig.experimentalQuestions === true } })
141
+ if (!initialized?.userAgent?.startsWith('pushary_codex_host/' + adapterConfig.codexVersion + ' ')) throw new Error('Unsupported Codex binary version')
142
+ enqueue({ method: 'initialized' })
143
+ } catch (error) { await stop(); throw error }
144
+ return {
145
+ adapter,
146
+ close: stop,
147
+ waitForTurn,
148
+ startThread: async () => {
149
+ const result = await rpc('thread/start', { cwd, approvalPolicy: 'on-request', approvalsReviewer: 'user', sandbox: 'read-only' })
150
+ adapter.registerThread(result); ownedThreads.add(result.thread.id); return result
151
+ },
152
+ startTurn: (threadId, text, collaborationMode) => {
153
+ if (!ownedThreads.has(threadId)) throw new Error('Thread not owned by this connection')
154
+ return rpc('turn/start', { threadId, input: [{ type: 'text', text }], ...(collaborationMode ? { collaborationMode } : {}) })
155
+ },
156
+ }
157
+ }
package/package.json CHANGED
@@ -1,6 +1,68 @@
1
1
  {
2
2
  "name": "@pushary/codex-app-server",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.0",
4
+ "description": "Partner approvals for live Codex app-server sessions",
5
+ "author": "Pushary <business@pushary.com>",
6
+ "homepage": "https://github.com/Pushary/pushary-codex-app-server#readme",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "https://github.com/Pushary/pushary-codex-app-server.git"
10
+ },
11
+ "bugs": {
12
+ "url": "https://github.com/Pushary/pushary-codex-app-server/issues"
13
+ },
14
+ "license": "MIT",
15
+ "type": "module",
16
+ "main": "./dist/index.cjs",
17
+ "module": "./dist/index.js",
18
+ "types": "./dist/index.d.ts",
19
+ "exports": {
20
+ ".": {
21
+ "types": "./dist/index.d.ts",
22
+ "import": "./dist/index.js",
23
+ "require": "./dist/index.cjs"
24
+ }
25
+ },
26
+ "files": [
27
+ "dist",
28
+ "README.md",
29
+ "COMPATIBILITY.md",
30
+ "LICENSE",
31
+ "NOTICE",
32
+ "examples"
33
+ ],
34
+ "sideEffects": false,
35
+ "engines": {
36
+ "node": ">=22"
37
+ },
38
+ "scripts": {
39
+ "build": "tsup",
40
+ "typecheck": "tsc --noEmit",
41
+ "test": "vitest run && npm run build && npm run test:conformance",
42
+ "test:conformance": "node --test test/conformance.test.mjs test/host-lifecycle.test.mjs",
43
+ "capture:protocol": "node scripts/capture-protocol.mjs",
44
+ "prepublishOnly": "npm run build && npm run typecheck && npm test"
45
+ },
46
+ "dependencies": {
47
+ "@pushary/server": "^2.3.0"
48
+ },
49
+ "devDependencies": {
50
+ "@types/node": "^22.0.0",
51
+ "tsup": "^8.0.0",
52
+ "typescript": "^5.0.0",
53
+ "vitest": "^2.1.0",
54
+ "@openai/codex": "0.160.1",
55
+ "ajv": "^8.17.1"
56
+ },
57
+ "publishConfig": {
58
+ "access": "public"
59
+ },
60
+ "keywords": [
61
+ "pushary",
62
+ "codex",
63
+ "app-server",
64
+ "human-in-the-loop",
65
+ "approvals",
66
+ "partner-api"
67
+ ]
68
+ }