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
8 changes: 8 additions & 0 deletions docs/heartbeat-automation-prompt.md
Original file line number Diff line number Diff line change
Expand Up @@ -472,6 +472,14 @@ The result contains `native_config.hooks` and
its existing developer instructions. Preserve other configuration. Review and
trust only these exact hooks through Codex's native hook trust surface; the CLI
does not install files, approve hashes, enable diagnostics or run a guard.
An already loaded native thread may keep its earlier hook configuration even
when `hooks/list` discovers the new handlers as enabled and trusted. That readback
does not deliver `SessionStart` or initialize its context journal. After active
work reaches its normal safe boundary, use the host's start/resume path with the
resulting configuration and verify actual lifecycle delivery before qualifying
live use. Do not restart an active host merely to manufacture a sample. Until
then, preserve full delivery and the original capture; do not fabricate events
or borrow another session's journal.
Use one configuration for the declared Goal/Agent. A shared private root keeps
different native sessions in separate journals, including interleaved sessions.
For an interactive SSH Goal, use `--runtime-profile codex_app_ssh_goal` in place
Expand Down
14 changes: 12 additions & 2 deletions docs/reference/required-work-context.md
Original file line number Diff line number Diff line change
Expand Up @@ -267,7 +267,13 @@ with `--runtime-profile codex_app_ssh_goal` in setup and the thin/bootstrap prom
Its generated guard keeps the native Goal identity and visible-Goal settlement;
it does not become a hosted heartbeat. It does not install or trust hooks,
change host configuration or restart the running App. Configure and trust its
exact hooks for the next native session. The native path preserves authentication,
exact hooks for the next native session. Existing loaded threads may retain
their earlier hook configuration despite enabled/trusted `hooks/list` readback;
discovery does not deliver `SessionStart` or initialize a context journal. Use
the host's start/resume path after active work reaches its normal safe boundary,
then verify lifecycle delivery. Keep full delivery and the original capture
until then; do not fabricate events, borrow another session's journal or restart
an active host to manufacture adoption. The native path preserves authentication,
endpoints, provider selection and connection mode, including implicit history via
`previous_response_id`. It neither enables input diagnostics nor scans model-input
history. Benchmark preparation stages those same hooks in its isolated trial home.
Expand Down Expand Up @@ -453,7 +459,11 @@ exec stdout;把整包重打进哈希工具不构成原始字节已被保留的
在失效事件发生前仍可用;这不是实际输入保留证明或模型 ACK。共享 codec 仍校验精确基线
摘要、作用域、本轮控制与完整重建。显式 final-input Host 客户端保留独立的确认输入契约。

为下次原生会话配置并信任精确 hooks,不重启当前 App。原生路径保留认证、端点、provider
为下次原生会话配置并信任精确 hooks,不重启当前 App。已加载线程可能继续沿用旧 hook
配置;`hooks/list` 的 enabled/trusted 读回不等于向该线程交付 `SessionStart` 或初始化
context journal。当前工作到达正常安全边界后,沿宿主正常启动/恢复入口加载配置,再
核验实际生命周期交付。此前保持全量与原捕获,不伪造事件、借用其他会话 journal 或为
制造采用样本重启活动宿主。原生路径保留认证、端点、provider
和连接方式,包括带 `previous_response_id` 的隐式历史;不启用输入诊断,也不逐轮扫描
模型完整输入。Benchmark 在各自隔离的 trial home 内自动准备同一组 hooks。

Expand Down
5 changes: 4 additions & 1 deletion loopx/cli_commands/quota_capture.py
Original file line number Diff line number Diff line change
Expand Up @@ -483,7 +483,10 @@ def handle_native_context_command(args: argparse.Namespace, *, print_payload: Ca
inherit_machine=bool(machine_runtime))
payload = {"ok": True, "context_delivery": activation, "native_config": config,
"activation": "Merge hooks and append developer_instructions to the existing native configuration; "
"review and trust only these exact hooks in Codex. Restart/resume with the resulting configuration. "
"review and trust only these exact hooks in Codex. "
"Existing loaded threads may retain their original hook configuration; enabled/trusted "
"discovery is not session initialization or live adoption. Start/resume through the host "
"with the resulting configuration after active work reaches its normal safe boundary. "
+ ("Machine defaults are re-read on each invocation; generated hooks inherit the same machine runtime. "
"Omit per-command activation and location flags to retain that inheritance. " if machine_runtime else
"Pass --context-delivery on with the same declared host mode, context location and a decision-output-root to heartbeat-prompt/should-run. ")
Expand Down
74 changes: 73 additions & 1 deletion tests/control_plane/test_codex_context_boundary.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ def _input_text(request):


def _app_server(binary, env, workspace, home, trusted, additional_context=None, *, sandbox="read-only", resume=None,
turn_timeout_sec=30):
turn_timeout_sec=30, before_turn=None):
with (home / "stderr.txt").open("w") as stderr, StdioNativeGoalTransport.spawn(
[binary, "app-server", "--listen", "stdio://"], cwd=str(workspace), env=env,
stderr=stderr, response_timeout_sec=20,
Expand All @@ -102,6 +102,8 @@ def _app_server(binary, env, workspace, home, trusted, additional_context=None,
if resume:
params["threadId"] = resume
source = native.request("thread/resume" if resume else "thread/start", params)["thread"]["id"]
if before_turn is not None:
before_turn(native)
turn_params = {"threadId": source,
"input": [{"type": "text", "text": "Synthetic context-delivery fixture."}]}
if additional_context is not None:
Expand Down Expand Up @@ -144,6 +146,76 @@ def _configure_provider(home, url):
'wire_api="responses"\nrequires_openai_auth=false\n')


@pytest.mark.parametrize("source", ["inline", "file"])
def test_native_late_hook_discovery_does_not_deliver_session_context(tmp_path, source):
"""Discovery/trust after start is not SessionStart delivery to that thread.

A real process resume is the positive control. Do not freeze whether future
hosts hot-load tool hooks; only actual lifecycle delivery qualifies context.
"""
binary, env, workspace, home = _native_fixture(tmp_path)
marker = "Synthetic late-config requirement: stop before publishing."
hook = home / "late-hook.py"
hook.write_text(
"import json,pathlib,sys\nevent=json.load(sys.stdin)\n"
"with pathlib.Path(__file__).with_name('events.jsonl').open('a') as f:\n"
" f.write(json.dumps({'event':event['hook_event_name'],'source':event.get('source')})+'\\n')\n"
"print(json.dumps({'hookSpecificOutput':{'hookEventName':'SessionStart',"
f"'additionalContext':{marker!r}"
"}}) if event['hook_event_name']=='SessionStart' else '{}')\n")

def events():
path = home / "events.jsonl"
return [json.loads(line) for line in path.read_text().splitlines()] if path.exists() else []

def configure_after_start(native):
command = shlex.join([sys.executable, str(hook)])
if source == "inline":
with (home / "config.toml").open("a") as config:
for name in ("SessionStart", "PostToolUse"):
config.write(f"\n[[hooks.{name}]]\n[[hooks.{name}.hooks]]\n"
f"type=\"command\"\ncommand={json.dumps(command)}\ntimeout=10\n")
else:
handler = {"type": "command", "command": command, "timeout": 10}
(home / "hooks.json").write_text(json.dumps({"hooks": {
name: [{"hooks": [handler]}] for name in ("SessionStart", "PostToolUse")}}))
listed = native.request("hooks/list", {"cwds": [str(workspace)]})
handlers = [handler for entry in listed["data"] for handler in entry["hooks"]]
assert len(handlers) == 2
expected_path = home / ("config.toml" if source == "inline" else "hooks.json")
assert all(handler["sourcePath"] == str(expected_path.resolve()) for handler in handlers)
with (home / "config.toml").open("a") as config:
for handler in handlers:
config.write(f"\n[hooks.state.{json.dumps(handler['key'])}]\n"
f"trusted_hash={json.dumps(handler['currentHash'])}\n")
trusted = native.request("hooks/list", {"cwds": [str(workspace)]})
assert all(handler["enabled"] and handler["trustStatus"] == "trusted"
for entry in trusted["data"] for handler in entry["hooks"])

def response(request, number, message):
if number % 2:
return {"type": "function_call", "id": f"fc_{number}", "call_id": f"call_{number}",
"name": "exec_command", "arguments": json.dumps({
"cmd": "printf synthetic-late-hook-result", "max_output_tokens": 100})}
return message

with local_inference_provider(response) as (url, calls, requests):
_configure_provider(home, url)
config = home / "config.toml"
config.write_text(config.read_text().replace("[features]\n", "[features]\ncode_mode_host=true\n"))
thread = _app_server(binary, env, workspace, home, False, before_turn=configure_after_start)
assert len(calls) == 2
assert all(marker not in _input_text(request) for request in requests)
assert not any(event["event"] == "SessionStart" for event in events())
configured = config.read_bytes()
assert _app_server(binary, env, workspace, home, False, resume=thread) == thread
assert len(calls) == 4
assert all(marker in _input_text(request) for request in requests[2:])
assert {"event": "SessionStart", "source": "resume"} in events()
assert any(event["event"] == "PostToolUse" for event in events())
assert config.read_bytes() == configured


@pytest.mark.parametrize("host", ["exec", "app_server"])
@pytest.mark.parametrize("failed_primary", [False, True])
@pytest.mark.parametrize("surface", ["quota_packet", "turn_envelope"])
Expand Down
Loading