fix(gateway): a prepared branch hashes the child's own edit policy

On the prepared fork path _child_secret_ref stamped the durable config-secret
reference's legacy policy hash from the parent's policy (Don't Ask), while the
child is stored at Default. Hash the policy the child is actually stored with.
This commit is contained in:
Teknium committed 2026-10-18 22:58:30 -07:00
1 parent 098a7b71ba
commit 8d389383f6
2 files changed
+29 -1

No files matched your search

+2 -1
View File
@@ -489,7 +489,8 @@ async def prepare_branch_editor(authority, ref, servers, gate):
request_id = 'branch:' + uuid.uuid4().hex
child = local_identity(saved['profile_id'], saved['principal_id'], request_id)
policy, _ = await asyncio.to_thread(rebind_editor_mcp, authority, child, parent, servers)
policy = _child_secret_ref(authority, child, parent, policy)
# Hashed as the child is stored: a branch starts at Default (hermes_state_mutation_branch._child_policy).
policy = _child_secret_ref(authority, child, parent, replace(policy, edit_approval_policy='ask'))
return {**gated, 'request_id': request_id, 'child': child, 'parent_editor_mcp_json': parent.editor_mcp_json,
'editor_mcp_json': policy.editor_mcp_json, 'toolsets': list(policy.toolsets),
'config_secret_ref': policy.config_secret_ref}, servers
+27
View File
@@ -121,6 +121,33 @@ async def test_a_branch_of_a_dont_ask_session_starts_at_ask(tmp_path, monkeypatc
store.close_all_db_handles()
@pytest.mark.asyncio
async def test_a_prepared_branch_stamps_its_secret_reference_with_the_childs_ask_policy(tmp_path, monkeypatch):
"""The prepared fork's durable config-secret reference hashes the policy the child is stored with
(``ask``), never the parent's Don't Ask."""
from dataclasses import asdict
import hermes_state_local
from gateway import session_local_mcp, session_policy_credentials as creds
from gateway.session_policy import build_policy
parent = replace(build_policy({'source': 'acp', 'cwd': str(tmp_path), 'model': 'fixture'}, {}),
edit_approval_policy='session', editor_mcp_json='{"scope": "p"}',
config_secret_ref=creds.PREFIX + '{}')
saved = {'policy': asdict(parent), 'profile_id': 'owned', 'principal_id': 'human'}
monkeypatch.setattr(hermes_state_local, 'local_receipt', lambda db, sid: saved)
monkeypatch.setattr(session_local_mcp, 'rebind_editor_mcp', lambda a, child, policy, servers: (policy, False))
monkeypatch.setattr(creds, 'recover_config_secrets', lambda *a, **k: {})
hashed = []
monkeypatch.setattr(creds, 'config_reference', lambda a, child, policy, secrets: hashed.append(policy) or 'ref')
async def gate():
return {'snapshot': {}}
authority = SimpleNamespace(db=None, _local_editor_mcp={'p': []})
prepared, _ = await session_local_mcp.prepare_branch_editor(
authority, SimpleNamespace(session_id='parent'), [], gate)
assert prepared['config_secret_ref'] == 'ref'
assert [p.edit_approval_policy for p in hashed] == ['ask']
def test_a_policy_stored_before_the_field_restores_as_ask_with_its_identity_unchanged(tmp_path):
"""A receipt main minted has no ``edit_approval_policy``: it restores as ``ask`` and its credential
reference (a whole-policy hash) still matches, so no stored session loses its launch secrets."""