From 1e3a1c94ea0c74f6ca448d1460ba67d187c823bb Mon Sep 17 00:00:00 2001 From: Epinephrine Date: Mon, 28 Sep 2026 06:08:40 +0000 Subject: [PATCH 1/9] docs(codex): propose native remote-list provider policy with isolated probes Publish the #5848 implementation alternatives and executable specifications. Prefer an opt-in native remote-only list policy over a shared-backend relay. Keep the existing runtime, authentication, routing and history untouched. Validation: 60 isolated Python tests passed; native/Bun/mobile validation remains outstanding. This is an RFC and research unit, not a production fix for #5848. --- .../000_plan.md | 32 +++ .../010_design.md | 134 ++++++++++ .../020_verification.md | 50 ++++ .../probes/native_policy.py | 70 ++++++ .../probes/remote_list_probe.py | 160 ++++++++++++ .../probes/test_loopback_bridge.py | 228 ++++++++++++++++++ .../probes/test_native_policy.py | 153 ++++++++++++ .../probes/test_probe.py | 196 +++++++++++++++ 8 files changed, 1023 insertions(+) create mode 100644 devlog/_plan/260928_remote_thread_provider_policy/000_plan.md create mode 100644 devlog/_plan/260928_remote_thread_provider_policy/010_design.md create mode 100644 devlog/_plan/260928_remote_thread_provider_policy/020_verification.md create mode 100644 devlog/_plan/260928_remote_thread_provider_policy/probes/native_policy.py create mode 100644 devlog/_plan/260928_remote_thread_provider_policy/probes/remote_list_probe.py create mode 100644 devlog/_plan/260928_remote_thread_provider_policy/probes/test_loopback_bridge.py create mode 100644 devlog/_plan/260928_remote_thread_provider_policy/probes/test_native_policy.py create mode 100644 devlog/_plan/260928_remote_thread_provider_policy/probes/test_probe.py diff --git a/devlog/_plan/260928_remote_thread_provider_policy/000_plan.md b/devlog/_plan/260928_remote_thread_provider_policy/000_plan.md new file mode 100644 index 00000000000..530fa2bd582 --- /dev/null +++ b/devlog/_plan/260928_remote_thread_provider_policy/000_plan.md @@ -0,0 +1,32 @@ +# 5848: native remote-list provider policy before a backend relay + +Status: **proposal and executable specifications only; no production fix**. +Date: 2026-09-28. + +Related: [#5848](https://github.com/lidge-jun/opencodex/issues/5848), +[duplicate #5906](https://github.com/lidge-jun/opencodex/issues/5906), +[upstream #48358](https://github.com/openai/codex/issues/48358). + +## Proposed decision + +Prefer a narrowly scoped, operator-opt-in policy in **native Codex app-server** +for remote `thread/list` requests that do not supply a provider array. Keep the +normal default-provider behavior, all existing authorization, and history intact. +The mobile client's explicit all-provider request remains the simplest upstream +client correction. Compare both with the previously tested local backend relay; +do not turn that experimental relay into an installed feature in this PR. + +The current OpenCodex runtime and [ADR-5848](../../../structure/decisions/ADR-5848-provider-table-remote-history-visibility.md) +remain unchanged. This proposal does **not** close #5848. + +## Work remaining + +- [x] Identify the native remote-connection boundary and existing provider predicate. +- [x] Specify precedence and test the native-policy proposal offline. +- [x] Retain and rerun the isolated loopback relay alternative. +- [ ] Obtain upstream agreement on the native configuration/API contract. +- [ ] Implement config/schema, trusted-origin plumbing, and Rust regressions upstream. +- [ ] Verify native pagination, managed policy, account changes, and actual mobile resume. +- [ ] Establish released-version/capability detection before adding any OpenCodex toggle. + +See [design](010_design.md) and [verification](020_verification.md). diff --git a/devlog/_plan/260928_remote_thread_provider_policy/010_design.md b/devlog/_plan/260928_remote_thread_provider_policy/010_design.md new file mode 100644 index 00000000000..a0f8ca22474 --- /dev/null +++ b/devlog/_plan/260928_remote_thread_provider_policy/010_design.md @@ -0,0 +1,134 @@ +# Design comparison and proposed native contract + +This is an RFC, not an accepted architecture change or a released setting. +`probes/native_policy.py` is an executable specification, **not native Rust code**. +No probe is imported by OpenCodex or registered in its runtime/CLI/test suite. + +## Evidence and scope + +The source baseline is OpenAI Codex +`1cc7e2361237ce7244430ee1d581c77f95c57ac8` and OpenCodex `dev` +`eb7f0f0970c2298f8b2d66d170c4d4be869f301b`. + +- [Native list predicate](https://github.com/openai/codex/blob/1cc7e2361237ce7244430ee1d581c77f95c57ac8/codex-rs/app-server/src/request_processors/thread_processor.rs#L5456-L5530): explicit nonempty arrays filter to those ids; `[]` removes the provider predicate; omission defaults to the configured provider, except for related-thread queries. +- [Remote connection origin](https://github.com/openai/codex/blob/1cc7e2361237ce7244430ee1d581c77f95c57ac8/codex-rs/app-server-transport/src/transport/remote_control/client_tracker.rs): native remote sessions open with `ConnectionOrigin::RemoteControl`; messages become native transport events. +- [Original rationale, #5658](https://github.com/openai/codex/pull/5658): provider annotations/filtering address cross-provider resume/decryption failures. Visibility is not proof that a conversation is safe to resume under a different provider. +- [OpenCodex mitigation, #6007](https://github.com/lidge-jun/opencodex/pull/6007): warnings and documentation without rewriting native history or intercepting native RPC. + +## Alternatives + +| Approach | Can leave official mobile app unchanged? | Additional ownership | Recommendation | +| --- | --- | --- | --- | +| Mobile explicitly sends `modelProviders: []` (or a chosen list) | No | Client's list behavior and resume UX | Simplest client correction; continue upstream tracking. | +| Native, opt-in, remote-only default list policy | Yes, once a compatible native build ships | Native config and request policy, no extra service | **Preferred implementation proposal** when the host needs explicit control. | +| Local relay selected through `chatgpt_base_url` | Potentially; live service not verified | Shared backend traffic, token forwarding, connection lifecycle | Research fallback, not a default or currently supported remedy. | +| Globally change omitted filter to all providers | Yes | Changes behavior for every caller | Do not use: loses the existing default isolation behavior. | +| Rewrite `openai` history tags to `opencodex` | Yes in some observations | Native SQLite/rollout state | Do not use: violates the retained history-writer boundary. | + +The native proposal minimizes new connection and credential handling; this is an +engineering recommendation, not evidence of upstream acceptance or deployment. +Neither upstream route can be delivered by modifying OpenCodex's `/v1` inference +proxy alone. This PR publishes the comparison rather than disguising a probe as a fix. + +## Proposed native setting (name subject to upstream review) + +**Illustrative only; do not add this to current Codex/OpenCodex configuration.** + +```toml +[remote_control] +thread_list_model_providers = ["openai", "opencodex"] +``` + +Absent setting means opt-out. An explicitly empty array means all providers; +a nonempty array means exactly those ids. Do not automatically infer equivalence +from provider names, model names, or a shared URL. The setting changes listing, +not authentication, permission checks, routing, compaction, or resume semantics. +An operator who only needs two provider ids need not opt into every provider. + +### Precedence + +1. Existing authentication and managed remote-control policy still run first. +2. A request's explicit provider **array**, including `[]`, always wins. +3. With no array, parent/ancestor queries retain their current no-default-filter behavior. +4. Only a server-identified remote connection may use the operator's configured policy. +5. With no policy, and for every non-remote connection, preserve the existing default. + +Typed Rust `Option>` treats omission and JSON `null` alike. This +proposal deliberately applies its default to both. The raw relay probe preserves +all present JSON keys, including null; that difference is documented and must not +be mistaken for identical behavior between the two alternatives. + +Use the native `ConnectionOrigin` attached to the connection. Never infer remote +origin from clientInfo.name, a user-agent, JSON fields, or a caller-supplied header. +The Python enum tests only model this trusted input; they do not prove authentication. + +### Native implementation boundary + +The upstream change must add and validate the config contract, snapshot it with +the native app-server's effective configuration, carry the trusted connection +origin to the `thread/list` handler, and resolve the provider predicate before +calling the existing store pagination. A new RPC layer is unnecessary. + +Conceptual selection, **not a drop-in patch**: + +```text +if request supplies provider array: use that array +else if parent/ancestor query: use existing related-thread behavior +else if trusted origin is RemoteControl and operator policy is configured: + use the configured array +else: use existing configured-default behavior +``` + +Do not add a global exception to `list_threads_common` for all callers. Do not +change `thread/start`, `thread/resume`, `turn/start`, stored provider metadata, +or request error handling. Keep source/cwd/archive/project filters and ordering. +The new configuration's trust/precedence should prevent project content from +silently opting an operator into a broader default; upstream must choose and test +the appropriate configuration layers and any managed restrictions. + +Hold one effective policy throughout a listing/pagination sequence. A changed +policy requires a fresh listing cursor; do not combine pages obtained under +incompatible filters. Native tests must pin behavior with actual cursor semantics. +The fixture tests here use integer pagination, not native opaque cursors. + +## Local relay alternative: what was and was not shown + +[Native startup](https://github.com/openai/codex/blob/1cc7e2361237ce7244430ee1d581c77f95c57ac8/codex-rs/app-server/src/lib.rs) +uses `config.chatgpt_base_url` for remote control. The +[URL protocol](https://github.com/openai/codex/blob/1cc7e2361237ce7244430ee1d581c77f95c57ac8/codex-rs/app-server-transport/src/transport/remote_control/protocol.rs) +accepts loopback URLs. A host-side relay is therefore a concrete design alternative: + +```text +unchanged mobile <-> existing ChatGPT backend <-> local relay <-> native app-server +``` + +Connection establishment starts on the host. Enroll/pair/refresh can be forwarded +rather than reimplemented. However, `chatgpt_base_url` is shared with +[authentication configuration](https://github.com/openai/codex/blob/1cc7e2361237ce7244430ee1d581c77f95c57ac8/codex-rs/core/src/config/auth_keyring.rs) +and other backend consumers. A WebSocket-only implementation is insufficient. +[Enrollment persistence](https://github.com/openai/codex/blob/1cc7e2361237ce7244430ee1d581c77f95c57ac8/codex-rs/app-server-transport/src/transport/remote_control/enroll.rs) +also keys state by URL/account/client, so changing the URL can affect enrollment +selection; whether live re-pairing is required remains unverified. + +The retained relay fixture accepts only literal `127.0.0.1` HTTP upstreams and +synthetic credentials. It is not a production service and cannot be configured +for ChatGPT. It handles regular and single-chunk frames; multi-chunk messages are +passed unchanged. Actual protocol-v3 segmentation, native reconnect/ACK/cursors, +account changes, managed network policy, and full backend compatibility remain +release gates, not claims supported by these tests. See native +[WebSocket handling](https://github.com/openai/codex/blob/1cc7e2361237ce7244430ee1d581c77f95c57ac8/codex-rs/app-server-transport/src/transport/remote_control/websocket.rs). + +No global base-URL mutation, token-pool integration, production listener, +configuration migration, history write, or authentication bypass is proposed here. + +## Upstream and downstream follow-through + +An upstream implementation needs Rust config/schema, origin-scoping, explicit/null/ +related-query, pagination, and managed-policy tests. Keep existing defaults intact. +Document the omitted/default behavior and regenerate affected schema/TS fixtures. + +After an accepted implementation is released, OpenCodex may consider an opt-in +integration with positive capability/version evidence and precise configuration +ownership/restoration. Merely writing an unknown TOML key is not a feature test. +Until then, retain #6007's warnings and keep #5848 open. Do not suppress the warning +because an experimental setting was written or a fixture passed. diff --git a/devlog/_plan/260928_remote_thread_provider_policy/020_verification.md b/devlog/_plan/260928_remote_thread_provider_policy/020_verification.md new file mode 100644 index 00000000000..76564b99534 --- /dev/null +++ b/devlog/_plan/260928_remote_thread_provider_policy/020_verification.md @@ -0,0 +1,50 @@ +# Verification + +Date: 2026-09-28. Environment: Python 3.13.5, aiohttp 3.13.3, Linux. +All inputs, accounts, tokens, thread ids, and database rows are synthetic. +No native Codex process, user home, installed config, or real service was accessed. + +From the repository root: + +```sh +cd devlog/_plan/260928_remote_thread_provider_policy/probes +python -m unittest -v test_native_policy test_probe test_loopback_bridge +``` + +**Executed: 60 tests, 0 failures.** + +- 22 tests specify the proposed native policy: opt-out, trusted-origin scoping, + explicit arrays, null semantics, parent/ancestor exceptions, immutable policy, + exact ids, validation, and synthetic paginated fixture reads. +- 29 retained raw-frame tests cover the relay alternative's ordinary/single-chunk + rewrites, byte-identical pass-through, limits, and unsupported inputs. +- 9 retained localhost HTTP/WebSocket tests use a mock host and mock backend, + checking two-way traffic, fixture auth/headers, explicit filters, enrollment, + unrelated HTTP, and fixture endpoint restrictions. + +The 22 + 29 offline tests use the Python standard library only. The 9 socket tests +need aiohttp already installed; no production dependency manifest is changed. + +```sh +python -m unittest -v test_native_policy test_probe +``` + +The synthetic database has 5,200 `openai` rows, one `opencodex` row, and one `other` +row. Default filtering yields one; the two-id opt-in yields 5,201; all providers +yields 5,202. Page sizes 1, 37, and 1,000 are exercised. The fixture dump hash stays +unchanged after reads. This demonstrates the specification, not native SQLite +schema compatibility or actual mobile results. + +## Not executed / not established + +- Native Rust implementation, compilation, config/schema generation, or native tests. +- Actual ChatGPT mobile pairing, full pagination, resume, token renewal, or reconnect. +- OpenCodex Bun typecheck, full suite, structure gate, or repository privacy gate: + Bun and a full checkout are unavailable in the execution environment. A git + checkout attempt failed at DNS resolution. Focused Python tests are not a + substitute for required repository gates; the PR must remain draft. +- Production multi-segment relay support or management/security review. + +The PR adds only this research unit, not a runtime fix. No workflow, executable +configuration, release artifact, or native storage is changed. Required exact-head +CI and independent review remain outstanding even if these probes pass. diff --git a/devlog/_plan/260928_remote_thread_provider_policy/probes/native_policy.py b/devlog/_plan/260928_remote_thread_provider_policy/probes/native_policy.py new file mode 100644 index 00000000000..d010f74df61 --- /dev/null +++ b/devlog/_plan/260928_remote_thread_provider_policy/probes/native_policy.py @@ -0,0 +1,70 @@ +"""Executable specification for a PROPOSED native Codex policy, not an integration. + +No socket, authentication, config, history, or rollout access. The real Rust +implementation must obtain origin from its authenticated server-side connection, +not from JSON, clientInfo.name, or an HTTP header. This enum only models that input. +None as the result means no provider predicate, not no results. +""" +from __future__ import annotations + +from dataclasses import dataclass +from enum import Enum + + +class ConnectionOrigin(Enum): + STDIO = "stdio" + WEBSOCKET = "websocket" + IN_PROCESS = "in_process" + REMOTE_CONTROL = "remote_control" + + +def _validate_ids(value: tuple[str, ...], label: str) -> None: + if not isinstance(value, tuple): + raise ValueError(f"{label} must be a tuple") + if any(not isinstance(item, str) or not item.strip() for item in value): + raise ValueError(f"{label} must contain non-empty strings") + if len(set(value)) != len(value): + raise ValueError(f"{label} must not contain duplicate identifiers") + + +@dataclass(frozen=True) +class RemoteListPolicy: + # None: opt-out; (): explicitly all; non-empty: exactly these provider ids. + providers: tuple[str, ...] | None = None + + def __post_init__(self) -> None: + if self.providers is not None: + _validate_ids(self.providers, "providers") + + +def resolve_provider_filter( + *, + origin: ConnectionOrigin, + default_provider: str, + requested: tuple[str, ...] | None = None, + parent_thread_id: str | None = None, + ancestor_thread_id: str | None = None, + policy: RemoteListPolicy = RemoteListPolicy(), +) -> tuple[str, ...] | None: + """Model precedence after typed request decoding and existing authorization. + + Rust Option> decodes both omission and JSON null as None. They + intentionally have the same semantics in this native-policy proposal. The + separate raw-frame relay probe instead preserves every present JSON key. + Empty requested tuple is an explicit client request for all providers. + The policy applies only to thread/list; callers must not reuse it for resume. + """ + if not isinstance(origin, ConnectionOrigin): + raise ValueError("origin must be supplied by the trusted connection context") + _validate_ids((default_provider,), "default_provider") + if requested is not None: + # Preserve every typed client array, including duplicate/empty ids. + # Native Vec accepts them; this proposal must not add rejection. + if not isinstance(requested, tuple) or any(not isinstance(item, str) for item in requested): + raise ValueError("requested must model a typed string array") + return requested or None + if parent_thread_id is not None or ancestor_thread_id is not None: + return None + if origin is ConnectionOrigin.REMOTE_CONTROL and policy.providers is not None: + return policy.providers or None + return (default_provider,) diff --git a/devlog/_plan/260928_remote_thread_provider_policy/probes/remote_list_probe.py b/devlog/_plan/260928_remote_thread_provider_policy/probes/remote_list_probe.py new file mode 100644 index 00000000000..a2622cdf18e --- /dev/null +++ b/devlog/_plan/260928_remote_thread_provider_policy/probes/remote_list_probe.py @@ -0,0 +1,160 @@ +"""Offline research probe; NOT an installed OpenCodex feature or production proxy. + +Based on openai/codex commit 1cc7e2361237ce7244430ee1d581c77f95c57ac8. +Only changes omitted thread/list.modelProviders in explicit opt-in mode. +No network, authentication, filesystem, database, or rollout writes occur here. +Complete multi-segment messages need a separate bounded streaming reassembler; +this probe deliberately passes those messages through unchanged. +""" +from __future__ import annotations + +import base64 +import binascii +import json +from dataclasses import dataclass +from typing import Any + + +@dataclass(frozen=True) +class Policy: + enabled: bool = False + # An empty tuple explicitly requests all providers, matching modelProviders: []. + providers: tuple[str, ...] = ("openai", "opencodex") + max_wire_bytes: int = 150 * 1024 + + def __post_init__(self) -> None: + if not isinstance(self.providers, tuple) or any( + not isinstance(p, str) or not p.strip() for p in self.providers + ): + raise ValueError("providers must be a tuple of non-empty strings") + if len(set(self.providers)) != len(self.providers): + raise ValueError("duplicate provider identifiers") + if self.max_wire_bytes < 64: + raise ValueError("max_wire_bytes must be at least 64") + + +@dataclass(frozen=True) +class Outcome: + text: str + changed: bool + reason: str + + +def _unique_object(pairs: list[tuple[str, Any]]) -> dict[str, Any]: + result: dict[str, Any] = {} + for key, value in pairs: + if key in result: + raise ValueError("duplicate JSON member") + result[key] = value + return result + + +def _reject_constant(value: str) -> None: + raise ValueError("non-standard JSON constant") + + +def decode(text: str | bytes) -> Any: + return json.loads(text, object_pairs_hook=_unique_object, + parse_constant=_reject_constant) + + +def encode(value: Any) -> str: + return json.dumps(value, ensure_ascii=False, separators=(",", ":"), + allow_nan=False) + + +def _patch_message(message: Any, policy: Policy) -> tuple[bool, str]: + if not isinstance(message, dict) or message.get("method") != "thread/list": + return False, "not_thread_list" + request_id = message.get("id") + if isinstance(request_id, bool) or not isinstance(request_id, (int, str)): + return False, "not_request" + if "result" in message or "error" in message: + return False, "ambiguous_request" + params = message.get("params", {}) + if not isinstance(params, dict): + return False, "non_object_params" + # Preserve explicit client choices, including an explicit null. + if "modelProviders" in params: + return False, "explicit_filter_preserved" + # Native related-thread queries deliberately bypass the omitted-provider default. + if params.get("parentThreadId") is not None or params.get("ancestorThreadId") is not None: + return False, "relation_query_preserved" + params["modelProviders"] = list(policy.providers) + message["params"] = params + return True, "omitted_filter_patched" + + +def rewrite_backend_frame(text: str, policy: Policy) -> Outcome: + """Process ONE backend-to-host text frame. No authentication decision is made. + + The caller must already enforce the authorized connection and user opt-in. + Unmodified and unsupported messages are returned byte-for-byte unchanged. + """ + if not isinstance(text, str): + raise TypeError("text frame must be str") + if not policy.enabled: + return Outcome(text, False, "disabled") + try: + if len(text.encode("utf-8")) > policy.max_wire_bytes: + return Outcome(text, False, "input_size_limit") + envelope = decode(text) + if not isinstance(envelope, dict): + return Outcome(text, False, "not_envelope") + if not isinstance(envelope.get("client_id"), str) or not envelope["client_id"]: + return Outcome(text, False, "missing_client_id") + kind = envelope.get("type") + if kind == "client_message": + changed, reason = _patch_message(envelope.get("message"), policy) + elif kind == "client_message_chunk": + if type(envelope.get("segment_count")) is not int or type(envelope.get("segment_id")) is not int: + return Outcome(text, False, "invalid_segment_index") + if envelope["segment_count"] != 1 or envelope["segment_id"] != 0: + return Outcome(text, False, "multi_segment_not_implemented") + size = envelope.get("message_size_bytes") + if type(size) is not int or size < 0 or size > policy.max_wire_bytes: + return Outcome(text, False, "invalid_segment_size") + payload_b64 = envelope.get("message_chunk_base64") + if not isinstance(payload_b64, str): + return Outcome(text, False, "invalid_segment_payload") + payload = base64.b64decode(payload_b64, validate=True) + if len(payload) != size: + return Outcome(text, False, "segment_length_mismatch") + message = decode(payload) + changed, reason = _patch_message(message, policy) + if changed: + encoded_payload = encode(message).encode("utf-8") + envelope["message_size_bytes"] = len(encoded_payload) + envelope["message_chunk_base64"] = base64.b64encode(encoded_payload).decode("ascii") + else: + return Outcome(text, False, "unrelated_envelope") + if not changed: + return Outcome(text, False, reason) + output = encode(envelope) + if len(output.encode("utf-8")) > policy.max_wire_bytes: + return Outcome(text, False, "output_size_limit") + return Outcome(output, True, reason) + except (ValueError, TypeError, UnicodeError, binascii.Error, RecursionError): + return Outcome(text, False, "invalid_frame_preserved") + + +def make_envelope(message: dict[str, Any], *, chunk: bool = False) -> dict[str, Any]: + """Synthetic fixture builder; all identifiers below are test-only.""" + envelope: dict[str, Any] = { + "client_id": "mock-client", "stream_id": "mock-stream", + "seq_id": 7, "cursor": "mock-backend-cursor", + } + if chunk: + payload = encode(message).encode("utf-8") + envelope.update(type="client_message_chunk", segment_id=0, segment_count=1, + message_size_bytes=len(payload), + message_chunk_base64=base64.b64encode(payload).decode("ascii")) + else: + envelope.update(type="client_message", message=message) + return envelope + + +def extract_message(envelope: dict[str, Any]) -> dict[str, Any]: + if envelope["type"] == "client_message": + return envelope["message"] + return decode(base64.b64decode(envelope["message_chunk_base64"], validate=True)) diff --git a/devlog/_plan/260928_remote_thread_provider_policy/probes/test_loopback_bridge.py b/devlog/_plan/260928_remote_thread_provider_policy/probes/test_loopback_bridge.py new file mode 100644 index 00000000000..1d2c840c095 --- /dev/null +++ b/devlog/_plan/260928_remote_thread_provider_policy/probes/test_loopback_bridge.py @@ -0,0 +1,228 @@ +"""Loopback-only mock transport proof. NEVER connects to ChatGPT/OpenAI. + +This is a test fixture, not a production proxy. Native Codex is not executed. +The mock backend validates a hard-coded, non-secret fixture token. +""" +from __future__ import annotations +import asyncio +import contextlib +import unittest +from urllib.parse import urlsplit + +import aiohttp +from aiohttp import web +from remote_list_probe import Policy, decode, encode, make_envelope, extract_message, rewrite_backend_frame + +BASE = "/backend-api" +WS_PATH = BASE + "/wham/remote/control/server" +MOCK_AUTH = "Bearer NOT-A-REAL-TOKEN-MOCK-ONLY" + + +def assert_loopback_only(base: str) -> None: + url = urlsplit(base) + if url.scheme != "http" or url.hostname != "127.0.0.1" or not url.port or url.username or url.password: + raise ValueError("this research fixture permits only HTTP at literal 127.0.0.1") + if url.query or url.fragment or url.path: + raise ValueError("mock base must contain only origin") + + +def selected_headers(headers) -> dict[str, str]: + return {key: value for key, value in headers.items() + if key.lower() in ("authorization", "chatgpt-account-id", "content-type") + or key.lower().startswith("x-codex-")} + + +class LoopbackFixtureRelay: + def __init__(self, mock_backend: str, session: aiohttp.ClientSession, policy: Policy): + assert_loopback_only(mock_backend) + self.backend = mock_backend + self.session = session + self.policy = policy + + async def handle(self, request: web.Request) -> web.StreamResponse: + # Test-only origin/host guard. No remote bind and no arbitrary upstream selection. + if request.headers.get("Origin"): + return web.Response(status=403, text="browser-origin-blocked") + if not request.host.startswith("127.0.0.1:"): + return web.Response(status=403, text="unexpected-host") + if request.path == WS_PATH and request.headers.get("Upgrade", "").lower() == "websocket": + return await self.handle_websocket(request) + target = self.backend + request.raw_path + async with self.session.request(request.method, target, + data=await request.read(), + headers=selected_headers(request.headers), + allow_redirects=False) as response: + body = await response.read() + headers = {key: value for key, value in response.headers.items() + if key.lower() in ("content-type", "x-request-id", "retry-after")} + return web.Response(status=response.status, body=body, headers=headers) + + async def handle_websocket(self, request: web.Request) -> web.StreamResponse: + target = self.backend + request.raw_path + try: + upstream = await self.session.ws_connect(target, + headers=selected_headers(request.headers), autoping=False) + except aiohttp.WSServerHandshakeError as exc: + return web.Response(status=exc.status, text="mock-upstream-rejected") + downstream = web.WebSocketResponse(autoping=False) + await downstream.prepare(request) + + async def pump(source, destination, transform: bool): + async for message in source: + if message.type == aiohttp.WSMsgType.TEXT: + text = rewrite_backend_frame(message.data, self.policy).text if transform else message.data + await destination.send_str(text) + elif message.type == aiohttp.WSMsgType.BINARY: + await destination.send_bytes(message.data) + elif message.type == aiohttp.WSMsgType.PING: + await destination.ping(message.data) + elif message.type == aiohttp.WSMsgType.PONG: + await destination.pong(message.data) + else: + break + + tasks = [asyncio.create_task(pump(upstream, downstream, True)), + asyncio.create_task(pump(downstream, upstream, False))] + try: + done, _ = await asyncio.wait(tasks, return_when=asyncio.FIRST_COMPLETED) + for task in done: + task.result() + finally: + for task in tasks: + task.cancel() + await asyncio.gather(*tasks, return_exceptions=True) + await upstream.close() + await downstream.close() + return downstream + + +async def start_loopback_app(app: web.Application): + runner = web.AppRunner(app, access_log=None) + await runner.setup() + site = web.TCPSite(runner, "127.0.0.1", 0) + await site.start() + port = runner.addresses[0][1] + return runner, f"http://127.0.0.1:{port}" + + +class LoopbackBridgeTests(unittest.IsolatedAsyncioTestCase): + async def asyncSetUp(self): + self.frames = [] + self.received = asyncio.Queue() + self.handshake_headers = {} + self.http_received = [] + self.backend_app = web.Application() + self.backend_app.router.add_route("*", "/backend-api/{tail:.*}", self.mock_backend) + self.backend_runner, self.backend_base = await start_loopback_app(self.backend_app) + self.relay_session = aiohttp.ClientSession(timeout=aiohttp.ClientTimeout(total=5)) + self.relay = LoopbackFixtureRelay(self.backend_base, self.relay_session, Policy(enabled=True)) + relay_app = web.Application() + relay_app.router.add_route("*", "/backend-api/{tail:.*}", self.relay.handle) + self.relay_runner, self.relay_base = await start_loopback_app(relay_app) + self.host = aiohttp.ClientSession(timeout=aiohttp.ClientTimeout(total=5)) + + async def asyncTearDown(self): + await self.host.close() + await self.relay_runner.cleanup() + await self.relay_session.close() + await self.backend_runner.cleanup() + + async def mock_backend(self, request: web.Request): + if request.headers.get("Authorization") != MOCK_AUTH: + return web.Response(status=401, text="mock-auth-required") + if request.path == WS_PATH and request.headers.get("Upgrade", "").lower() == "websocket": + self.handshake_headers = dict(request.headers) + websocket = web.WebSocketResponse(autoping=False) + await websocket.prepare(request) + for frame in self.frames: + await websocket.send_str(frame) + async for message in websocket: + if message.type == aiohttp.WSMsgType.TEXT: + await self.received.put(message.data) + elif message.type == aiohttp.WSMsgType.PING: + await websocket.pong(message.data) + return websocket + body = await request.read() + self.http_received.append((request.path, body, selected_headers(request.headers))) + if request.path.endswith("/enroll"): + return web.json_response({"server_id": "mock-server", "environment_id": "mock-env", + "remote_control_token": "MOCK-ONLY", "expires_at": "2099-01-01T00:00:00Z"}, + status=201, headers={"x-request-id": "mock-http-request"}) + return web.Response(status=200, body=b"mock-unrelated-backend-endpoint") + + def add_request(self, params=None, *, chunk=False): + request = {"id": 42, "method": "thread/list", "params": params or {"limit": 20}} + self.frames.append(encode(make_envelope(request, chunk=chunk))) + + async def receive_host_frame(self, headers=None): + async with self.host.ws_connect(self.relay_base + WS_PATH, + headers=headers or {"Authorization": MOCK_AUTH}) as host: + message = await asyncio.wait_for(host.receive(), 2) + self.assertEqual(message.type, aiohttp.WSMsgType.TEXT) + return message.data + + async def test_backend_to_host_rewrite_and_host_reply_passthrough(self): + self.add_request() + async with self.host.ws_connect(self.relay_base + WS_PATH, + headers={"Authorization": MOCK_AUTH}) as host: + incoming = await asyncio.wait_for(host.receive(), 2) + envelope = decode(incoming.data) + self.assertEqual(extract_message(envelope)["params"]["modelProviders"], ["openai", "opencodex"]) + response = '{ "type":"server_message", "client_id":"mock-client", "stream_id":"mock-stream", "seq_id":9, "message":{"id":42,"result":{"data":[]}} }' + await host.send_str(response) + self.assertEqual(await asyncio.wait_for(self.received.get(), 2), response) + + async def test_auth_and_protocol_handshake_headers_preserved(self): + self.add_request() + headers = {"Authorization": MOCK_AUTH, "chatgpt-account-id": "mock-account", + "x-codex-protocol-version": "3", "x-codex-server-id": "mock-server", + "x-codex-installation-id": "mock-installation", "x-codex-subscribe-cursor": "mock-resume-cursor"} + await self.receive_host_frame(headers) + actual = {k.lower(): v for k, v in self.handshake_headers.items()} + for name, value in headers.items(): + self.assertEqual(actual[name.lower()], value) + + async def test_invalid_auth_stays_denied(self): + with self.assertRaises(aiohttp.WSServerHandshakeError) as ctx: + await self.host.ws_connect(self.relay_base + WS_PATH, + headers={"Authorization": "Bearer WRONG-MOCK"}) + self.assertEqual(ctx.exception.status, 401) + + async def test_explicit_filter_frame_is_byte_identical(self): + self.add_request({"modelProviders": ["other"], "limit": 20}) + self.assertEqual(await self.receive_host_frame(), self.frames[0]) + + async def test_single_chunk_transport(self): + self.add_request(chunk=True) + incoming = decode(await self.receive_host_frame()) + self.assertEqual(extract_message(incoming)["params"]["modelProviders"], ["openai", "opencodex"]) + self.assertEqual(incoming["seq_id"], 7) + self.assertEqual(incoming["cursor"], "mock-backend-cursor") + + async def test_enrollment_http_forwarding_with_fake_credentials(self): + body = b'{"name":"mock-host","installation_id":"mock-installation"}' + async with self.host.post(self.relay_base + WS_PATH + "/enroll", data=body, + headers={"Authorization": MOCK_AUTH, "chatgpt-account-id": "mock-account"}) as response: + self.assertEqual(response.status, 201) + self.assertEqual(response.headers["x-request-id"], "mock-http-request") + self.assertEqual((await response.json())["server_id"], "mock-server") + self.assertEqual(self.http_received[0][1], body) + + async def test_unrelated_http_backend_endpoint_forwarded(self): + async with self.host.get(self.relay_base + BASE + "/mock/account", headers={"Authorization": MOCK_AUTH}) as response: + self.assertEqual(response.status, 200) + self.assertEqual(await response.read(), b"mock-unrelated-backend-endpoint") + + async def test_cross_origin_browser_request_denied(self): + async with self.host.get(self.relay_base + BASE + "/mock/account", headers={"Authorization": MOCK_AUTH, "Origin": "https://example.invalid"}) as response: + self.assertEqual(response.status, 403) + self.assertFalse(self.http_received) + + async def test_fixture_rejects_real_service_upstream(self): + for base in ("https://chatgpt.com", "http://example.invalid", "http://0.0.0.0:1234"): + with self.subTest(base=base): + with self.assertRaises(ValueError): + LoopbackFixtureRelay(base, self.relay_session, Policy(enabled=True)) + +if __name__ == "__main__": + unittest.main(verbosity=2) diff --git a/devlog/_plan/260928_remote_thread_provider_policy/probes/test_native_policy.py b/devlog/_plan/260928_remote_thread_provider_policy/probes/test_native_policy.py new file mode 100644 index 00000000000..e2a9ccf4079 --- /dev/null +++ b/devlog/_plan/260928_remote_thread_provider_policy/probes/test_native_policy.py @@ -0,0 +1,153 @@ +"""Offline contract tests. This does NOT run Rust, native Codex, or a mobile app.""" +from __future__ import annotations + +import hashlib +import json +import sqlite3 +import unittest +from dataclasses import FrozenInstanceError +from native_policy import ConnectionOrigin as Origin, RemoteListPolicy, resolve_provider_filter + + +class NativePolicyTests(unittest.TestCase): + def resolve(self, **kwargs): + defaults = {"origin": Origin.REMOTE_CONTROL, "default_provider": "opencodex"} + defaults.update(kwargs) + return resolve_provider_filter(**defaults) + + def test_opt_out_keeps_existing_default_for_every_origin(self): + for origin in Origin: + with self.subTest(origin=origin): + self.assertEqual(self.resolve(origin=origin), ("opencodex",)) + + def test_only_native_remote_origin_uses_opt_in(self): + policy = RemoteListPolicy(("openai", "opencodex")) + for origin in Origin: + with self.subTest(origin=origin): + expected = policy.providers if origin is Origin.REMOTE_CONTROL else ("opencodex",) + self.assertEqual(self.resolve(origin=origin, policy=policy), expected) + + def test_explicit_nonempty_filter_wins_over_every_policy(self): + for policy in (RemoteListPolicy(), RemoteListPolicy(()), RemoteListPolicy(("openai",))): + for origin in Origin: + with self.subTest(policy=policy, origin=origin): + for requested in (("other",), ("other", "other"), ("",), (" other ",)): + self.assertEqual(self.resolve(origin=origin, requested=requested, policy=policy), requested) + + def test_explicit_empty_filter_is_all_for_every_origin(self): + for origin in Origin: + with self.subTest(origin=origin): + self.assertIsNone(self.resolve(origin=origin, requested=(), policy=RemoteListPolicy(("openai",)))) + + def test_opt_in_all_is_not_the_default(self): + self.assertIsNone(self.resolve(policy=RemoteListPolicy(()))) + self.assertEqual(self.resolve(), ("opencodex",)) + + def test_parent_query_keeps_native_no_default_filter(self): + for policy in (RemoteListPolicy(), RemoteListPolicy(("opencodex",))): + self.assertIsNone(self.resolve(parent_thread_id="fixture-parent", policy=policy)) + + def test_ancestor_query_keeps_native_no_default_filter(self): + for policy in (RemoteListPolicy(), RemoteListPolicy(("opencodex",))): + self.assertIsNone(self.resolve(ancestor_thread_id="fixture-ancestor", policy=policy)) + + def test_explicit_filter_still_wins_for_related_queries(self): + for relation in ({"parent_thread_id": "p"}, {"ancestor_thread_id": "a"}): + self.assertEqual(self.resolve(requested=("other",), policy=RemoteListPolicy(()), **relation), ("other",)) + + def test_null_and_omitted_match_typed_native_option_semantics(self): + for wire in ('{}', '{"modelProviders": null}'): + requested = json.loads(wire).get("modelProviders") + self.assertIsNone(requested) + self.assertEqual(self.resolve(requested=requested, policy=RemoteListPolicy(("openai",))), ("openai",)) + + def test_provider_names_are_not_hardcoded(self): + self.assertEqual(self.resolve(default_provider="custom"), ("custom",)) + self.assertEqual(self.resolve(policy=RemoteListPolicy(("azure-team", "local"))), ("azure-team", "local")) + + def test_provider_ids_are_exact_not_aliases(self): + self.assertEqual(self.resolve(policy=RemoteListPolicy(("OpenAI", " openai "))), ("OpenAI", " openai ")) + + def test_policy_cannot_be_mutated_during_pagination(self): + policy = RemoteListPolicy(("openai",)) + with self.assertRaises(FrozenInstanceError): + policy.providers = () + + def test_rejects_invalid_policy(self): + for value in (["openai"], "openai", ("",), (" ",), (1,), ("openai", "openai")): + with self.subTest(value=value), self.assertRaises(ValueError): + RemoteListPolicy(value) + + def test_rejects_untrusted_origin_string(self): + with self.assertRaises(ValueError): + self.resolve(origin="remote_control") + + def test_rejects_invalid_default_provider(self): + for value in ("", " ", None, 1): + with self.subTest(value=value), self.assertRaises(ValueError): + self.resolve(default_provider=value) + + def test_rejects_invalid_requested_filter(self): + for value in (["other"], "other", (1,), (None,)): + with self.subTest(value=value), self.assertRaises(ValueError): + self.resolve(requested=value) + + +class SyntheticPaginationTests(unittest.TestCase): + """Fixture SQL illustrates the contract, not the native Codex store schema.""" + def setUp(self): + self.db = sqlite3.connect(":memory:") + self.addCleanup(self.db.close) + self.db.execute("CREATE TABLE threads (id INTEGER PRIMARY KEY, provider TEXT NOT NULL)") + self.db.executemany("INSERT INTO threads VALUES (?, ?)", + [(i, "openai") for i in range(1, 5201)] + [(5201, "opencodex"), (5202, "other")]) + self.before = self.digest() + + def digest(self): + return hashlib.sha256("\n".join(self.db.iterdump()).encode()).hexdigest() + + def list_all(self, policy, limit=37, requested=None): + providers = resolve_provider_filter(origin=Origin.REMOTE_CONTROL, + default_provider="opencodex", requested=requested, policy=policy) + rows, last_id = [], 0 + while True: + values = [last_id] + sql = "SELECT id, provider FROM threads WHERE id > ?" + if providers is not None: + sql += " AND provider IN (" + ",".join("?" for _ in providers) + ")" + values.extend(providers) + sql += " ORDER BY id LIMIT ?" + values.append(limit) + page = self.db.execute(sql, values).fetchall() + if not page: + break + rows.extend(page) + last_id = page[-1][0] + self.assertEqual(self.digest(), self.before, "read must not retag fixture rows") + self.assertEqual(len({row[0] for row in rows}), len(rows)) + return rows + + def test_default_remains_one_thread(self): + self.assertEqual(self.list_all(RemoteListPolicy()), [(5201, "opencodex")]) + + def test_opt_in_two_provider_list_returns_5201_rows(self): + rows = self.list_all(RemoteListPolicy(("openai", "opencodex"))) + self.assertEqual(len(rows), 5201) + self.assertNotIn("other", {row[1] for row in rows}) + + def test_explicit_all_returns_5202_rows(self): + self.assertEqual(len(self.list_all(RemoteListPolicy(()))), 5202) + + def test_explicit_client_filter_not_broadened(self): + self.assertEqual(self.list_all(RemoteListPolicy(()), requested=("other",)), [(5202, "other")]) + + def test_unlisted_provider_returns_empty_without_fallback(self): + self.assertEqual(self.list_all(RemoteListPolicy(("not-in-fixture",))), []) + + def test_every_page_uses_same_policy_without_duplicates(self): + policy = RemoteListPolicy(("openai", "opencodex")) + self.assertEqual(self.list_all(policy, limit=1), self.list_all(policy, limit=1000)) + + +if __name__ == "__main__": + unittest.main(verbosity=2) diff --git a/devlog/_plan/260928_remote_thread_provider_policy/probes/test_probe.py b/devlog/_plan/260928_remote_thread_provider_policy/probes/test_probe.py new file mode 100644 index 00000000000..a8e4ff7a485 --- /dev/null +++ b/devlog/_plan/260928_remote_thread_provider_policy/probes/test_probe.py @@ -0,0 +1,196 @@ +from __future__ import annotations +import base64 +import hashlib +import json +import sqlite3 +import unittest +from copy import deepcopy +from remote_list_probe import Policy, decode, encode, make_envelope, extract_message, rewrite_backend_frame + + +class RewriteTests(unittest.TestCase): + def setUp(self): + self.policy = Policy(enabled=True) + self.request = {"id": 27, "method": "thread/list", "params": {"limit": 20, "cursor": "opaque"}} + + def frame(self, request=None, chunk=False): + return encode(make_envelope(deepcopy(self.request if request is None else request), chunk=chunk)) + + def result(self, request=None, chunk=False, policy=None): + return rewrite_backend_frame(self.frame(request, chunk), policy or self.policy) + + def assert_preserved(self, raw): + outcome = rewrite_backend_frame(raw, self.policy) + self.assertFalse(outcome.changed) + self.assertEqual(raw, outcome.text) + + def test_opt_in_required(self): + raw = self.frame() + self.assertEqual(rewrite_backend_frame(raw, Policy()).text, raw) + self.assertFalse(rewrite_backend_frame(raw, Policy()).changed) + + def test_plain_omitted_filter(self): + out = self.result() + self.assertTrue(out.changed) + self.assertEqual(extract_message(decode(out.text))["params"]["modelProviders"], ["openai", "opencodex"]) + + def test_explicit_all_provider_mode(self): + out = self.result(policy=Policy(enabled=True, providers=())) + self.assertEqual(extract_message(decode(out.text))["params"]["modelProviders"], []) + + def test_preserves_envelope_metadata(self): + before = decode(self.frame()) + after = decode(self.result().text) + for key in ("client_id", "stream_id", "seq_id", "cursor"): + self.assertEqual(before[key], after[key]) + + def test_preserves_all_other_parameters(self): + self.request["params"].update(archived=True, cwd="C:/mock", sourceKinds=["cli"], + sortDirection="asc", searchTerm="mock", isPinned=False) + after = extract_message(decode(self.result().text)) + del after["params"]["modelProviders"] + self.assertEqual(after, self.request) + + def test_explicit_filter_preserved(self): + self.request["params"]["modelProviders"] = ["other"] + self.assert_preserved(self.frame()) + + def test_explicit_empty_filter_preserved(self): + self.request["params"]["modelProviders"] = [] + self.assert_preserved(self.frame()) + + def test_explicit_null_filter_preserved(self): + self.request["params"]["modelProviders"] = None + self.assert_preserved(self.frame()) + + def test_missing_params_created(self): + del self.request["params"] + self.assertTrue(self.result().changed) + + def test_non_object_params_preserved(self): + for value in (None, [], "not-an-object", 1): + with self.subTest(value=value): + self.request["params"] = value + self.assert_preserved(self.frame()) + + def test_resume_and_other_methods_unchanged(self): + for method in ("thread/resume", "thread/start", "turn/start", "turn/interrupt", "initialize", "thread/search"): + with self.subTest(method=method): + self.request["method"] = method + self.assert_preserved(self.frame()) + + def test_notification_without_id_unchanged(self): + del self.request["id"] + self.assert_preserved(self.frame()) + + def test_request_id_preserved_exactly(self): + for request_id in ("request-string", 9007199254740993, 0, -1): + with self.subTest(request_id=request_id): + self.request["id"] = request_id + self.assertEqual(extract_message(decode(self.result().text))["id"], request_id) + + def test_response_like_object_unchanged(self): + self.request["result"] = {"data": []} + self.assert_preserved(self.frame()) + + def test_relation_query_bypass_preserved(self): + for key in ("parentThreadId", "ancestorThreadId"): + with self.subTest(key=key): + self.request["params"] = {key: "mock-parent"} + self.assert_preserved(self.frame()) + + def test_null_relation_does_not_block(self): + self.request["params"]["parentThreadId"] = None + self.assertTrue(self.result().changed) + + def test_ping_ack_close_unchanged(self): + for kind in ("ping", "ack", "client_closed", "unknown_future_event"): + with self.subTest(kind=kind): + self.assert_preserved(encode({"type": kind, "client_id": "mock-client", "seq_id": 3})) + + def test_invalid_json_preserved(self): + for raw in ("{broken", "[]", "null", '{"type":"client_message","client_id":"x","message":NaN}'): + with self.subTest(raw=raw): + self.assert_preserved(raw) + + def test_duplicate_members_preserved(self): + raw = self.frame().replace('"method":"thread/list"', '"method":"thread/list","method":"thread/resume"') + self.assert_preserved(raw) + + def test_unknown_envelope_fields_preserved(self): + envelope = decode(self.frame()) + envelope["future_field"] = {"tag": [1, 2, 3]} + out = rewrite_backend_frame(encode(envelope), self.policy) + self.assertEqual(decode(out.text)["future_field"], envelope["future_field"]) + + def test_single_chunk_transformed_with_correct_byte_size(self): + self.request["params"]["searchTerm"] = "ν•œκΈ€πŸ™‚" + out = self.result(chunk=True) + self.assertTrue(out.changed) + after = decode(out.text) + payload = base64.b64decode(after["message_chunk_base64"], validate=True) + self.assertEqual(len(payload), after["message_size_bytes"]) + self.assertEqual(decode(payload)["params"]["modelProviders"], ["openai", "opencodex"]) + self.assertEqual(after["seq_id"], 7) + + def test_single_chunk_existing_filter_unchanged(self): + self.request["params"]["modelProviders"] = ["other"] + self.assert_preserved(self.frame(chunk=True)) + + def test_multi_segment_deliberately_not_implemented(self): + envelope = decode(self.frame(chunk=True)) + envelope["segment_count"] = 2 + out = rewrite_backend_frame(encode(envelope), self.policy) + self.assertEqual(out.reason, "multi_segment_not_implemented") + self.assertFalse(out.changed) + + def test_bad_chunk_length_unchanged(self): + envelope = decode(self.frame(chunk=True)) + envelope["message_size_bytes"] += 1 + self.assert_preserved(encode(envelope)) + + def test_bad_chunk_base64_unchanged(self): + envelope = decode(self.frame(chunk=True)) + envelope["message_chunk_base64"] = "not@@base64" + self.assert_preserved(encode(envelope)) + + def test_oversized_input_unchanged(self): + raw = self.frame() + out = rewrite_backend_frame(raw, Policy(enabled=True, max_wire_bytes=64)) + self.assertFalse(out.changed) + self.assertEqual(out.text, raw) + + def test_oversized_output_unchanged(self): + raw = self.frame() + out = rewrite_backend_frame(raw, Policy(enabled=True, max_wire_bytes=len(raw.encode()))) + self.assertFalse(out.changed) + self.assertEqual(out.reason, "output_size_limit") + + def test_invalid_policy_rejected(self): + for providers in (("",), ("a", "a"), (1,), ["a"]): + with self.subTest(providers=providers): + with self.assertRaises(ValueError): + Policy(enabled=True, providers=providers) + + def test_synthetic_sqlite_visibility_and_no_mutation(self): + # Synthetic fixture, NOT the user's database and NOT native Codex. + db = sqlite3.connect(":memory:") + try: + db.execute("CREATE TABLE threads (id INTEGER, model_provider TEXT)") + db.executemany("INSERT INTO threads VALUES (?, ?)", [(i, "openai") for i in range(5200)]) + db.execute("INSERT INTO threads VALUES (5200, 'opencodex')") + db.execute("INSERT INTO threads VALUES (5201, 'unrelated')") + before = hashlib.sha256("\n".join(db.iterdump()).encode()).hexdigest() + self.assertEqual(db.execute("SELECT count(*) FROM threads WHERE model_provider = 'opencodex'").fetchone()[0], 1) + providers = extract_message(decode(self.result().text))["params"]["modelProviders"] + placeholders = ",".join("?" for _ in providers) + scoped = db.execute(f"SELECT count(*) FROM threads WHERE model_provider IN ({placeholders})", providers).fetchone()[0] + self.assertEqual(scoped, 5201) + self.assertEqual(db.execute("SELECT count(*) FROM threads").fetchone()[0], 5202) + after = hashlib.sha256("\n".join(db.iterdump()).encode()).hexdigest() + self.assertEqual(before, after) + finally: + db.close() + +if __name__ == "__main__": + unittest.main(verbosity=2) From a0f933c78232f10cb42a8e31071b88edb510a924 Mon Sep 17 00:00:00 2001 From: luvs01 <27862058+luvs01@users.noreply.github.com> Date: Mon, 28 Sep 2026 16:19:53 +0900 Subject: [PATCH 2/9] docs-codex: reject malformed thread ids in the native-policy spec --- .../probes/native_policy.py | 8 ++++++++ .../probes/test_native_policy.py | 9 +++++++++ 2 files changed, 17 insertions(+) diff --git a/devlog/_plan/260928_remote_thread_provider_policy/probes/native_policy.py b/devlog/_plan/260928_remote_thread_provider_policy/probes/native_policy.py index d010f74df61..00dd5c737bf 100644 --- a/devlog/_plan/260928_remote_thread_provider_policy/probes/native_policy.py +++ b/devlog/_plan/260928_remote_thread_provider_policy/probes/native_policy.py @@ -57,6 +57,14 @@ def resolve_provider_filter( if not isinstance(origin, ConnectionOrigin): raise ValueError("origin must be supplied by the trusted connection context") _validate_ids((default_provider,), "default_provider") + for label, tid in ( + ("parent_thread_id", parent_thread_id), + ("ancestor_thread_id", ancestor_thread_id), + ): + if tid is not None and not tid.strip(): + raise ValueError(f"{label} must be a non-empty thread id") + if parent_thread_id is not None and ancestor_thread_id is not None: + raise ValueError("parent_thread_id and ancestor_thread_id are mutually exclusive") if requested is not None: # Preserve every typed client array, including duplicate/empty ids. # Native Vec accepts them; this proposal must not add rejection. diff --git a/devlog/_plan/260928_remote_thread_provider_policy/probes/test_native_policy.py b/devlog/_plan/260928_remote_thread_provider_policy/probes/test_native_policy.py index e2a9ccf4079..61af3a5a2b8 100644 --- a/devlog/_plan/260928_remote_thread_provider_policy/probes/test_native_policy.py +++ b/devlog/_plan/260928_remote_thread_provider_policy/probes/test_native_policy.py @@ -51,6 +51,15 @@ def test_ancestor_query_keeps_native_no_default_filter(self): for policy in (RemoteListPolicy(), RemoteListPolicy(("opencodex",))): self.assertIsNone(self.resolve(ancestor_thread_id="fixture-ancestor", policy=policy)) + def test_related_thread_ids_follow_native_validation(self): + # Upstream thread_list_response_inner rejects malformed ids and rejects + # parent+ancestor together; a malformed id must not silently widen the list. + for relation in ({"parent_thread_id": ""}, {"ancestor_thread_id": " "}): + with self.subTest(relation=relation), self.assertRaises(ValueError): + self.resolve(**relation) + with self.assertRaises(ValueError): + self.resolve(parent_thread_id="p", ancestor_thread_id="a") + def test_explicit_filter_still_wins_for_related_queries(self): for relation in ({"parent_thread_id": "p"}, {"ancestor_thread_id": "a"}): self.assertEqual(self.resolve(requested=("other",), policy=RemoteListPolicy(()), **relation), ("other",)) From 3b2911795b4e4dbeae52d66dfbbcc44802ed233f Mon Sep 17 00:00:00 2001 From: luvs01 <27862058+luvs01@users.noreply.github.com> Date: Tue, 29 Sep 2026 04:06:18 +0900 Subject: [PATCH 3/9] docs(codex): validate UUID thread ids, fix probe counts, and run probes in CI (#6157) --- .github/workflows/devlog-probes.yml | 43 +++++++++++++++++++ .../020_verification.md | 12 ++++-- .../probes/native_policy.py | 13 +++++- .../probes/test_native_policy.py | 22 +++++++--- .../probes/test_probe.py | 2 +- 5 files changed, 80 insertions(+), 12 deletions(-) create mode 100644 .github/workflows/devlog-probes.yml diff --git a/.github/workflows/devlog-probes.yml b/.github/workflows/devlog-probes.yml new file mode 100644 index 00000000000..611454f40a6 --- /dev/null +++ b/.github/workflows/devlog-probes.yml @@ -0,0 +1,43 @@ +name: devlog probes + +# Runs the offline unit probes that live beside a devlog plan. The suite exists so +# an exact-head gate actually executes the research fixtures a docs PR cites +# instead of leaving them as locally-verified-only claims. + +permissions: + contents: read + +on: + pull_request: + paths: + - "devlog/**/probes/**" + - ".github/workflows/devlog-probes.yml" + +concurrency: + group: devlog-probes-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + unittest: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + - uses: actions/setup-python@v6 + with: + python-version: "3.x" + - name: aiohttp for socket fixtures (best effort) + run: python -m pip install aiohttp + continue-on-error: true + - name: Run every discovered probe suite + shell: bash + run: | + set -euo pipefail + found=0 + while IFS= read -r -d '' dir; do + found=1 + echo "== probes: $dir" + (cd "$dir" && python -m unittest discover -s . -p 'test_*.py' -v) + done < <(find devlog -type d -name probes -print0) + if [[ "$found" -eq 0 ]]; then + echo "no devlog probe directories present" + fi diff --git a/devlog/_plan/260928_remote_thread_provider_policy/020_verification.md b/devlog/_plan/260928_remote_thread_provider_policy/020_verification.md index 76564b99534..28f3dc7bcef 100644 --- a/devlog/_plan/260928_remote_thread_provider_policy/020_verification.md +++ b/devlog/_plan/260928_remote_thread_provider_policy/020_verification.md @@ -11,20 +11,24 @@ cd devlog/_plan/260928_remote_thread_provider_policy/probes python -m unittest -v test_native_policy test_probe test_loopback_bridge ``` -**Executed: 60 tests, 0 failures.** +**Executed: 61 tests, 0 failures.** -- 22 tests specify the proposed native policy: opt-out, trusted-origin scoping, +- 23 tests specify the proposed native policy: opt-out, trusted-origin scoping, explicit arrays, null semantics, parent/ancestor exceptions, immutable policy, - exact ids, validation, and synthetic paginated fixture reads. + exact ids, UUID thread-id validation, and synthetic paginated fixture reads. - 29 retained raw-frame tests cover the relay alternative's ordinary/single-chunk rewrites, byte-identical pass-through, limits, and unsupported inputs. - 9 retained localhost HTTP/WebSocket tests use a mock host and mock backend, checking two-way traffic, fixture auth/headers, explicit filters, enrollment, unrelated HTTP, and fixture endpoint restrictions. -The 22 + 29 offline tests use the Python standard library only. The 9 socket tests +The 23 + 29 offline tests use the Python standard library only. The 9 socket tests need aiohttp already installed; no production dependency manifest is changed. +Hosted CI runs the same command for every devlog/**/probes directory it finds +(.github/workflows/devlog-probes.yml): the offline unit tests always run, and +the aiohttp socket tests run when aiohttp is present on the runner image. + ```sh python -m unittest -v test_native_policy test_probe ``` diff --git a/devlog/_plan/260928_remote_thread_provider_policy/probes/native_policy.py b/devlog/_plan/260928_remote_thread_provider_policy/probes/native_policy.py index 00dd5c737bf..8f2aa1066f7 100644 --- a/devlog/_plan/260928_remote_thread_provider_policy/probes/native_policy.py +++ b/devlog/_plan/260928_remote_thread_provider_policy/probes/native_policy.py @@ -9,6 +9,7 @@ from dataclasses import dataclass from enum import Enum +from uuid import UUID class ConnectionOrigin(Enum): @@ -27,6 +28,14 @@ def _validate_ids(value: tuple[str, ...], label: str) -> None: raise ValueError(f"{label} must not contain duplicate identifiers") +def _validate_thread_id(value: str, label: str) -> None: + """Upstream parses ThreadId as a UUID; a non-UUID string must not widen the list.""" + try: + UUID(value) + except (ValueError, AttributeError, TypeError): + raise ValueError(f"{label} must be a UUID thread id") + + @dataclass(frozen=True) class RemoteListPolicy: # None: opt-out; (): explicitly all; non-empty: exactly these provider ids. @@ -61,8 +70,8 @@ def resolve_provider_filter( ("parent_thread_id", parent_thread_id), ("ancestor_thread_id", ancestor_thread_id), ): - if tid is not None and not tid.strip(): - raise ValueError(f"{label} must be a non-empty thread id") + if tid is not None: + _validate_thread_id(tid, label) if parent_thread_id is not None and ancestor_thread_id is not None: raise ValueError("parent_thread_id and ancestor_thread_id are mutually exclusive") if requested is not None: diff --git a/devlog/_plan/260928_remote_thread_provider_policy/probes/test_native_policy.py b/devlog/_plan/260928_remote_thread_provider_policy/probes/test_native_policy.py index 61af3a5a2b8..eba438a51e4 100644 --- a/devlog/_plan/260928_remote_thread_provider_policy/probes/test_native_policy.py +++ b/devlog/_plan/260928_remote_thread_provider_policy/probes/test_native_policy.py @@ -45,23 +45,35 @@ def test_opt_in_all_is_not_the_default(self): def test_parent_query_keeps_native_no_default_filter(self): for policy in (RemoteListPolicy(), RemoteListPolicy(("opencodex",))): - self.assertIsNone(self.resolve(parent_thread_id="fixture-parent", policy=policy)) + self.assertIsNone(self.resolve(parent_thread_id="00000000-0000-4000-8000-0000000000aa", policy=policy)) def test_ancestor_query_keeps_native_no_default_filter(self): for policy in (RemoteListPolicy(), RemoteListPolicy(("opencodex",))): - self.assertIsNone(self.resolve(ancestor_thread_id="fixture-ancestor", policy=policy)) + self.assertIsNone(self.resolve(ancestor_thread_id="00000000-0000-4000-8000-0000000000bb", policy=policy)) def test_related_thread_ids_follow_native_validation(self): # Upstream thread_list_response_inner rejects malformed ids and rejects # parent+ancestor together; a malformed id must not silently widen the list. - for relation in ({"parent_thread_id": ""}, {"ancestor_thread_id": " "}): + for relation in ( + {"parent_thread_id": ""}, + {"ancestor_thread_id": " "}, + {"parent_thread_id": "fixture-parent"}, + {"ancestor_thread_id": "fixture-ancestor"}, + {"parent_thread_id": "p"}, + ): with self.subTest(relation=relation), self.assertRaises(ValueError): self.resolve(**relation) with self.assertRaises(ValueError): - self.resolve(parent_thread_id="p", ancestor_thread_id="a") + self.resolve( + parent_thread_id="00000000-0000-4000-8000-0000000000aa", + ancestor_thread_id="00000000-0000-4000-8000-0000000000bb", + ) def test_explicit_filter_still_wins_for_related_queries(self): - for relation in ({"parent_thread_id": "p"}, {"ancestor_thread_id": "a"}): + for relation in ( + {"parent_thread_id": "00000000-0000-4000-8000-0000000000aa"}, + {"ancestor_thread_id": "00000000-0000-4000-8000-0000000000bb"}, + ): self.assertEqual(self.resolve(requested=("other",), policy=RemoteListPolicy(()), **relation), ("other",)) def test_null_and_omitted_match_typed_native_option_semantics(self): diff --git a/devlog/_plan/260928_remote_thread_provider_policy/probes/test_probe.py b/devlog/_plan/260928_remote_thread_provider_policy/probes/test_probe.py index a8e4ff7a485..ffdb0c6e24d 100644 --- a/devlog/_plan/260928_remote_thread_provider_policy/probes/test_probe.py +++ b/devlog/_plan/260928_remote_thread_provider_policy/probes/test_probe.py @@ -96,7 +96,7 @@ def test_response_like_object_unchanged(self): def test_relation_query_bypass_preserved(self): for key in ("parentThreadId", "ancestorThreadId"): with self.subTest(key=key): - self.request["params"] = {key: "mock-parent"} + self.request["params"] = {key: "00000000-0000-4000-8000-0000000000aa"} self.assert_preserved(self.frame()) def test_null_relation_does_not_block(self): From 787ef3069805b5e5b3832e919d75649414cd3a8c Mon Sep 17 00:00:00 2001 From: luvs01 <27862058+luvs01@users.noreply.github.com> Date: Tue, 29 Sep 2026 04:21:23 +0900 Subject: [PATCH 4/9] ci(probes): pin actions to full commit SHAs per repo policy (#6157) --- .github/workflows/devlog-probes.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/devlog-probes.yml b/.github/workflows/devlog-probes.yml index 611454f40a6..29446bae9ec 100644 --- a/.github/workflows/devlog-probes.yml +++ b/.github/workflows/devlog-probes.yml @@ -21,8 +21,8 @@ jobs: unittest: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v6 - - uses: actions/setup-python@v6 + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 + - uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6 with: python-version: "3.x" - name: aiohttp for socket fixtures (best effort) From bb282758b429814d8b22ec00b6b7ef1a5e52f5c2 Mon Sep 17 00:00:00 2001 From: luvs01 <27862058+luvs01@users.noreply.github.com> Date: Tue, 29 Sep 2026 05:52:16 +0900 Subject: [PATCH 5/9] fix(probes): enforce upstream UUID shapes, deterministic aiohttp, consistent verification counts (#6157) --- .github/workflows/devlog-probes.yml | 3 +-- .../020_verification.md | 21 +++++++++------ .../__pycache__/native_policy.cpython-314.pyc | Bin 0 -> 6627 bytes .../remote_list_probe.cpython-314.pyc | Bin 0 -> 11283 bytes .../test_native_policy.cpython-314.pyc | Bin 0 -> 17811 bytes .../__pycache__/test_probe.cpython-314.pyc | Bin 0 -> 18804 bytes .../probes/native_policy.py | 16 ++++++++++++ .../probes/test_native_policy.py | 24 ++++++++++++++++++ 8 files changed, 54 insertions(+), 10 deletions(-) create mode 100644 devlog/_plan/260928_remote_thread_provider_policy/probes/__pycache__/native_policy.cpython-314.pyc create mode 100644 devlog/_plan/260928_remote_thread_provider_policy/probes/__pycache__/remote_list_probe.cpython-314.pyc create mode 100644 devlog/_plan/260928_remote_thread_provider_policy/probes/__pycache__/test_native_policy.cpython-314.pyc create mode 100644 devlog/_plan/260928_remote_thread_provider_policy/probes/__pycache__/test_probe.cpython-314.pyc diff --git a/.github/workflows/devlog-probes.yml b/.github/workflows/devlog-probes.yml index 29446bae9ec..f7e716a3c83 100644 --- a/.github/workflows/devlog-probes.yml +++ b/.github/workflows/devlog-probes.yml @@ -25,9 +25,8 @@ jobs: - uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6 with: python-version: "3.x" - - name: aiohttp for socket fixtures (best effort) + - name: aiohttp for socket fixtures (required; the socket suite imports it unconditionally) run: python -m pip install aiohttp - continue-on-error: true - name: Run every discovered probe suite shell: bash run: | diff --git a/devlog/_plan/260928_remote_thread_provider_policy/020_verification.md b/devlog/_plan/260928_remote_thread_provider_policy/020_verification.md index 28f3dc7bcef..aee77879afe 100644 --- a/devlog/_plan/260928_remote_thread_provider_policy/020_verification.md +++ b/devlog/_plan/260928_remote_thread_provider_policy/020_verification.md @@ -11,7 +11,7 @@ cd devlog/_plan/260928_remote_thread_provider_policy/probes python -m unittest -v test_native_policy test_probe test_loopback_bridge ``` -**Executed: 61 tests, 0 failures.** +**Inventory: 61 tests across three suites.** - 23 tests specify the proposed native policy: opt-out, trusted-origin scoping, explicit arrays, null semantics, parent/ancestor exceptions, immutable policy, @@ -23,11 +23,15 @@ python -m unittest -v test_native_policy test_probe test_loopback_bridge unrelated HTTP, and fixture endpoint restrictions. The 23 + 29 offline tests use the Python standard library only. The 9 socket tests -need aiohttp already installed; no production dependency manifest is changed. +need aiohttp; no production dependency manifest is changed. -Hosted CI runs the same command for every devlog/**/probes directory it finds -(.github/workflows/devlog-probes.yml): the offline unit tests always run, and -the aiohttp socket tests run when aiohttp is present on the runner image. +Author-local execution on 2026-09-28 covered the 52 standard-library tests +(test_native_policy + test_probe); the socket suite was not part of that recorded +run. Hosted exact-head execution is authoritative for the remaining 9: the +devlog-probes workflow added by this PR installs aiohttp deterministically (the +step fails the job if install fails) and discovers every devlog/**/probes +directory. On head 787ef30698 the hosted unittest job ran all 61 tests with 0 +failures. ```sh python -m unittest -v test_native_policy test_probe @@ -49,6 +53,7 @@ schema compatibility or actual mobile results. substitute for required repository gates; the PR must remain draft. - Production multi-segment relay support or management/security review. -The PR adds only this research unit, not a runtime fix. No workflow, executable -configuration, release artifact, or native storage is changed. Required exact-head -CI and independent review remain outstanding even if these probes pass. +The PR adds this research unit plus one workflow that runs it: devlog-probes.yml +is a new CI lane, so the executable contract now executes on exact head rather +than only locally. No executable configuration, release artifact, or native +storage is changed. Independent review remains outstanding even with green CI. diff --git a/devlog/_plan/260928_remote_thread_provider_policy/probes/__pycache__/native_policy.cpython-314.pyc b/devlog/_plan/260928_remote_thread_provider_policy/probes/__pycache__/native_policy.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a552271f3153121ba4b3334eab183f94c7fee2a9 GIT binary patch literal 6627 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 literal 0 HcmV?d00001 diff --git a/devlog/_plan/260928_remote_thread_provider_policy/probes/__pycache__/test_probe.cpython-314.pyc b/devlog/_plan/260928_remote_thread_provider_policy/probes/__pycache__/test_probe.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..7e005de22f909ecb804f12239d0cd0edb1a2d67c GIT binary patch literal 18804 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 literal 0 HcmV?d00001 diff --git a/devlog/_plan/260928_remote_thread_provider_policy/probes/native_policy.py b/devlog/_plan/260928_remote_thread_provider_policy/probes/native_policy.py index 8f2aa1066f7..80859ac8395 100644 --- a/devlog/_plan/260928_remote_thread_provider_policy/probes/native_policy.py +++ b/devlog/_plan/260928_remote_thread_provider_policy/probes/native_policy.py @@ -10,6 +10,20 @@ from dataclasses import dataclass from enum import Enum from uuid import UUID +import re + +# Rust's Uuid::parse_str accepts exactly four textual shapes - hyphenated, simple, +# braced-hyphenated and the urn:uuid prefix. Python's UUID() is looser: it tolerates +# arbitrary hyphen placement inside a 32-hex payload, so wrapper/hyphen variants +# must be rejected by shape before parsing instead of leaning on the constructor. +_UUID_ACCEPTED_SHAPES = re.compile( + r"(?:" + r"[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}" + r"|[0-9a-fA-F]{32}" + r"|\{[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}\}" + r"|urn:uuid:[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}" + r")\Z" +) class ConnectionOrigin(Enum): @@ -30,6 +44,8 @@ def _validate_ids(value: tuple[str, ...], label: str) -> None: def _validate_thread_id(value: str, label: str) -> None: """Upstream parses ThreadId as a UUID; a non-UUID string must not widen the list.""" + if not isinstance(value, str) or not _UUID_ACCEPTED_SHAPES.match(value): + raise ValueError(f"{label} must be a UUID thread id") try: UUID(value) except (ValueError, AttributeError, TypeError): diff --git a/devlog/_plan/260928_remote_thread_provider_policy/probes/test_native_policy.py b/devlog/_plan/260928_remote_thread_provider_policy/probes/test_native_policy.py index eba438a51e4..58d07be97f2 100644 --- a/devlog/_plan/260928_remote_thread_provider_policy/probes/test_native_policy.py +++ b/devlog/_plan/260928_remote_thread_provider_policy/probes/test_native_policy.py @@ -63,6 +63,30 @@ def test_related_thread_ids_follow_native_validation(self): ): with self.subTest(relation=relation), self.assertRaises(ValueError): self.resolve(**relation) + # Python's UUID() tolerates hyphen shapes Rust Uuid::parse_str rejects: + # shifted hyphens, a bare 32-hex blob inside braces, an uppercased URN, and + # a braced simple string all stay invalid no matter the provider shape. + malformed = ( + "000-00000-0000-4000-8000-0000000000aa", + "{000000000000400080000000000000aa}", + "URN:UUID:00000000-0000-4000-8000-0000000000aa", + "{000000000000-4000-8000-0000000000aa}", + ) + for key in ("parent_thread_id", "ancestor_thread_id"): + for bad in malformed: + for requested in (None, ("openai",)): + with self.subTest(key=key, bad=bad, requested=requested), self.assertRaises(ValueError): + self.resolve(**{key: bad}, requested=requested) + # The four shapes upstream accepts stay valid for both relations. + for key in ("parent_thread_id", "ancestor_thread_id"): + for good in ( + "00000000-0000-4000-8000-0000000000aa", + "000000000000400080000000000000aa", + "{00000000-0000-4000-8000-0000000000aa}", + "urn:uuid:00000000-0000-4000-8000-0000000000aa", + ): + with self.subTest(key=key, good=good): + self.assertIsNone(self.resolve(**{key: good})) with self.assertRaises(ValueError): self.resolve( parent_thread_id="00000000-0000-4000-8000-0000000000aa", From 96148ab724a02f3b1e3010345ead91f92f1f2035 Mon Sep 17 00:00:00 2001 From: luvs01 <27862058+luvs01@users.noreply.github.com> Date: Tue, 29 Sep 2026 05:53:21 +0900 Subject: [PATCH 6/9] chore(probes): drop accidentally committed __pycache__ (#6157) --- .../__pycache__/native_policy.cpython-314.pyc | Bin 6627 -> 0 bytes .../remote_list_probe.cpython-314.pyc | Bin 11283 -> 0 bytes .../test_native_policy.cpython-314.pyc | Bin 17811 -> 0 bytes .../__pycache__/test_probe.cpython-314.pyc | Bin 18804 -> 0 bytes 4 files changed, 0 insertions(+), 0 deletions(-) delete mode 100644 devlog/_plan/260928_remote_thread_provider_policy/probes/__pycache__/native_policy.cpython-314.pyc delete mode 100644 devlog/_plan/260928_remote_thread_provider_policy/probes/__pycache__/remote_list_probe.cpython-314.pyc delete mode 100644 devlog/_plan/260928_remote_thread_provider_policy/probes/__pycache__/test_native_policy.cpython-314.pyc delete mode 100644 devlog/_plan/260928_remote_thread_provider_policy/probes/__pycache__/test_probe.cpython-314.pyc diff --git a/devlog/_plan/260928_remote_thread_provider_policy/probes/__pycache__/native_policy.cpython-314.pyc b/devlog/_plan/260928_remote_thread_provider_policy/probes/__pycache__/native_policy.cpython-314.pyc deleted file mode 100644 index a552271f3153121ba4b3334eab183f94c7fee2a9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6627 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 diff --git a/devlog/_plan/260928_remote_thread_provider_policy/probes/__pycache__/test_probe.cpython-314.pyc b/devlog/_plan/260928_remote_thread_provider_policy/probes/__pycache__/test_probe.cpython-314.pyc deleted file mode 100644 index 7e005de22f909ecb804f12239d0cd0edb1a2d67c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 18804 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 From 858add4fa7aeabb1a50c6f65ea3f6a72a9cb8674 Mon Sep 17 00:00:00 2001 From: luvs01 <27862058+luvs01@users.noreply.github.com> Date: Tue, 29 Sep 2026 07:05:21 +0900 Subject: [PATCH 7/9] ci(devlog): persist no checkout credentials; probe bridge forwards close codes (lidge-jun#6157) The devlog-probes workflow runs pull-request Python fixtures, so the checkout must not retain the GITHUB_TOKEN git credential. The loopback bridge's pump now forwards the peer's close code to its destination instead of leaving the other side hanging on an already-ended socket. --- .github/workflows/devlog-probes.yml | 2 ++ .../probes/test_loopback_bridge.py | 5 +++++ 2 files changed, 7 insertions(+) diff --git a/.github/workflows/devlog-probes.yml b/.github/workflows/devlog-probes.yml index f7e716a3c83..cc852a89817 100644 --- a/.github/workflows/devlog-probes.yml +++ b/.github/workflows/devlog-probes.yml @@ -22,6 +22,8 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 + with: + persist-credentials: false - uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6 with: python-version: "3.x" diff --git a/devlog/_plan/260928_remote_thread_provider_policy/probes/test_loopback_bridge.py b/devlog/_plan/260928_remote_thread_provider_policy/probes/test_loopback_bridge.py index 1d2c840c095..dcdc19ac8fb 100644 --- a/devlog/_plan/260928_remote_thread_provider_policy/probes/test_loopback_bridge.py +++ b/devlog/_plan/260928_remote_thread_provider_policy/probes/test_loopback_bridge.py @@ -80,6 +80,11 @@ async def pump(source, destination, transform: bool): await destination.pong(message.data) else: break + # Forward the peer's close so the other side sees the same code + # instead of hanging on a connection that already ended. + close_code = getattr(source, "close_code", None) or 1000 + if not destination.closed: + await destination.close(code=close_code) tasks = [asyncio.create_task(pump(upstream, downstream, True)), asyncio.create_task(pump(downstream, upstream, False))] From f43e66b395b82b05620bc1bdbf8eb332af16b116 Mon Sep 17 00:00:00 2001 From: luvs01 <27862058+luvs01@users.noreply.github.com> Date: Tue, 29 Sep 2026 07:29:18 +0900 Subject: [PATCH 8/9] test(devlog): cover bidirectional close-code forwarding in the bridge fixture (lidge-jun#6157) The pump close-forwarding path had no coverage: host-initiated closes now assert the backend sees 1001, and backend-initiated closes assert the host sees 1011. --- .../probes/test_loopback_bridge.py | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/devlog/_plan/260928_remote_thread_provider_policy/probes/test_loopback_bridge.py b/devlog/_plan/260928_remote_thread_provider_policy/probes/test_loopback_bridge.py index dcdc19ac8fb..2438a4334a6 100644 --- a/devlog/_plan/260928_remote_thread_provider_policy/probes/test_loopback_bridge.py +++ b/devlog/_plan/260928_remote_thread_provider_policy/probes/test_loopback_bridge.py @@ -116,6 +116,9 @@ async def asyncSetUp(self): self.received = asyncio.Queue() self.handshake_headers = {} self.http_received = [] + self.backend_closed = asyncio.Event() + self.backend_close_code = None + self.backend_close_on_connect = None self.backend_app = web.Application() self.backend_app.router.add_route("*", "/backend-api/{tail:.*}", self.mock_backend) self.backend_runner, self.backend_base = await start_loopback_app(self.backend_app) @@ -141,11 +144,16 @@ async def mock_backend(self, request: web.Request): await websocket.prepare(request) for frame in self.frames: await websocket.send_str(frame) + if self.backend_close_on_connect is not None: + await websocket.close(code=self.backend_close_on_connect) + return websocket async for message in websocket: if message.type == aiohttp.WSMsgType.TEXT: await self.received.put(message.data) elif message.type == aiohttp.WSMsgType.PING: await websocket.pong(message.data) + self.backend_close_code = websocket.close_code + self.backend_closed.set() return websocket body = await request.read() self.http_received.append((request.path, body, selected_headers(request.headers))) @@ -204,6 +212,24 @@ async def test_single_chunk_transport(self): self.assertEqual(incoming["seq_id"], 7) self.assertEqual(incoming["cursor"], "mock-backend-cursor") + async def test_host_close_code_reaches_backend(self): + async with self.host.ws_connect(self.relay_base + WS_PATH, + headers={"Authorization": MOCK_AUTH}) as host: + await host.close(code=1001) + await asyncio.wait_for(self.backend_closed.wait(), 5) + self.assertEqual(self.backend_close_code, 1001) + + async def test_backend_close_code_reaches_host(self): + self.backend_close_on_connect = 1011 + async with self.host.ws_connect(self.relay_base + WS_PATH, + headers={"Authorization": MOCK_AUTH}) as host: + async for message in host: + if message.type in (aiohttp.WSMsgType.CLOSE, + aiohttp.WSMsgType.CLOSED, + aiohttp.WSMsgType.CLOSING): + break + self.assertEqual(host.close_code, 1011) + async def test_enrollment_http_forwarding_with_fake_credentials(self): body = b'{"name":"mock-host","installation_id":"mock-installation"}' async with self.host.post(self.relay_base + WS_PATH + "/enroll", data=body, From bd317d08eb76c40ebd6bd4f9b21b43c37177c6b7 Mon Sep 17 00:00:00 2001 From: Epinephrine Date: Wed, 30 Sep 2026 07:31:15 -0700 Subject: [PATCH 9/9] docs: distinguish PR head metadata from merge-ref checkout evidence --- .../020_verification.md | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/devlog/_plan/260928_remote_thread_provider_policy/020_verification.md b/devlog/_plan/260928_remote_thread_provider_policy/020_verification.md index 4edbd47f8e0..0facb61573a 100644 --- a/devlog/_plan/260928_remote_thread_provider_policy/020_verification.md +++ b/devlog/_plan/260928_remote_thread_provider_policy/020_verification.md @@ -35,11 +35,13 @@ run. The then-current socket inventory was 9 tests. The devlog-probes workflow added by this PR installs aiohttp deterministically (the step fails the job if install fails) and discovers every devlog/**/probes directory. The retained record for head `787ef30698` reports 61 hosted tests. -The later close-code regressions raised the count to 63: exact-head CI run -`36492693336`, job `109164783438`, executed all 63 successfully at `f43e66b3`. -The current local execution above independently ran all three suites. New-head -hosted evidence must be checked separately; old hosted runs do not attest to a -new integration commit. +The later close-code regressions raised the count to 63. PR CI run `36492693336`, +job `109164783438`, is associated with head `f43e66b3`, but its checkout log records +the PR merge ref at `7b2ec3931dcd92f777a642c464d80990e7f38008`; that checked-out +merge ref passed all 63 tests. Run metadata's head SHA is not the checkout SHA. +The current local execution above independently ran all three suites. Each new +PR head needs its own associated hosted results; old runs do not attest to a new +integration commit. The workflow retains its normal merge-ref checkout behavior. ```sh python -m unittest -v test_native_policy test_probe @@ -58,18 +60,19 @@ availability limitation no longer describes this environment. Typecheck, structure, privacy and file-size checks are recorded with the current PR checkpoint. The full Bun suite was not rerun for this research-only integration: the complete 63-case probe set is the focused behavioral scope, and wider -repository coverage remains for exact-head CI. No test budget was relaxed. +repository coverage remains for CI associated with the current PR head, using +the normal PR merge ref. No test budget was relaxed. ## Not executed / not established - Native Rust implementation, compilation, config/schema generation, or native tests. - Actual ChatGPT mobile pairing, full pagination, resume, token renewal, or reconnect. -- The full local OpenCodex Bun suite, and new exact-head hosted checks until that +- The full local OpenCodex Bun suite, and new current-head-associated hosted checks until that run completes. The earlier 52-test local run and 61-test hosted record remain historical evidence only. - Production multi-segment relay support or management/security review. The PR adds this research unit plus one workflow that runs it: devlog-probes.yml -is a new CI lane, so the executable contract now executes on exact head rather +is a new CI lane, so the executable contract now executes on the PR merge ref rather than only locally. No executable configuration, release artifact, or native storage is changed. Independent review remains outstanding even with green CI.