diff --git a/docs/en/docs/integrations/codex.md b/docs/en/docs/integrations/codex.md index 776a73236d..2f769665c7 100644 --- a/docs/en/docs/integrations/codex.md +++ b/docs/en/docs/integrations/codex.md @@ -45,7 +45,9 @@ of a returned marketplace name, registration may already have completed; this do Open a new Codex session after setup. Before sending the first message, use `/hooks` to inspect PowerContext's hooks and, when prompted, grant trust. `SessionStart` resolves the Session Scope and optionally loads startup context; -`UserPromptSubmit` prepares relevant context and independently captures the user input as a Source. The plugin also +`UserPromptSubmit` prepares relevant context and independently captures the user input as a Source. When Codex's model +traffic goes through the gateway, turn prompt-time recall off so the gateway prepares context; see +[Leave prompt-time context to the gateway](#leave-prompt-time-context-to-the-gateway). The plugin also uses `PreToolUse` for MCP Scope binding. Loading the plugin or discovering MCP tools does not prove that these hooks ran. If trust was skipped or denied, inspect the PowerContext entries in `/hooks` and follow the host's available controls @@ -227,6 +229,31 @@ export POWERCONTEXT_CODEX_FLUSH_ON_CAPTURE=true This adds inference latency to each prompt and is not the normal interactive setting. +## Leave prompt-time context to the gateway + +When Codex sends its model requests through the PowerContext gateway, the gateway prepares and injects context for +each turn and, with `PC_GATEWAY_SEDIMENT=on`, captures the turn. Core checks every request the gateway forwards and +rejects a prepared-context block that the gateway did not inject, so a turn that also carries the plugin's block fails. +Turn the plugin's prompt-time recall and prompt capture off before starting Codex: + +```bash +export POWERCONTEXT_CODEX_PROMPT_RECALL=false +export POWERCONTEXT_CODEX_CAPTURE_PROMPTS=false +codex +``` + +With recall off, `UserPromptSubmit` sends no preparation request and adds no context. It still captures prompts while +`POWERCONTEXT_CODEX_CAPTURE_PROMPTS` is `true`; with both off, it sends no request. The Session binding, bootstrap +context, MCP tools, and diagnostics keep their own settings. Keep the default where Codex calls its model directly or +signs in with a subscription account, because the gateway does not carry that traffic. + +With recall off, prompt-time context comes only from the gateway, with two differences: + +- The gateway prepares and captures in the Scope set by `PC_GATEWAY_DEFAULT_SCOPE`, not in the Scope this plugin + resolves for the Session or workspace. Set it to the same Scope. +- The gateway does not send the bootstrap receipt, so Memory already injected by + [Session bootstrap context](#opt-in-to-session-bootstrap-context) can appear again in the gateway's context. + ## Connect to an authenticated local Server Local Server authentication is disabled by default and can be enabled independently of Dashboard. @@ -358,6 +385,7 @@ Complete the [Source, topic evolution, and cross-session recall check](../get-st | `POWERCONTEXT_CODEX_BOOTSTRAP_CONTEXT` | `false` | Inject the curated bootstrap profile on supported `SessionStart` events | | `POWERCONTEXT_CODEX_BOOTSTRAP_MAX_BYTES` | `4096` | Bootstrap package limit; valid values are 512 through 8192 bytes | | `POWERCONTEXT_CODEX_BOOTSTRAP_HANDOFF` | unset | JSON identity of one exact committed Handoff, for example `{"artifact_id":"...","revision":3}` | +| `POWERCONTEXT_CODEX_PROMPT_RECALL` | `true` | Prepare and inject context for each prompt; set `false` when the gateway carries Codex's model traffic | | `POWERCONTEXT_CODEX_CAPTURE_PROMPTS` | `true` | Capture user prompts as Source evidence | | `POWERCONTEXT_CODEX_FLUSH_ON_CAPTURE` | `false` | Wait for Source processing after capture | | `POWERCONTEXT_CODEX_REQUEST_TIMEOUT_SECONDS` | `3` | Per-request hook timeout | diff --git a/docs/zh/docs/integrations/codex.md b/docs/zh/docs/integrations/codex.md index 6cb8c831c0..04b3c591cf 100644 --- a/docs/zh/docs/integrations/codex.md +++ b/docs/zh/docs/integrations/codex.md @@ -40,7 +40,8 @@ powercontext doctor codex ### 首次会话 Hook 信任 配置完成后开启新的 Codex 会话。发送第一条消息前,通过 `/hooks` 检查 PowerContext Hook,并在收到提示时授予信任。 -`SessionStart` 解析会话 Scope,并按配置加载启动上下文;`UserPromptSubmit` 准备相关上下文,并独立地将用户输入采集为 Source。 +`SessionStart` 解析会话 Scope,并按配置加载启动上下文;`UserPromptSubmit` 准备相关上下文,并独立地将用户输入采集为 Source。Codex 的模型流量经过 gateway 时, +请关闭提示词时召回,由 gateway 准备上下文,见[把提示词时上下文交给 gateway](#把提示词时上下文交给-gateway)。 插件还使用 `PreToolUse` 绑定 MCP 操作的 Scope。插件已加载或 MCP 工具可发现,都不能证明这些 Hook 已执行。 如果跳过或拒绝信任,进入 `/hooks` 检查 PowerContext 条目,按当前宿主提供的操作启用或信任它们。 @@ -195,6 +196,29 @@ export POWERCONTEXT_CODEX_FLUSH_ON_CAPTURE=true 这会给每个提示词增加推理延迟,不适合作为日常交互配置。 +## 把提示词时上下文交给 gateway + +Codex 的模型请求经过 PowerContext gateway 时,gateway 为每一轮准备并注入上下文;设置 `PC_GATEWAY_SEDIMENT=on` +后,gateway 也会采集这一轮。Core 检查 gateway 转发的每个请求,拒绝不是 gateway 注入的 prepared-context 块, +因此同时带有插件上下文块的一轮会失败。启动 Codex 前,关闭插件的提示词时召回和提示词采集: + +```bash +export POWERCONTEXT_CODEX_PROMPT_RECALL=false +export POWERCONTEXT_CODEX_CAPTURE_PROMPTS=false +codex +``` + +关闭召回后,`UserPromptSubmit` 不发送准备请求,也不添加上下文。`POWERCONTEXT_CODEX_CAPTURE_PROMPTS` 为 `true` +时它仍会采集提示词;两项都关闭时,它不发送任何请求。Session binding、启动上下文、MCP 工具和诊断仍由各自的设置控制。 +Codex 直接调用模型或使用订阅账号登录时,gateway 不承载这些流量,请保留默认值。 + +关闭召回后,提示词时上下文只来自 gateway,有两点不同: + +- gateway 在 `PC_GATEWAY_DEFAULT_SCOPE` 指定的 Scope 中准备和采集,而不是插件为 Session 或 workspace 解析的 Scope。 + 请把它设为同一个 Scope。 +- gateway 不发送启动上下文的 receipt,因此[启动上下文](#选择启用-session-启动上下文)已经注入的 Memory + 可能再次出现在 gateway 的上下文中。 + ## 连接启用鉴权的本地 Server 本地 Server 默认关闭认证,需要时可独立开启。首次本地配置向导默认不启用 Dashboard;选择启用 Dashboard 不要求配置 token。 @@ -318,6 +342,7 @@ MCP 显示 connected 也不等于 Source 已采集。 | `POWERCONTEXT_CODEX_BOOTSTRAP_CONTEXT` | `false` | 在支持的 `SessionStart` 事件中注入精选启动 profile | | `POWERCONTEXT_CODEX_BOOTSTRAP_MAX_BYTES` | `4096` | 启动上下文总预算;有效范围为 512 到 8192 bytes | | `POWERCONTEXT_CODEX_BOOTSTRAP_HANDOFF` | 未设置 | 一个 exact committed Handoff 的 JSON 身份,例如 `{"artifact_id":"...","revision":3}` | +| `POWERCONTEXT_CODEX_PROMPT_RECALL` | `true` | 为每个提示词准备并注入上下文;gateway 承载 Codex 的模型流量时设为 `false` | | `POWERCONTEXT_CODEX_CAPTURE_PROMPTS` | `true` | 把用户提示词采集为 Source 证据 | | `POWERCONTEXT_CODEX_FLUSH_ON_CAPTURE` | `false` | 采集后等待 Source 处理 | | `POWERCONTEXT_CODEX_REQUEST_TIMEOUT_SECONDS` | `3` | Hook 单次请求超时 | diff --git a/integrations/codex/plugins/powercontext/README.md b/integrations/codex/plugins/powercontext/README.md index 6ba22dcc9c..ec3d917098 100644 --- a/integrations/codex/plugins/powercontext/README.md +++ b/integrations/codex/plugins/powercontext/README.md @@ -141,6 +141,10 @@ overrides the saved credential. On Windows, setup also maintains the user enviro for Desktop compatibility. Restart Codex after setup. Missing credentials preserve the default unauthenticated flow. Never put the token in `.mcp.json`, the Server URL, or a static MCP header. +Prompt-time recall is enabled by default. Set `POWERCONTEXT_CODEX_PROMPT_RECALL=false` +when Codex's model traffic goes through the PowerContext gateway, which prepares and +injects context for each turn; the hook then sends no preparation request. + Prompt capture is enabled by default. Set `POWERCONTEXT_CODEX_CAPTURE_PROMPTS=false` when prompts must not be persisted. Captured Sources are normally processed by the Server's Memory extraction job. They remain ordinary prompt evidence: the diff --git a/integrations/codex/plugins/powercontext/hooks/recall.py b/integrations/codex/plugins/powercontext/hooks/recall.py index 1d8c33baef..89242a323e 100644 --- a/integrations/codex/plugins/powercontext/hooks/recall.py +++ b/integrations/codex/plugins/powercontext/hooks/recall.py @@ -133,7 +133,9 @@ def main(settings: CodexPluginSettings | None = None) -> int: payload = cast(dict[str, Any], json.loads(stdin.buffer.read().decode("utf-8"))) else: payload = cast(dict[str, Any], json.load(stdin)) - if not _is_user_prompt_submit(payload.get("hook_event_name")): + if not _is_user_prompt_submit(payload.get("hook_event_name")) or not ( + settings.prompt_recall or settings.capture_prompts + ): return 0 emitted_diagnostics: set[str] = set() diagnostic_events: list[dict[str, object]] = [] @@ -155,14 +157,19 @@ def main(settings: CodexPluginSettings | None = None) -> int: authorization = settings.authorization.get_secret_value() if settings.authorization is not None else None safe_prompt = scrub_secrets(prompt, authorization) query = _bounded_recall_query(safe_prompt) - context = _recall_context( - query, - scope_id, - settings=settings, - deadline=http_deadline, - bootstrap_receipt_id=bootstrap_receipt_id, - emitted_diagnostics=emitted_diagnostics, - diagnostic_events=diagnostic_events, + # Behind the gateway, Core rejects any prepared-context block that the gateway did not inject itself. + context = ( + _recall_context( + query, + scope_id, + settings=settings, + deadline=http_deadline, + bootstrap_receipt_id=bootstrap_receipt_id, + emitted_diagnostics=emitted_diagnostics, + diagnostic_events=diagnostic_events, + ) + if settings.prompt_recall + else None ) if safe_prompt != prompt: with suppress(Exception): diff --git a/integrations/codex/plugins/powercontext/settings.py b/integrations/codex/plugins/powercontext/settings.py index 8e8a611fad..b902ddde0a 100644 --- a/integrations/codex/plugins/powercontext/settings.py +++ b/integrations/codex/plugins/powercontext/settings.py @@ -143,6 +143,7 @@ class CodexPluginSettings(BaseSettings): bootstrap_max_bytes: int = Field(default=4096, ge=512, le=8192) bootstrap_handoff: BootstrapHandoff | None = None include_code: bool = False + prompt_recall: bool = True capture_prompts: bool = True flush_on_capture: bool = False request_timeout_seconds: float = Field(default=3.0, gt=0) diff --git a/tests/fixtures/hook_characterization/codex.json b/tests/fixtures/hook_characterization/codex.json index acca31c822..a33da9a7ec 100644 --- a/tests/fixtures/hook_characterization/codex.json +++ b/tests/fixtures/hook_characterization/codex.json @@ -1476,6 +1476,123 @@ } ] }, + { + "id": "prompt-recall-can-be-disabled", + "description": "POWERCONTEXT_CODEX_PROMPT_RECALL turns preparation off: the prompt is captured and no context is injected.", + "steps": [ + { + "hook": "hooks/recall.py", + "environment": { + "POWERCONTEXT_CODEX_PROMPT_RECALL": "false" + }, + "payload": { + "session_id": "thread-a", + "turn_id": "turn-1", + "transcript_path": "$CONFIG/sessions/2026/10/26/rollout-thread-a.jsonl", + "cwd": "$CWD", + "hook_event_name": "UserPromptSubmit", + "model": "gpt-5.5", + "prompt": "Which service owns the retry budget?" + }, + "server": { + "POST /v1/scope-bindings/resolve": [ + "scope" + ], + "POST /v1/context/prepare": [ + "ready" + ], + "POST /v1/sources/content": [ + "accepted" + ] + }, + "expected": { + "exit_code": 0, + "requests": [ + { + "method": "POST", + "path": "/v1/scope-bindings/resolve", + "headers": "client", + "body": { + "binding_keys": [ + { + "external_id": "thread-a", + "integration": "codex", + "kind": "session" + }, + { + "external_id": "$WORKSPACE_KEY", + "integration": "codex", + "kind": "workspace" + } + ], + "explicit_scope_id": null + } + }, + { + "method": "POST", + "path": "/v1/sources/content", + "headers": "client-dream-contract", + "body": { + "content": "Which service owns the retry budget?", + "metadata": { + "cwd": "$CWD", + "event": "user_prompt_submit", + "origin": "codex", + "session_id": "thread-a", + "turn_id": "turn-1" + }, + "scope_id": "scope-characterized", + "source_id": "codex-user-prompt:d624469cd52a51e771cda027909092c971ee7584700ea37535efa17355ad7c7a" + } + } + ], + "stdout": [], + "stderr": [], + "state": {} + } + } + ] + }, + { + "id": "prompt-recall-and-capture-can-be-disabled", + "description": "With prompt recall and capture both off, the prompt hook sends no request and writes nothing.", + "steps": [ + { + "hook": "hooks/recall.py", + "environment": { + "POWERCONTEXT_CODEX_PROMPT_RECALL": "false", + "POWERCONTEXT_CODEX_CAPTURE_PROMPTS": "false" + }, + "payload": { + "session_id": "thread-a", + "turn_id": "turn-1", + "transcript_path": "$CONFIG/sessions/2026/10/26/rollout-thread-a.jsonl", + "cwd": "$CWD", + "hook_event_name": "UserPromptSubmit", + "model": "gpt-5.5", + "prompt": "Which service owns the retry budget?" + }, + "server": { + "POST /v1/scope-bindings/resolve": [ + "scope" + ], + "POST /v1/context/prepare": [ + "ready" + ], + "POST /v1/sources/content": [ + "accepted" + ] + }, + "expected": { + "exit_code": 0, + "requests": [], + "stdout": [], + "stderr": [], + "state": {} + } + } + ] + }, { "id": "git-subdirectory-binds-the-repository-root", "description": "From a repository subdirectory, the workspace binding key identifies the repository root.",