@principles/codex-adapter 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.
- package/dist/codec/index.d.ts +6 -0
- package/dist/codec/index.js +5 -0
- package/dist/codec/input-decoder.d.ts +12 -0
- package/dist/codec/input-decoder.js +84 -0
- package/dist/codec/output-encoder.d.ts +32 -0
- package/dist/codec/output-encoder.js +51 -0
- package/dist/host-adapter.d.ts +30 -0
- package/dist/host-adapter.js +24 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +10 -0
- package/dist/pd-hook.d.ts +9 -0
- package/dist/pd-hook.js +98 -0
- package/package.json +52 -0
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Codec barrel — input decoder + output encoder for Codex CLI.
|
|
3
|
+
*/
|
|
4
|
+
export { decodeCodexInput, CodexDecoderError, CODEX_EVENT_PRE_TOOL_USE, CODEX_EVENT_POST_TOOL_USE, CODEX_EVENT_USER_PROMPT_SUBMIT, CODEX_EVENT_SESSION_START, CODEX_EVENT_SESSION_END, } from './input-decoder.js';
|
|
5
|
+
export { encodeCodexOutput, codexOutputFieldsAreWhitelisted, CodexEncoderError, } from './output-encoder.js';
|
|
6
|
+
export type { CodexPreToolUseOutput, CodexPostToolUseOutput, CodexUserPromptSubmitOutput, CodexSessionStartOutput, CodexHookOutput, } from './output-encoder.js';
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Codec barrel — input decoder + output encoder for Codex CLI.
|
|
3
|
+
*/
|
|
4
|
+
export { decodeCodexInput, CodexDecoderError, CODEX_EVENT_PRE_TOOL_USE, CODEX_EVENT_POST_TOOL_USE, CODEX_EVENT_USER_PROMPT_SUBMIT, CODEX_EVENT_SESSION_START, CODEX_EVENT_SESSION_END, } from './input-decoder.js';
|
|
5
|
+
export { encodeCodexOutput, codexOutputFieldsAreWhitelisted, CodexEncoderError, } from './output-encoder.js';
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { HostEvent } from '@principles/core/host';
|
|
2
|
+
export declare const CODEX_EVENT_PRE_TOOL_USE = "PreToolUse";
|
|
3
|
+
export declare const CODEX_EVENT_POST_TOOL_USE = "PostToolUse";
|
|
4
|
+
export declare const CODEX_EVENT_USER_PROMPT_SUBMIT = "UserPromptSubmit";
|
|
5
|
+
export declare const CODEX_EVENT_SESSION_START = "SessionStart";
|
|
6
|
+
export declare const CODEX_EVENT_SESSION_END = "SessionEnd";
|
|
7
|
+
export declare class CodexDecoderError extends Error {
|
|
8
|
+
readonly reason: string;
|
|
9
|
+
readonly nextAction: string;
|
|
10
|
+
constructor(reason: string, nextAction: string);
|
|
11
|
+
}
|
|
12
|
+
export declare function decodeCodexInput(raw: unknown): HostEvent;
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
export const CODEX_EVENT_PRE_TOOL_USE = 'PreToolUse';
|
|
2
|
+
export const CODEX_EVENT_POST_TOOL_USE = 'PostToolUse';
|
|
3
|
+
export const CODEX_EVENT_USER_PROMPT_SUBMIT = 'UserPromptSubmit';
|
|
4
|
+
export const CODEX_EVENT_SESSION_START = 'SessionStart';
|
|
5
|
+
export const CODEX_EVENT_SESSION_END = 'SessionEnd';
|
|
6
|
+
const EVENT_KINDS = new Map([
|
|
7
|
+
[CODEX_EVENT_PRE_TOOL_USE, 'before_tool_call'],
|
|
8
|
+
[CODEX_EVENT_POST_TOOL_USE, 'after_tool_call'],
|
|
9
|
+
[CODEX_EVENT_USER_PROMPT_SUBMIT, 'before_prompt_build'],
|
|
10
|
+
[CODEX_EVENT_SESSION_START, 'session_start'],
|
|
11
|
+
]);
|
|
12
|
+
export class CodexDecoderError extends Error {
|
|
13
|
+
reason;
|
|
14
|
+
nextAction;
|
|
15
|
+
constructor(reason, nextAction) {
|
|
16
|
+
super(`Codex input decode failed: ${reason}`);
|
|
17
|
+
this.reason = reason;
|
|
18
|
+
this.nextAction = nextAction;
|
|
19
|
+
this.name = 'CodexDecoderError';
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
function isRecord(value) {
|
|
23
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
24
|
+
}
|
|
25
|
+
function own(value, key) {
|
|
26
|
+
return Object.hasOwn(value, key) ? Object.getOwnPropertyDescriptor(value, key)?.value : undefined;
|
|
27
|
+
}
|
|
28
|
+
function requiredString(value, key) {
|
|
29
|
+
const candidate = own(value, key);
|
|
30
|
+
if (typeof candidate !== 'string' || candidate.trim().length === 0) {
|
|
31
|
+
throw new CodexDecoderError(`missing or malformed required field "${key}"`, `Use the exact codex-cli 0.147.0 ${key} field.`);
|
|
32
|
+
}
|
|
33
|
+
return candidate;
|
|
34
|
+
}
|
|
35
|
+
function requiredNullableString(value, key) {
|
|
36
|
+
if (!Object.hasOwn(value, key))
|
|
37
|
+
throw new CodexDecoderError(`missing required field "${key}"`, `Use the exact codex-cli 0.147.0 ${key} field.`);
|
|
38
|
+
const candidate = own(value, key);
|
|
39
|
+
if (candidate !== null && typeof candidate !== 'string') {
|
|
40
|
+
throw new CodexDecoderError(`malformed required field "${key}"`, `${key} must be a string or null.`);
|
|
41
|
+
}
|
|
42
|
+
return candidate;
|
|
43
|
+
}
|
|
44
|
+
function requiredUnknown(value, key) {
|
|
45
|
+
if (!Object.hasOwn(value, key))
|
|
46
|
+
throw new CodexDecoderError(`missing required field "${key}"`, `Use the exact codex-cli 0.147.0 ${key} field.`);
|
|
47
|
+
return own(value, key);
|
|
48
|
+
}
|
|
49
|
+
function validateCommon(raw, needsTurn) {
|
|
50
|
+
const sessionId = requiredString(raw, 'session_id');
|
|
51
|
+
requiredNullableString(raw, 'transcript_path');
|
|
52
|
+
const workspaceDir = requiredString(raw, 'cwd');
|
|
53
|
+
requiredString(raw, 'model');
|
|
54
|
+
requiredString(raw, 'permission_mode');
|
|
55
|
+
const turnId = needsTurn ? requiredString(raw, 'turn_id') : undefined;
|
|
56
|
+
return { workspaceDir, sessionId, ...(turnId ? { turnId } : {}) };
|
|
57
|
+
}
|
|
58
|
+
export function decodeCodexInput(raw) {
|
|
59
|
+
if (!isRecord(raw))
|
|
60
|
+
throw new CodexDecoderError('stdin payload is not a JSON object', 'Run this executable only as a Codex command hook.');
|
|
61
|
+
const eventName = requiredString(raw, 'hook_event_name');
|
|
62
|
+
const kind = EVENT_KINDS.get(eventName);
|
|
63
|
+
if (!kind)
|
|
64
|
+
throw new CodexDecoderError(`unknown hook_event_name "${eventName}"`, 'Configure only PreToolUse, PostToolUse, UserPromptSubmit, or SessionStart.');
|
|
65
|
+
const common = validateCommon(raw, kind !== 'session_start');
|
|
66
|
+
let context;
|
|
67
|
+
let rawPayload = raw;
|
|
68
|
+
if (kind === 'before_tool_call' || kind === 'after_tool_call') {
|
|
69
|
+
const toolName = requiredString(raw, 'tool_name');
|
|
70
|
+
const toolInput = requiredUnknown(raw, 'tool_input');
|
|
71
|
+
requiredString(raw, 'tool_use_id');
|
|
72
|
+
const toolOutput = kind === 'after_tool_call' ? requiredUnknown(raw, 'tool_response') : undefined;
|
|
73
|
+
context = { ...common, toolName, toolInput, ...(kind === 'after_tool_call' ? { toolOutput } : {}) };
|
|
74
|
+
rawPayload = { toolInput: { toolName, params: toolInput } };
|
|
75
|
+
}
|
|
76
|
+
else if (kind === 'before_prompt_build') {
|
|
77
|
+
context = { ...common, promptContent: requiredString(raw, 'prompt') };
|
|
78
|
+
}
|
|
79
|
+
else {
|
|
80
|
+
context = { ...common, source: requiredString(raw, 'source') };
|
|
81
|
+
}
|
|
82
|
+
const source = `codex:${eventName.replace(/([a-z0-9])([A-Z])/g, '$1_$2').toLowerCase()}`;
|
|
83
|
+
return { kind, context, rawPayload, source };
|
|
84
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { HostEventResult } from '@principles/core/host';
|
|
2
|
+
type HookName = 'PreToolUse' | 'PostToolUse' | 'UserPromptSubmit' | 'SessionStart';
|
|
3
|
+
interface HookSpecificOutput {
|
|
4
|
+
hookEventName: HookName;
|
|
5
|
+
permissionDecision?: 'deny';
|
|
6
|
+
permissionDecisionReason?: string;
|
|
7
|
+
additionalContext?: string;
|
|
8
|
+
}
|
|
9
|
+
export interface CodexPreToolUseOutput {
|
|
10
|
+
hookSpecificOutput: HookSpecificOutput;
|
|
11
|
+
}
|
|
12
|
+
export interface CodexPostToolUseOutput {
|
|
13
|
+
hookSpecificOutput: HookSpecificOutput;
|
|
14
|
+
}
|
|
15
|
+
export interface CodexUserPromptSubmitOutput {
|
|
16
|
+
hookSpecificOutput: HookSpecificOutput;
|
|
17
|
+
}
|
|
18
|
+
export interface CodexSessionStartOutput {
|
|
19
|
+
hookSpecificOutput: HookSpecificOutput;
|
|
20
|
+
}
|
|
21
|
+
export type CodexHookOutput = CodexPreToolUseOutput | CodexPostToolUseOutput | CodexUserPromptSubmitOutput | CodexSessionStartOutput;
|
|
22
|
+
export declare class CodexEncoderError extends Error {
|
|
23
|
+
readonly reason: string;
|
|
24
|
+
readonly nextAction: string;
|
|
25
|
+
constructor(reason: string, nextAction: string);
|
|
26
|
+
}
|
|
27
|
+
export declare function encodeCodexOutput(result: HostEventResult, kind: string): CodexHookOutput;
|
|
28
|
+
export declare function codexOutputFieldsAreWhitelisted(output: unknown): {
|
|
29
|
+
ok: boolean;
|
|
30
|
+
violators: string[];
|
|
31
|
+
};
|
|
32
|
+
export {};
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
export class CodexEncoderError extends Error {
|
|
2
|
+
reason;
|
|
3
|
+
nextAction;
|
|
4
|
+
constructor(reason, nextAction) {
|
|
5
|
+
super(`Codex output encode failed: ${reason}`);
|
|
6
|
+
this.reason = reason;
|
|
7
|
+
this.nextAction = nextAction;
|
|
8
|
+
this.name = 'CodexEncoderError';
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
function nonEmpty(value) { return typeof value === 'string' && value.trim().length > 0; }
|
|
12
|
+
export function encodeCodexOutput(result, kind) {
|
|
13
|
+
if (result.modifiedInput !== undefined)
|
|
14
|
+
throw new CodexEncoderError('modifiedInput is unsupported by the PD Codex adapter', 'Deny or allow the original input.');
|
|
15
|
+
if (result.additionalContext !== undefined && !nonEmpty(result.additionalContext))
|
|
16
|
+
throw new CodexEncoderError('additionalContext must be non-empty', 'Drop or populate additionalContext.');
|
|
17
|
+
const names = new Map([['before_tool_call', 'PreToolUse'], ['after_tool_call', 'PostToolUse'], ['before_prompt_build', 'UserPromptSubmit'], ['session_start', 'SessionStart']]);
|
|
18
|
+
const hookEventName = names.get(kind);
|
|
19
|
+
if (!hookEventName)
|
|
20
|
+
throw new CodexEncoderError(`unknown event kind "${kind}"`, 'Encode only a supported Codex hook event.');
|
|
21
|
+
if (result.decision === 'deny' && kind !== 'before_tool_call')
|
|
22
|
+
throw new CodexEncoderError(`deny is unsupported for ${kind}`, 'Return the route-compatible host result.');
|
|
23
|
+
const hookSpecificOutput = { hookEventName };
|
|
24
|
+
if (result.decision === 'deny') {
|
|
25
|
+
if (!nonEmpty(result.reason))
|
|
26
|
+
throw new CodexEncoderError('deny requires a non-empty reason', 'Supply an operator-readable reason.');
|
|
27
|
+
hookSpecificOutput.permissionDecision = 'deny';
|
|
28
|
+
hookSpecificOutput.permissionDecisionReason = result.reason.trim();
|
|
29
|
+
}
|
|
30
|
+
else if (result.reason !== undefined) {
|
|
31
|
+
throw new CodexEncoderError('reason without deny is unsupported', 'Drop reason for a non-deny decision.');
|
|
32
|
+
}
|
|
33
|
+
if (result.additionalContext !== undefined)
|
|
34
|
+
hookSpecificOutput.additionalContext = result.additionalContext;
|
|
35
|
+
return { hookSpecificOutput };
|
|
36
|
+
}
|
|
37
|
+
const TOP_LEVEL = new Set(['hookSpecificOutput']);
|
|
38
|
+
const NESTED = new Set(['hookEventName', 'permissionDecision', 'permissionDecisionReason', 'additionalContext']);
|
|
39
|
+
export function codexOutputFieldsAreWhitelisted(output) {
|
|
40
|
+
if (typeof output !== 'object' || output === null || Array.isArray(output))
|
|
41
|
+
return { ok: false, violators: ['<not-an-object>'] };
|
|
42
|
+
const top = Object.keys(output).filter((key) => !TOP_LEVEL.has(key));
|
|
43
|
+
const descriptor = Object.getOwnPropertyDescriptor(output, 'hookSpecificOutput')?.value;
|
|
44
|
+
if (typeof descriptor !== 'object' || descriptor === null || Array.isArray(descriptor))
|
|
45
|
+
return { ok: false, violators: [...top, 'hookSpecificOutput'] };
|
|
46
|
+
const nested = Object.keys(descriptor).filter((key) => !NESTED.has(key)).map((key) => `hookSpecificOutput.${key}`);
|
|
47
|
+
const name = Object.getOwnPropertyDescriptor(descriptor, 'hookEventName')?.value;
|
|
48
|
+
if (!['PreToolUse', 'PostToolUse', 'UserPromptSubmit', 'SessionStart'].includes(String(name)))
|
|
49
|
+
nested.push('hookSpecificOutput.hookEventName');
|
|
50
|
+
return { ok: top.length + nested.length === 0, violators: [...top, ...nested] };
|
|
51
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CodexHooksHostAdapter — implements HostAdapter for OpenAI Codex CLI (ADR-0020 §2.5)
|
|
3
|
+
*
|
|
4
|
+
* Codex CLI spawns `pd-hook.js` as a subprocess for each hook event, writes
|
|
5
|
+
* the event payload as JSON on stdin, and reads the output as JSON on stdout.
|
|
6
|
+
* This adapter owns:
|
|
7
|
+
* 1. Decoding the raw stdin JSON into a unified HostEvent (via codec/input-decoder).
|
|
8
|
+
* 2. Encoding a unified HostEventResult into Codex's camelCase stdout JSON
|
|
9
|
+
* (via codec/output-encoder).
|
|
10
|
+
*
|
|
11
|
+
* The adapter does NOT own hook business logic (pain detection, principle
|
|
12
|
+
* injection, gate enforcement). Those run in the openclaw-plugin/core code and
|
|
13
|
+
* are invoked by `pd-hook.js` after decodeEvent() and before encodeOutput().
|
|
14
|
+
*
|
|
15
|
+
* MVP scope (ADR-0014 / ADR-0020):
|
|
16
|
+
* - Subscribes to 4 events: before_tool_call, after_tool_call, before_prompt_build, session_start.
|
|
17
|
+
* - session_end is deferred (observe-only, no MVP-Core activation path uses it).
|
|
18
|
+
* - hostKind = 'subprocess' (Codex spawns pd-hook.js; OpenClaw is 'inprocess').
|
|
19
|
+
*
|
|
20
|
+
* Feature flag: when `host.codex.enabled = false` (default), `pd-hook.js`
|
|
21
|
+
* short-circuits to `{} + exit 0` before invoking this adapter (rc-9).
|
|
22
|
+
*/
|
|
23
|
+
import type { HostAdapter, HostEvent, HostEventKind, HostEventResult } from '@principles/core/host';
|
|
24
|
+
export declare class CodexHooksHostAdapter implements HostAdapter {
|
|
25
|
+
readonly hostId = "codex";
|
|
26
|
+
readonly hostKind: 'subprocess';
|
|
27
|
+
subscribedEvents(): readonly HostEventKind[];
|
|
28
|
+
decodeEvent(raw: unknown): HostEvent;
|
|
29
|
+
encodeOutput(result: HostEventResult, kind: HostEventKind): unknown;
|
|
30
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { decodeCodexInput, encodeCodexOutput } from './codec/index.js';
|
|
2
|
+
const SUBSCRIBED_EVENTS = [
|
|
3
|
+
'before_tool_call',
|
|
4
|
+
'after_tool_call',
|
|
5
|
+
'before_prompt_build',
|
|
6
|
+
'session_start',
|
|
7
|
+
// 'session_end' deferred to post-MVP
|
|
8
|
+
];
|
|
9
|
+
export class CodexHooksHostAdapter {
|
|
10
|
+
hostId = 'codex';
|
|
11
|
+
hostKind = 'subprocess';
|
|
12
|
+
// eslint-disable-next-line @typescript-eslint/class-methods-use-this
|
|
13
|
+
subscribedEvents() {
|
|
14
|
+
return SUBSCRIBED_EVENTS;
|
|
15
|
+
}
|
|
16
|
+
// eslint-disable-next-line @typescript-eslint/class-methods-use-this
|
|
17
|
+
decodeEvent(raw) {
|
|
18
|
+
return decodeCodexInput(raw);
|
|
19
|
+
}
|
|
20
|
+
// eslint-disable-next-line @typescript-eslint/class-methods-use-this
|
|
21
|
+
encodeOutput(result, kind) {
|
|
22
|
+
return encodeCodexOutput(result, kind);
|
|
23
|
+
}
|
|
24
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @principles/codex-adapter — Codex CLI Host Adapter (ADR-0020)
|
|
3
|
+
*
|
|
4
|
+
* Implements HostAdapter for OpenAI Codex CLI's stdin/stdout JSON hook model.
|
|
5
|
+
* This package is INDEPENDENT of `packages/openclaw-plugin/` — the two hosts
|
|
6
|
+
* have different extension models (Codex: subprocess; OpenClaw: in-process).
|
|
7
|
+
*/
|
|
8
|
+
export { CodexHooksHostAdapter } from './host-adapter.js';
|
|
9
|
+
export { processHookInvocation } from './pd-hook.js';
|
|
10
|
+
export type { PdHookResult } from './pd-hook.js';
|
|
11
|
+
export * from './codec/index.js';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @principles/codex-adapter — Codex CLI Host Adapter (ADR-0020)
|
|
3
|
+
*
|
|
4
|
+
* Implements HostAdapter for OpenAI Codex CLI's stdin/stdout JSON hook model.
|
|
5
|
+
* This package is INDEPENDENT of `packages/openclaw-plugin/` — the two hosts
|
|
6
|
+
* have different extension models (Codex: subprocess; OpenClaw: in-process).
|
|
7
|
+
*/
|
|
8
|
+
export { CodexHooksHostAdapter } from './host-adapter.js';
|
|
9
|
+
export { processHookInvocation } from './pd-hook.js';
|
|
10
|
+
export * from './codec/index.js';
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
type EnvMap = Record<string, string | undefined>;
|
|
3
|
+
export interface PdHookResult {
|
|
4
|
+
stdout: unknown;
|
|
5
|
+
exitCode: number;
|
|
6
|
+
stderr: string[];
|
|
7
|
+
}
|
|
8
|
+
export declare function processHookInvocation(rawStdin: string, _env?: EnvMap, cwd?: string): Promise<PdHookResult>;
|
|
9
|
+
export {};
|
package/dist/pd-hook.js
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { readFileSync } from 'node:fs';
|
|
3
|
+
import process from 'node:process';
|
|
4
|
+
import { pathToFileURL } from 'node:url';
|
|
5
|
+
import { createProductionHostRuntime, loadPdConfigForPlugin, resolveNearestPdWorkspace } from '@principles/host-runtime';
|
|
6
|
+
import { computeFeatureFlagsFromConfig } from '@principles/core/runtime-v2';
|
|
7
|
+
import { CodexHooksHostAdapter } from './host-adapter.js';
|
|
8
|
+
import { CodexDecoderError, CodexEncoderError } from './codec/index.js';
|
|
9
|
+
const MAX_DIAGNOSTIC = 500;
|
|
10
|
+
function diagnostic(reason, nextAction) {
|
|
11
|
+
const boundedReason = reason.replace(/\s+/g, ' ').trim().slice(0, MAX_DIAGNOSTIC);
|
|
12
|
+
const boundedNextAction = nextAction.replace(/\s+/g, ' ').trim().slice(0, MAX_DIAGNOSTIC);
|
|
13
|
+
return `[PD] status=degraded reason=${boundedReason} nextAction=${boundedNextAction}`;
|
|
14
|
+
}
|
|
15
|
+
function errorMessage(error) {
|
|
16
|
+
return error instanceof Error ? error.message.slice(0, MAX_DIAGNOSTIC) : 'unknown_error';
|
|
17
|
+
}
|
|
18
|
+
export async function processHookInvocation(rawStdin, _env = process.env, cwd = process.cwd()) {
|
|
19
|
+
let parsed;
|
|
20
|
+
try {
|
|
21
|
+
parsed = JSON.parse(rawStdin);
|
|
22
|
+
}
|
|
23
|
+
catch (error) {
|
|
24
|
+
return { stdout: {}, exitCode: 0, stderr: [diagnostic(`stdin_json_invalid:${errorMessage(error)}`, 'Verify Codex invokes the PD hook with one JSON object.')] };
|
|
25
|
+
}
|
|
26
|
+
const adapter = new CodexHooksHostAdapter();
|
|
27
|
+
let event;
|
|
28
|
+
try {
|
|
29
|
+
event = adapter.decodeEvent(parsed);
|
|
30
|
+
}
|
|
31
|
+
catch (error) {
|
|
32
|
+
const reason = error instanceof CodexDecoderError ? error.reason : `decode_threw:${errorMessage(error)}`;
|
|
33
|
+
const nextAction = error instanceof CodexDecoderError ? error.nextAction : 'Inspect the Codex 0.147 hook payload.';
|
|
34
|
+
return { stdout: {}, exitCode: 0, stderr: [diagnostic(reason, nextAction)] };
|
|
35
|
+
}
|
|
36
|
+
// Codex 0.147 supplies the invocation cwd. It is the authoritative starting
|
|
37
|
+
// point; a process-global environment variable can otherwise route one
|
|
38
|
+
// Workspace's hook into another Workspace's business state.
|
|
39
|
+
const requestedCwd = event.context.workspaceDir || cwd;
|
|
40
|
+
const resolution = resolveNearestPdWorkspace(requestedCwd);
|
|
41
|
+
if (!resolution.ok)
|
|
42
|
+
return { stdout: {}, exitCode: 0, stderr: [diagnostic(resolution.reason, resolution.nextAction)] };
|
|
43
|
+
event = { ...event, context: { ...event.context, workspaceDir: resolution.workspaceDir } };
|
|
44
|
+
const config = loadPdConfigForPlugin(resolution.workspaceDir);
|
|
45
|
+
if (!config.ok) {
|
|
46
|
+
const [first] = config.errors;
|
|
47
|
+
return { stdout: {}, exitCode: 0, stderr: [diagnostic(first?.reason ?? 'pd_config_invalid', first?.nextAction ?? 'Repair .pd/config.yaml.')] };
|
|
48
|
+
}
|
|
49
|
+
const { flags } = computeFeatureFlagsFromConfig(config.effective);
|
|
50
|
+
if (flags['host.codex']?.enabled !== true) {
|
|
51
|
+
return { stdout: {}, exitCode: 0, stderr: [diagnostic('host.codex_disabled', 'Set features.host.codex.enabled=true in the selected Workspace to enable PD.')] };
|
|
52
|
+
}
|
|
53
|
+
try {
|
|
54
|
+
if (event.kind === 'session_start') {
|
|
55
|
+
const health = await createProductionHostRuntime().health(resolution.workspaceDir);
|
|
56
|
+
if (!health.ok)
|
|
57
|
+
return { stdout: {}, exitCode: 0, stderr: [diagnostic(health.reason ?? 'runtime_unhealthy', health.nextAction ?? 'Inspect the Workspace runtime.')] };
|
|
58
|
+
return { stdout: adapter.encodeOutput({ decision: 'allow', source: event.source }, 'session_start'), exitCode: 0, stderr: [] };
|
|
59
|
+
}
|
|
60
|
+
const result = await createProductionHostRuntime().dispatch(event);
|
|
61
|
+
const stderr = (result.warnings ?? []).slice(0, 16).map((warning) => diagnostic(warning, 'Inspect PD Workspace state and retry; the hook failed open.'));
|
|
62
|
+
return { stdout: adapter.encodeOutput(result, event.kind), exitCode: 0, stderr };
|
|
63
|
+
}
|
|
64
|
+
catch (error) {
|
|
65
|
+
const reason = error instanceof CodexEncoderError ? error.reason : `runtime_failed:${errorMessage(error)}`;
|
|
66
|
+
const nextAction = error instanceof CodexEncoderError ? error.nextAction : 'Inspect PD Workspace state and retry; the hook failed open.';
|
|
67
|
+
return { stdout: {}, exitCode: 0, stderr: [diagnostic(reason, nextAction)] };
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
async function main() {
|
|
71
|
+
let raw;
|
|
72
|
+
try {
|
|
73
|
+
raw = readFileSync(0, 'utf8');
|
|
74
|
+
}
|
|
75
|
+
catch (error) {
|
|
76
|
+
process.stderr.write(`${diagnostic(`stdin_read_failed:${errorMessage(error)}`, 'Run the hook from Codex with JSON stdin.')}\n`);
|
|
77
|
+
process.stdout.write('{}\n');
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
let result;
|
|
81
|
+
try {
|
|
82
|
+
result = await processHookInvocation(raw);
|
|
83
|
+
}
|
|
84
|
+
catch (error) {
|
|
85
|
+
// Fail-open belt for an unexpected pre-dispatch throw (e.g. a workspace
|
|
86
|
+
// resolution race): Codex must still receive exactly one JSON object on
|
|
87
|
+
// stdout and a bounded diagnostic on stderr — never a bare crash.
|
|
88
|
+
process.stderr.write(`${diagnostic(`hook_pipeline_unexpected:${errorMessage(error)}`, 'Retry the tool call; if it repeats, inspect PD stderr and the Workspace .pd/config.yaml state.')}\n`);
|
|
89
|
+
result = { stdout: {}, exitCode: 0, stderr: [] };
|
|
90
|
+
}
|
|
91
|
+
for (const line of result.stderr)
|
|
92
|
+
process.stderr.write(`${line}\n`);
|
|
93
|
+
process.stdout.write(`${JSON.stringify(result.stdout)}\n`);
|
|
94
|
+
process.exitCode = result.exitCode;
|
|
95
|
+
}
|
|
96
|
+
const [, entry] = process.argv;
|
|
97
|
+
if (entry && import.meta.url === pathToFileURL(entry).href)
|
|
98
|
+
void main();
|
package/package.json
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@principles/codex-adapter",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Codex CLI host adapter for Principles Disciple — implements HostAdapter interface for OpenAI Codex CLI's stdin/stdout JSON hook model (ADR-0020).",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"default": "./dist/index.js"
|
|
12
|
+
},
|
|
13
|
+
"./pd-hook": {
|
|
14
|
+
"types": "./dist/pd-hook.d.ts",
|
|
15
|
+
"default": "./dist/pd-hook.js"
|
|
16
|
+
},
|
|
17
|
+
"./host-adapter": {
|
|
18
|
+
"types": "./dist/host-adapter.d.ts",
|
|
19
|
+
"default": "./dist/host-adapter.js"
|
|
20
|
+
},
|
|
21
|
+
"./package.json": "./package.json"
|
|
22
|
+
},
|
|
23
|
+
"files": [
|
|
24
|
+
"dist",
|
|
25
|
+
"README.md"
|
|
26
|
+
],
|
|
27
|
+
"scripts": {
|
|
28
|
+
"build": "tsc",
|
|
29
|
+
"test": "npm run build && vitest run",
|
|
30
|
+
"test:coverage": "vitest run --coverage",
|
|
31
|
+
"lint": "eslint \"src/**/*.ts\""
|
|
32
|
+
},
|
|
33
|
+
"dependencies": {
|
|
34
|
+
"@principles/core": "^1.74.1",
|
|
35
|
+
"@principles/host-runtime": "^0.1.0"
|
|
36
|
+
},
|
|
37
|
+
"devDependencies": {
|
|
38
|
+
"@types/node": "^26.1.2",
|
|
39
|
+
"typescript": "^7.0.2",
|
|
40
|
+
"vite": "^8.0.16",
|
|
41
|
+
"vitest": "^4.1.10"
|
|
42
|
+
},
|
|
43
|
+
"license": "MIT",
|
|
44
|
+
"repository": {
|
|
45
|
+
"type": "git",
|
|
46
|
+
"url": "git+https://github.com/csuzngjh/principles.git",
|
|
47
|
+
"directory": "packages/codex-adapter"
|
|
48
|
+
},
|
|
49
|
+
"publishConfig": {
|
|
50
|
+
"access": "public"
|
|
51
|
+
}
|
|
52
|
+
}
|