fix(tools): fail closed when a bare session id resolves into another profile - #106773
liuhao1024 wants to merge 2 commits into
Conversation
…profile session_search's read shape auto-read the first profile whose state.db contained a missed session id — including when an explicit profile= read missed — silently returning another profile's full transcript. Keep the cross-profile locate as a discovery pointer, but require the caller to name the owning profile before its transcript is served.
Related: #106768 and #106772 are competing fixes for the same #106761 hole filed within the same hour. #106768 removes the cross-profile scan outright; #106772 gates it behind a new config key; this PR keeps the locate as a pointer and fails closed naming the owner (still an ownership oracle for a bare id). Flagging the cluster so a maintainer can pick one mechanism. |
|
Thanks for flagging the cluster — agreed the mechanism choice is the maintainer's call, and #87779 / #61947 are useful context (the config opt-out there is a partial mitigation, so a default-safe fix still seems worth landing). For the record on this PR's trade-off: relative to the current fail-open behavior (a bare id silently returns another profile's full transcript), failing closed while naming the owner is a strict tightening. The residual oracle only discloses that an already-possessed high-entropy session id exists and which profile owns it — the attacker must hold the id itself to learn anything. Keeping the locate as a pointer rather than removing the scan outright (as in #106768) also preserves the recovery path when an agent drops the profile prefix mid-task: the next hop can re-issue with the explicit profile instead of dead-ending. Happy to rework or withdraw if the maintainer prefers one of the other mechanisms. |
|
suggesting changes The patch prevents a bare ID from returning a foreign transcript, but its fail-closed error still reveals the owning profile. Remove that owner oracle before merge; bounded tests and named-profile controls pass.
Security evidence:
Not checked:
Signed: GPT-5.6-luna-max in Codex |
… oracle Per review: the fail-closed error still named the owning profile, which is a cross-profile metadata oracle for a bare id. Remove the locate scan outright; a bare id now returns the same generic not-found error whether the id is unknown or owned by another profile. Naming the profile (or an embedded @session:<profile>/<id> link) remains the authorized read path.
|
Pushed |
|
Prior-art / overlap on tools/session_search_tool.py bare session_id profile isolation (live heads):
Ask maintainers: which approach is canonical? The other should close as prior-art/duplicate rather than land both. No competing PR from me. |
|
Thanks for the overlap summary — happy for maintainers to pick the canonical one. For the record on the current head of this PR (
No objection to either landing; deferring to maintainer preference. |
Duplicate of #106768: after the review-driven push ( |
|
Acknowledged. As stated when the cluster was first flagged, I'm happy either way — maintainers pick the canonical branch and the other closes. One factual note on the duplicate framing: the mechanism convergence is the direct outcome of the P2 owner-oracle review on this branch (drop the locate → generic miss on a bare-id miss), not a re-implementation of #106768's approach. On the current state of both heads: this branch has its review feedback fully addressed ( |
Re-triaged: relabeled from duplicate to related. #106768 and this PR are open competing fixes for #106761 that converged on the same mechanism via independent review; the maintainer picks the canonical branch. Cross-linked: #106768, #106772 (closed), #106761, #87779. |
|
Closing in favor of #108074 (merged; fixes #106761). Its |
What does this PR do?
session_search's read shape (session_idalone) fails open across profiles: when the id misses in the caller's ownstate.db,_read_with_profile_fallbackunconditionally scans every profile'sstate.dbvia_locate_session_dband returns the first hit's full transcript. No opt-in, no authorization check — an agent (or user) in profile A can read profile B's entire conversation given only a session id, and session ids are not secrets (logs, tool output, the databases themselves). The same hole fires even for an explicitprofile=read that misses, silently serving a third profile's transcript instead of an error.This PR keeps the cross-profile locate as a discovery pointer but fails closed on the read: on a miss, the tool now reports which profile owns the id and asks the caller to name it (
re-run with profile='<owner>'). The transcript is only served when the caller explicitly passes that profile. This preserves the original safety net's recovery path (a model that dropped the profile prefix from an@session:<profile>/<id>link gets the owner named in the error and succeeds on the next call), while closing the leak.Related Issue
Fixes #106761
Type of Change
Changes Made
tools/session_search_tool.py—_read_with_profile_fallbackno longer auto-reads the located profile's db on a miss; it closes it and returns a fail-closed error naming the owning profile. Explicitprofile=reads, link-prefix autosplit, and same-profile reads are untouched.tests/tools/test_session_search.py—test_bare_id_locates_across_profilesnow asserts the fail-closed contract: a bare id that lives in another profile returnssuccess: falsewithprofile='asdf'in the error and nomessages; the named re-run (profile='asdf') still reads the session (authorized path, unchanged).How to Test
default+asdf), each with its ownstate.db;asdfowns sessions_far.default, callsession_search(session_id="s_far").{"success": true, "profile": "asdf", "messages": [...full transcript of asdf...]}.{"success": false, "error": "session_id 's_far' lives in profile 'asdf'. Reading another profile's session requires naming the profile: re-run with profile='asdf'."}— nomessageskey.session_search(session_id="s_far", profile="asdf"):{"success": true, "mode": "read", ...}— the authorized explicit-profile path is unchanged.pytest tests/tools/test_session_search.py -q→ 53 passed;pytest tests/agent/test_anthropic_mcp_prefix_strip.py tests/run_agent/test_token_persistence_non_cli.py -q→ 18 passed;ruff checkclean.Checklist
Code
fix(scope):,feat(scope):, etc.)pytest tests/ -qand all tests passDocumentation & Housekeeping
docs/, docstrings) — or N/A (docstring updated in place)cli-config.yaml.exampleif I added/changed config keys — N/ACONTRIBUTING.mdorAGENTS.mdif I changed architecture or workflows — N/AScreenshots / Logs
N/A — see How to Test for the before/after tool output.