[Init] Initial commit - NetMesh terminal manager
Some checks failed
build-packages / resolve bundled mosh-client (push) Has been cancelled
build-packages / resolve bundled et-client (push) Has been cancelled
build-packages / build-macos (push) Has been cancelled
build-packages / build-windows (push) Has been cancelled
build-packages / build-linux-x64 (push) Has been cancelled
build-packages / build-linux-arm64 (push) Has been cancelled
build-packages / release (push) Has been cancelled
build-packages / update Nix release metadata (push) Has been cancelled
build-packages / bump homebrew tap (push) Has been cancelled
test / lint-and-test (push) Has been cancelled
AI automation / Route event (push) Has been cancelled
AI automation / Hand reopened issue to maintainers (push) Has been cancelled
AI automation / Clean source issue state (push) Has been cancelled
AI automation / Reconcile handoffs (push) Has been cancelled
AI automation / Classify issue (push) Has been cancelled
AI automation / Claude Code smoke (push) Has been cancelled
AI automation / Review issue follow-up (push) Has been cancelled
AI automation / Publish issue follow-up (push) Has been cancelled
AI automation / Implement with Claude Code (push) Has been cancelled
AI automation / Publish implement PR (push) Has been cancelled
AI automation / Continue queued issue comments (push) Has been cancelled
AI automation / Codex review loop (push) Has been cancelled
AI automation / Publish Codex fix (push) Has been cancelled
AI automation / Clear Codex dispatch marker (push) Has been cancelled
AI automation / Own PR re-request Codex (push) Has been cancelled
AI automation / External PR re-request Codex (push) Has been cancelled
AI automation / Poll Codex reaction / retry (push) Has been cancelled
build-et-binaries / build-linux-x64 (push) Has been cancelled
build-et-binaries / build-linux-arm64 (push) Has been cancelled
build-et-binaries / build-macos-universal (push) Has been cancelled
build-et-binaries / build-windows-x64 (push) Has been cancelled
build-et-binaries / release (push) Has been cancelled
Some checks failed
build-packages / resolve bundled mosh-client (push) Has been cancelled
build-packages / resolve bundled et-client (push) Has been cancelled
build-packages / build-macos (push) Has been cancelled
build-packages / build-windows (push) Has been cancelled
build-packages / build-linux-x64 (push) Has been cancelled
build-packages / build-linux-arm64 (push) Has been cancelled
build-packages / release (push) Has been cancelled
build-packages / update Nix release metadata (push) Has been cancelled
build-packages / bump homebrew tap (push) Has been cancelled
test / lint-and-test (push) Has been cancelled
AI automation / Route event (push) Has been cancelled
AI automation / Hand reopened issue to maintainers (push) Has been cancelled
AI automation / Clean source issue state (push) Has been cancelled
AI automation / Reconcile handoffs (push) Has been cancelled
AI automation / Classify issue (push) Has been cancelled
AI automation / Claude Code smoke (push) Has been cancelled
AI automation / Review issue follow-up (push) Has been cancelled
AI automation / Publish issue follow-up (push) Has been cancelled
AI automation / Implement with Claude Code (push) Has been cancelled
AI automation / Publish implement PR (push) Has been cancelled
AI automation / Continue queued issue comments (push) Has been cancelled
AI automation / Codex review loop (push) Has been cancelled
AI automation / Publish Codex fix (push) Has been cancelled
AI automation / Clear Codex dispatch marker (push) Has been cancelled
AI automation / Own PR re-request Codex (push) Has been cancelled
AI automation / External PR re-request Codex (push) Has been cancelled
AI automation / Poll Codex reaction / retry (push) Has been cancelled
build-et-binaries / build-linux-x64 (push) Has been cancelled
build-et-binaries / build-linux-arm64 (push) Has been cancelled
build-et-binaries / build-macos-universal (push) Has been cancelled
build-et-binaries / build-windows-x64 (push) Has been cancelled
build-et-binaries / release (push) Has been cancelled
This commit is contained in:
198
infrastructure/ai/shared/codexAppServerInteractions.ts
Normal file
198
infrastructure/ai/shared/codexAppServerInteractions.ts
Normal file
@@ -0,0 +1,198 @@
|
||||
import {
|
||||
clearPendingApprovalIds,
|
||||
registerExternalApproval,
|
||||
} from './approvalGate';
|
||||
|
||||
export type CodexApprovalDecision = 'once' | 'session' | 'reject' | 'cancel';
|
||||
export type CodexCommandApprovalDecision =
|
||||
| 'accept'
|
||||
| 'acceptForSession'
|
||||
| 'decline'
|
||||
| 'cancel'
|
||||
| Record<string, unknown>;
|
||||
|
||||
export interface CodexUserInputQuestion {
|
||||
id: string;
|
||||
header: string;
|
||||
question: string;
|
||||
isOther: boolean;
|
||||
isSecret: boolean;
|
||||
options: Array<{ label: string; description: string }> | null;
|
||||
}
|
||||
|
||||
export type CodexAppServerInteraction =
|
||||
| {
|
||||
interactionId: string;
|
||||
source: 'codex-app-server';
|
||||
kind: 'command' | 'file-change' | 'permissions';
|
||||
requestId: string;
|
||||
chatSessionId: string;
|
||||
itemId?: string;
|
||||
toolName: string;
|
||||
args: Record<string, unknown>;
|
||||
availableDecisions?: CodexCommandApprovalDecision[];
|
||||
}
|
||||
| {
|
||||
interactionId: string;
|
||||
source: 'codex-app-server';
|
||||
kind: 'user-input';
|
||||
requestId: string;
|
||||
chatSessionId: string;
|
||||
itemId?: string;
|
||||
questions: CodexUserInputQuestion[];
|
||||
autoResolutionMs?: number | null;
|
||||
};
|
||||
|
||||
type InteractionListener = (interaction: CodexAppServerInteraction) => void;
|
||||
type ClearedListener = (interactionIds: string[]) => void;
|
||||
|
||||
const pendingInteractions = new Map<string, CodexAppServerInteraction>();
|
||||
const listeners = new Set<InteractionListener>();
|
||||
const clearedListeners = new Set<ClearedListener>();
|
||||
|
||||
function registerCodexApproval(
|
||||
interaction: Extract<CodexAppServerInteraction, { kind: 'command' | 'file-change' | 'permissions' }>,
|
||||
): void {
|
||||
const allowSession = interaction.kind === 'command'
|
||||
? interaction.availableDecisions?.includes('acceptForSession') === true
|
||||
: true;
|
||||
registerExternalApproval({
|
||||
toolCallId: interaction.interactionId,
|
||||
itemId: interaction.itemId,
|
||||
toolName: interaction.toolName,
|
||||
args: interaction.args,
|
||||
chatSessionId: interaction.chatSessionId,
|
||||
source: 'codex-app-server',
|
||||
approvalType: interaction.kind,
|
||||
allowSession,
|
||||
}, (resolution) => {
|
||||
const decision: CodexApprovalDecision = resolution.cancelled
|
||||
? 'cancel'
|
||||
: resolution.approved
|
||||
? resolution.scope
|
||||
: 'reject';
|
||||
void respondCodexApproval(interaction.interactionId, decision).catch((error) => {
|
||||
console.error('[Codex App Server] Failed to respond to approval:', error);
|
||||
if (pendingInteractions.has(interaction.interactionId)) registerCodexApproval(interaction);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function notifyCleared(interactionIds: string[]): void {
|
||||
if (interactionIds.length === 0) return;
|
||||
for (const listener of clearedListeners) {
|
||||
try { listener(interactionIds); } catch { /* ignore listener failures */ }
|
||||
}
|
||||
}
|
||||
|
||||
let bridgeInstallCount = 0;
|
||||
let bridgeTeardown: (() => void) | null = null;
|
||||
|
||||
function installCodexAppServerInteractionBridge(): () => void {
|
||||
const bridge = (window as unknown as {
|
||||
netcatty?: {
|
||||
onCodexAppServerInteractionRequest?: (
|
||||
cb: (payload: CodexAppServerInteraction) => void,
|
||||
) => () => void;
|
||||
onCodexAppServerInteractionCleared?: (
|
||||
cb: (payload: { interactionIds: string[] }) => void,
|
||||
) => () => void;
|
||||
};
|
||||
}).netcatty;
|
||||
if (!bridge?.onCodexAppServerInteractionRequest) return () => {};
|
||||
|
||||
const unsubscribeRequest = bridge.onCodexAppServerInteractionRequest((interaction) => {
|
||||
if (!interaction?.interactionId) return;
|
||||
pendingInteractions.set(interaction.interactionId, interaction);
|
||||
if (interaction.kind !== 'user-input') {
|
||||
registerCodexApproval(interaction);
|
||||
return;
|
||||
}
|
||||
for (const listener of listeners) {
|
||||
try { listener(interaction); } catch { /* ignore listener failures */ }
|
||||
}
|
||||
});
|
||||
const unsubscribeCleared = bridge.onCodexAppServerInteractionCleared?.((payload) => {
|
||||
const cleared: string[] = [];
|
||||
for (const interactionId of payload?.interactionIds || []) {
|
||||
if (pendingInteractions.delete(interactionId)) cleared.push(interactionId);
|
||||
}
|
||||
clearPendingApprovalIds(cleared);
|
||||
notifyCleared(cleared);
|
||||
});
|
||||
return () => {
|
||||
unsubscribeRequest();
|
||||
unsubscribeCleared?.();
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* App-singleton IPC bridge (same pattern as setupMcpApprovalBridge).
|
||||
* Ref-counted so StrictMode remount / accidental multi-mount does not stack
|
||||
* ipcRenderer listeners.
|
||||
*/
|
||||
export function setupCodexAppServerInteractionBridge(): () => void {
|
||||
bridgeInstallCount += 1;
|
||||
if (bridgeInstallCount === 1) {
|
||||
bridgeTeardown = installCodexAppServerInteractionBridge();
|
||||
}
|
||||
return () => {
|
||||
bridgeInstallCount = Math.max(0, bridgeInstallCount - 1);
|
||||
if (bridgeInstallCount === 0) {
|
||||
bridgeTeardown?.();
|
||||
bridgeTeardown = null;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function onCodexAppServerInteraction(listener: InteractionListener): () => void {
|
||||
listeners.add(listener);
|
||||
return () => { listeners.delete(listener); };
|
||||
}
|
||||
|
||||
export function onCodexAppServerInteractionCleared(listener: ClearedListener): () => void {
|
||||
clearedListeners.add(listener);
|
||||
return () => { clearedListeners.delete(listener); };
|
||||
}
|
||||
|
||||
export function replayPendingCodexAppServerInteractions(listener: InteractionListener): void {
|
||||
for (const interaction of pendingInteractions.values()) {
|
||||
if (interaction.kind !== 'user-input') continue;
|
||||
try { listener(interaction); } catch { /* ignore listener failures */ }
|
||||
}
|
||||
}
|
||||
|
||||
async function respond(payload: Record<string, unknown>): Promise<void> {
|
||||
const interactionId = String(payload.interactionId || '');
|
||||
if (!interactionId) return;
|
||||
const bridge = (window as unknown as {
|
||||
netcatty?: {
|
||||
respondCodexAppServerInteraction?: (
|
||||
response: Record<string, unknown>,
|
||||
) => Promise<unknown>;
|
||||
};
|
||||
}).netcatty;
|
||||
if (!bridge?.respondCodexAppServerInteraction) {
|
||||
throw new Error('Codex App Server interaction bridge is unavailable');
|
||||
}
|
||||
const result = await bridge.respondCodexAppServerInteraction(payload) as { ok?: boolean; error?: string } | undefined;
|
||||
if (result?.ok === false) {
|
||||
throw new Error(result.error || 'Failed to respond to Codex App Server interaction');
|
||||
}
|
||||
pendingInteractions.delete(interactionId);
|
||||
notifyCleared([interactionId]);
|
||||
}
|
||||
|
||||
export function respondCodexApproval(
|
||||
interactionId: string,
|
||||
decision: CodexApprovalDecision,
|
||||
): Promise<void> {
|
||||
return respond({ interactionId, decision });
|
||||
}
|
||||
|
||||
export function respondCodexUserInput(
|
||||
interactionId: string,
|
||||
answers: Record<string, { answers: string[] }>,
|
||||
): Promise<void> {
|
||||
return respond({ interactionId, answers });
|
||||
}
|
||||
Reference in New Issue
Block a user