Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 17 additions & 12 deletions lib/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,17 @@ const CONFIG_PATH = getOpenCodePath("openhax-codex-config.json");
* CODEX_MODE is enabled by default for better Codex CLI parity
* Prompt caching is enabled by default to optimize token usage and reduce costs
*/
const DEFAULT_CONFIG: PluginConfig = {
codexMode: true,
enablePromptCaching: true,
logging: {
showWarningToasts: false,
logWarningsToConsole: false,
},
};
function getDefaultConfig(): PluginConfig {
return {
codexMode: true,
enablePromptCaching: true,
appendEnvContext: process.env.CODEX_APPEND_ENV_CONTEXT === "1",
logging: {
showWarningToasts: false,
logWarningsToConsole: false,
},
};
}

let cachedPluginConfig: PluginConfig | undefined;

Expand All @@ -38,10 +41,11 @@ export function loadPluginConfig(options: { forceReload?: boolean } = {}): Plugi
}

try {
const defaults = getDefaultConfig();
const fileContent = safeReadFile(CONFIG_PATH);
if (!fileContent) {
logWarn("Plugin config file not found, using defaults", { path: CONFIG_PATH });
cachedPluginConfig = { ...DEFAULT_CONFIG };
cachedPluginConfig = { ...defaults };
return cachedPluginConfig;
}

Expand All @@ -50,20 +54,21 @@ export function loadPluginConfig(options: { forceReload?: boolean } = {}): Plugi

// Merge with defaults (shallow merge + nested logging merge)
cachedPluginConfig = {
...DEFAULT_CONFIG,
...defaults,
...userConfig,
logging: {
...DEFAULT_CONFIG.logging,
...defaults.logging,
...userLogging,
},
};
return cachedPluginConfig;
} catch (error) {
const defaults = getDefaultConfig();
logWarn("Failed to load plugin config", {
path: CONFIG_PATH,
error: (error as Error).message,
});
cachedPluginConfig = { ...DEFAULT_CONFIG };
cachedPluginConfig = { ...defaults };
return cachedPluginConfig;
}
}
Expand Down
4 changes: 3 additions & 1 deletion lib/request/fetch-helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ export async function transformRequestForCodex(
userConfig: UserConfig,
codexMode = true,
sessionManager?: SessionManager,
_pluginConfig?: PluginConfig,
pluginConfig?: PluginConfig,
): Promise<
| {
body: RequestBody;
Expand Down Expand Up @@ -160,7 +160,9 @@ export async function transformRequestForCodex(
codexMode,
{
preserveIds: sessionContext?.preserveIds,
appendEnvContext: pluginConfig?.appendEnvContext ?? process.env.CODEX_APPEND_ENV_CONTEXT === "1",
},

sessionContext,
);
const appliedContext =
Expand Down
18 changes: 14 additions & 4 deletions lib/request/request-transformer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ export { getModelConfig, getReasoningConfig, normalizeModel } from "./model-conf
export interface TransformRequestOptions {
/** Preserve IDs when prompt caching requires it. */
preserveIds?: boolean;
/** Reattach env/files context to prompt tail (defaults from config/env). */
appendEnvContext?: boolean;
}

export interface TransformResult {
Expand All @@ -37,6 +39,7 @@ async function transformInputForCodex(
codexMode: boolean,
preserveIds: boolean,
hasNormalizedTools: boolean,
appendEnvContext: boolean,
sessionContext?: SessionContext,
): Promise<void> {
if (!body.input || !Array.isArray(body.input)) {
Expand All @@ -62,8 +65,7 @@ async function transformInputForCodex(
}

if (codexMode) {
const appendEnvTail = process.env.CODEX_APPEND_ENV_CONTEXT === "1";
if (appendEnvTail) {
if (appendEnvContext) {
const result = await filterOpenCodeSystemPromptsWithEnv(workingInput);
workingInput = result?.input;
if (result?.envSegments?.length) {
Expand Down Expand Up @@ -126,8 +128,16 @@ export async function transformRequestBody(
logCacheKeyDecision(cacheKeyResult, isNewSession);

const hasNormalizedTools = normalizeToolsForCodexBody(body, false);

await transformInputForCodex(body, codexMode, preserveIds, hasNormalizedTools, sessionContext);
const appendEnvContext = options.appendEnvContext ?? process.env.CODEX_APPEND_ENV_CONTEXT === "1";

await transformInputForCodex(
body,
codexMode,
preserveIds,
hasNormalizedTools,
appendEnvContext,
sessionContext,
);

const reasoningConfig = getReasoningConfig(originalModel, modelConfig);
body.reasoning = {
Expand Down
6 changes: 6 additions & 0 deletions lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,12 @@ export interface PluginConfig {
*/
enablePromptCaching?: boolean;

/**
* Reattach stripped environment/file context to the end of the prompt
* Default inherits from CODEX_APPEND_ENV_CONTEXT env var
*/
appendEnvContext?: boolean;

/**
* Logging configuration that can override environment variables
*/
Expand Down
29 changes: 29 additions & 0 deletions spec/append-env-context-config.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# Append Env Context Config

## Scope

- Add `appendEnvContext` to plugin config with default derived from `CODEX_APPEND_ENV_CONTEXT`.
- Route env/file tail reattachment through config/options instead of global env in transforms.
- Make tests explicit about env-tail behavior to avoid leaked env state.

## Touched Files

- lib/types.ts (PluginConfig additions)
- lib/config.ts (default config from env, merge logic)
- lib/request/request-transformer.ts (options-driven appendEnvContext handling)
- lib/request/fetch-helpers.ts (propagate config to transform)
- test/request-transformer.test.ts (explicit appendEnvContext expectations)
- test/cache-e2e.test.ts (pass plugin config with appendEnvContext=false)
- test/plugin-config.test.ts (cover env default + overrides, reset env)

## Definition of Done

- appendEnvContext configurable via config file with default from env.
- Transform respects config-driven appendEnvContext; no hard env dependency in tests.
- Cache/request-transformer tests pass with explicit config; plugin config tests cover new field.
- pnpm test test/cache-e2e.test.ts test/request-transformer.test.ts test/plugin-config.test.ts succeeds.

## Notes

- No existing issues/PRs linked for this change.
- Defaults recalc on load; `forceReload` picks up env changes.
9 changes: 7 additions & 2 deletions test/cache-e2e.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { describe, it, expect, vi, afterEach } from "vitest";
import { transformRequestForCodex } from "../lib/request/fetch-helpers.js";
import { SessionManager } from "../lib/session/session-manager.js";
import * as openCodeCodex from "../lib/prompts/opencode-codex.js";
import type { InputItem, RequestBody, UserConfig } from "../lib/types.js";
import type { InputItem, PluginConfig, RequestBody, UserConfig } from "../lib/types.js";
import * as logger from "../lib/logger.js";

const CODEX_INSTRUCTIONS = "codex instructions";
Expand Down Expand Up @@ -30,7 +30,11 @@ function envMessage(date: string, files: string[]): InputItem {
};
}

async function runTransform(body: RequestBody, sessionManager: SessionManager) {
async function runTransform(
body: RequestBody,
sessionManager: SessionManager,
pluginConfig: PluginConfig = { appendEnvContext: false },
) {
const init: RequestInit = { body: JSON.stringify(body) };
const result = await transformRequestForCodex(
init,
Expand All @@ -39,6 +43,7 @@ async function runTransform(body: RequestBody, sessionManager: SessionManager) {
USER_CONFIG,
true,
sessionManager,
pluginConfig,
);
if (!result) throw new Error("transformRequestForCodex returned undefined");
return result;
Expand Down
34 changes: 34 additions & 0 deletions test/plugin-config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,13 @@ beforeEach(async () => {

describe("Plugin Configuration", () => {
let originalEnv: string | undefined;
let originalAppendEnv: string | undefined;

beforeEach(() => {
originalEnv = process.env.CODEX_MODE;
originalAppendEnv = process.env.CODEX_APPEND_ENV_CONTEXT;
delete process.env.CODEX_MODE;
delete process.env.CODEX_APPEND_ENV_CONTEXT;
vi.clearAllMocks();
});

Expand All @@ -41,6 +45,12 @@ describe("Plugin Configuration", () => {
} else {
process.env.CODEX_MODE = originalEnv;
}

if (originalAppendEnv === undefined) {
delete process.env.CODEX_APPEND_ENV_CONTEXT;
} else {
process.env.CODEX_APPEND_ENV_CONTEXT = originalAppendEnv;
}
});

describe("loadPluginConfig", () => {
Expand All @@ -52,6 +62,7 @@ describe("Plugin Configuration", () => {
expect(config).toEqual({
codexMode: true,
enablePromptCaching: true,
appendEnvContext: false,
logging: { showWarningToasts: false, logWarningsToConsole: false },
});

Expand All @@ -69,6 +80,7 @@ describe("Plugin Configuration", () => {
expect(config).toEqual({
codexMode: false,
enablePromptCaching: true,
appendEnvContext: false,
logging: { showWarningToasts: false, logWarningsToConsole: false },
});
});
Expand All @@ -82,10 +94,30 @@ describe("Plugin Configuration", () => {
expect(config).toEqual({
codexMode: true,
enablePromptCaching: true,
appendEnvContext: false,
logging: { showWarningToasts: false, logWarningsToConsole: false },
});
});

it("should default appendEnvContext from env when config missing", () => {
process.env.CODEX_APPEND_ENV_CONTEXT = "1";
mockExistsSync.mockReturnValue(false);

const config = loadPluginConfig({ forceReload: true });

expect(config.appendEnvContext).toBe(true);
});

it("should let config override appendEnvContext even when env is set", () => {
process.env.CODEX_APPEND_ENV_CONTEXT = "1";
mockExistsSync.mockReturnValue(true);
mockReadFileSync.mockReturnValue(JSON.stringify({ appendEnvContext: false }));

const config = loadPluginConfig({ forceReload: true });

expect(config.appendEnvContext).toBe(false);
});

it("should merge nested logging config with defaults", () => {
mockExistsSync.mockReturnValue(true);
mockReadFileSync.mockReturnValue(
Expand All @@ -112,6 +144,7 @@ describe("Plugin Configuration", () => {
expect(config).toEqual({
codexMode: true,
enablePromptCaching: true,
appendEnvContext: false,
logging: { showWarningToasts: false, logWarningsToConsole: false },
});

Expand All @@ -131,6 +164,7 @@ describe("Plugin Configuration", () => {
expect(config).toEqual({
codexMode: true,
enablePromptCaching: true,
appendEnvContext: false,
logging: { showWarningToasts: false, logWarningsToConsole: false },
});
expect(logWarnSpy).toHaveBeenCalled();
Expand Down
30 changes: 24 additions & 6 deletions test/request-transformer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -714,7 +714,14 @@ describe("transformRequestBody caching stability", () => {
],
};

const result1 = await transformRequestBody(body1, CODEX_INSTRUCTIONS, userConfig, true, {}, undefined);
const result1 = await transformRequestBody(
body1,
CODEX_INSTRUCTIONS,
userConfig,
true,
{ appendEnvContext: false },
undefined,
);
expect(result1.prompt_cache_key).toContain("conv-env-stable");
expect(result1.input).toHaveLength(1);
expect(result1.input?.[0].role).toBe("user");
Expand All @@ -728,14 +735,20 @@ describe("transformRequestBody caching stability", () => {
],
};

const result2 = await transformRequestBody(body2, CODEX_INSTRUCTIONS, userConfig, true, {}, undefined);
const result2 = await transformRequestBody(
body2,
CODEX_INSTRUCTIONS,
userConfig,
true,
{ appendEnvContext: false },
undefined,
);
expect(result2.prompt_cache_key).toBe(result1.prompt_cache_key);
expect(result2.input).toHaveLength(1);
expect(result2.input?.[0].role).toBe("user");
});

it("can reattach env/files tail when flag enabled", async () => {
process.env.CODEX_APPEND_ENV_CONTEXT = "1";
const body: RequestBody = {
model: "gpt-5",
metadata: { conversation_id: "conv-env-tail" },
Expand All @@ -762,14 +775,19 @@ describe("transformRequestBody caching stability", () => {
],
};

const result = await transformRequestBody(body, CODEX_INSTRUCTIONS, userConfig, true, {}, undefined);
const result = await transformRequestBody(
body,
CODEX_INSTRUCTIONS,
userConfig,
true,
{ appendEnvContext: true },
undefined,
);
expect(result.input?.length).toBe(2);
expect(result.input?.[0].role).toBe("user");
expect(result.input?.[1].role).toBe("developer");
expect(result.input?.[1].content as string).toContain("<env>");
expect(result.input?.[1].content as string).toContain("<files>");

delete process.env.CODEX_APPEND_ENV_CONTEXT;
});
});

Expand Down
Loading