fix(#5307): close 2 more subagent-child writable-session holes (Codex round 2)
- api/routes.py GET /api/session synthesized response: stop hardcoding is_cli_session=True; serialize bool(synth.is_cli_session) so a recovered subagent child stays not-CLI-classified (was overriding the helper's False). - api/routes.py POST /api/session/import_cli: gate source='subagent' into the read-only view payload (is_cli_session=False, imported=False) BEFORE import_cli_session(), so a subagent child can never be materialized as a writable WebUI sidecar via this endpoint. - tests: assert import_cli routes subagent children read-only (no materialize). Both were paths that bypassed the _is_claimable_cli_source denylist. Fixes #5307
This commit is contained in:
parent
a823ab5ff8
commit
3ecb2625aa
2 changed files with 45 additions and 3 deletions
|
|
@ -11320,7 +11320,13 @@ def handle_get(handler, parsed) -> bool:
|
|||
"archived": bool(getattr(synth, "archived", False)),
|
||||
"project_id": getattr(synth, "project_id", None),
|
||||
"profile": synth.profile,
|
||||
"is_cli_session": True,
|
||||
# Read is_cli_session from the synthesized Session, not a
|
||||
# hardcoded True: delegated subagent children (#5307) are
|
||||
# recovered read-only with is_cli_session=False so they don't
|
||||
# pass the frontend _isExternalSession poll-skip / active-refresh
|
||||
# gates (#3603). Every other synthesized foreign session keeps
|
||||
# is_cli_session=True so its source badge renders.
|
||||
"is_cli_session": bool(getattr(synth, "is_cli_session", False)),
|
||||
"source_tag": synth.source_tag,
|
||||
"raw_source": synth.raw_source,
|
||||
"session_source": synth.session_source,
|
||||
|
|
@ -22313,6 +22319,14 @@ def _handle_session_import_cli(handler, body):
|
|||
cli_platform = cli_meta.get("platform") if cli_meta else None
|
||||
cli_parent_session_id = cli_meta.get("parent_session_id") if cli_meta else None
|
||||
cli_read_only = bool((cli_meta or {}).get("read_only"))
|
||||
# Delegated subagent children (#5307) are recovered VIEW-ONLY: they must
|
||||
# never be materialized as a writable WebUI sidecar via this endpoint, or a
|
||||
# subsequent chat-start/composer write would take ownership of a session
|
||||
# that belongs to the delegate runner. Treat them like an explicitly
|
||||
# read-only source (return the read-only stub payload, do not import), and
|
||||
# keep them out of the _isExternalSession frontend gates (is_cli_session=False).
|
||||
_sa_child = _is_subagent_child_session_id(sid)
|
||||
_read_only_view = cli_read_only or _sa_child
|
||||
|
||||
# Use the CLI session title if available (e.g., cron job name), otherwise derive from messages
|
||||
title = cli_title or title_from(msgs, "CLI Session")
|
||||
|
|
@ -22322,7 +22336,7 @@ def _handle_session_import_cli(handler, body):
|
|||
if is_cron_session(sid, cli_source_tag):
|
||||
cron_project_id = ensure_cron_project()
|
||||
|
||||
if cli_read_only:
|
||||
if _read_only_view:
|
||||
session_payload = {
|
||||
"session_id": sid,
|
||||
"title": title,
|
||||
|
|
@ -22336,7 +22350,10 @@ def _handle_session_import_cli(handler, body):
|
|||
"archived": False,
|
||||
"project_id": None,
|
||||
"profile": profile,
|
||||
"is_cli_session": True,
|
||||
# Subagent children (#5307) are recovered view-only and must NOT be
|
||||
# CLI-classified (keeps them out of the frontend _isExternalSession
|
||||
# gates); other explicitly-read-only sources keep is_cli_session=True.
|
||||
"is_cli_session": (False if _sa_child else True),
|
||||
"source_tag": cli_source_tag,
|
||||
"raw_source": cli_raw_source or cli_source_tag,
|
||||
"session_source": cli_session_source,
|
||||
|
|
|
|||
|
|
@ -257,3 +257,28 @@ def test_state_db_source_helper_reads_subagent(routes_module, isolated_state_db)
|
|||
assert routes_module._state_db_session_source("sa-1") == "subagent"
|
||||
assert routes_module._is_subagent_child_session_id("sa-1") is True
|
||||
assert routes_module._is_subagent_child_session_id("does-not-exist") is False
|
||||
|
||||
|
||||
def test_import_cli_endpoint_does_not_materialize_subagent_child():
|
||||
"""Codex hardening: POST /api/session/import_cli must NOT create a writable
|
||||
sidecar for a subagent child — the handler gates source='subagent' into the
|
||||
read-only view payload (is_cli_session=False, imported=False) before ever
|
||||
calling import_cli_session(). Pinned as a static-source contract check."""
|
||||
src = ROUTES_PY.read_text(encoding="utf-8")
|
||||
start = src.index("def _handle_session_import_cli(")
|
||||
m = re.search(r"\n(?:def |class )", src[start + 1:])
|
||||
block = src[start:(start + 1 + m.start()) if m else len(src)]
|
||||
assert "_is_subagent_child_session_id(sid)" in block, (
|
||||
"import_cli handler must detect subagent children"
|
||||
)
|
||||
assert "_read_only_view" in block, (
|
||||
"import_cli must route subagent children through the read-only view "
|
||||
"payload (no writable import_cli_session materialization)"
|
||||
)
|
||||
# the read-only view branch must return before import_cli_session()
|
||||
ro_idx = block.index("_read_only_view")
|
||||
import_idx = block.index("import_cli_session(")
|
||||
assert ro_idx < import_idx, (
|
||||
"the read-only-view gate must precede import_cli_session() so subagent "
|
||||
"children never materialize a writable sidecar"
|
||||
)
|
||||
|
|
|
|||
Loading…
Reference in a new issue