Files
Teknium c5270d2fbb fix(webapp): every stored owner is read by one rule, so old Webapp chats open and adopt again
Review of #145598 (comment 6448254020):

- Chats a Webapp tab imported before the upgrade open again: session_history_mount._reader read the
  import binding by string equality while the sidebar (authorize_history) used the owner rule, so
  the sidebar listed the chat and the open refused it. Both now ask binding_names().
- An old chat a Webapp tab edited is adopted again: bind_legacy_target compared the binding exactly.
- gateway.owner_identity.same_owner is now the one owner rule (owns_row_stamp folded into it), and
  every comparison of a stored owner/binding/principal with a caller in gateway/ goes through it.
- Local stamps match this machine's account only.
- hosted_auth_probe: restore the refused groups.create over another tab's room id and the
  'FORBIDDEN' never reaching the model, adapted to the single-owner contract.
2026-10-27 17:32:49 -07:00

39 lines
2.0 KiB
Python

"""Private provenance for already-authorized operator input.
Operator access is profile-local, not account linking or a change of authorship.
It never claims unowned legacy history: that still requires the native migration
path. Only SessionAuthority.submit writes this envelope after authorization;
public submission parameters cannot supply it.
"""
from gateway.owner_identity import same_owner
from hermes_state_runtime import RuntimeStoreError
# Fields ``SessionAuthority.submit`` commits for a local session's input; producers commit their own envelope.
LOCAL_INPUT_FIELDS = frozenset({'text', 'attachments_v1', 'finite', 'unattended', 'surface_v1', 'display_v1',
'cli_turn_v1', 'kanban_card_v1'})
def is_viewer_input(row):
"""A row a session viewer submitted through ``session.submit`` (a client's own prompt), not a producer
admission: ``local_automation_v1`` (plugin injection, delegation completions, process and watch notices,
/loop, heartbeat), a hosted-room task (``hosted:`` request id) or any other envelope."""
return (not str(row['request_id']).startswith('hosted:')
and {'text'} <= set(row['payload']) <= LOCAL_INPUT_FIELDS | {'local_operator_v1'})
def check_local_input(authority, ref, row, *, source=None):
payload = row['payload']
allowed = set(LOCAL_INPUT_FIELDS)
if 'local_operator_v1' in payload:
allowed.add('local_operator_v1')
if payload['local_operator_v1'] != {
'profile_id': authority.profile_id, 'session_id': ref.session_id,
'principal_id': row['principal_id']}:
raise RuntimeStoreError('permission_denied')
else:
source = source or authority.sessions[ref.session_id].source
if not same_owner(source.user_id, row['principal_id']): # the rule submit() stamped by
raise RuntimeStoreError('permission_denied')
if not {'text'} <= set(payload) <= allowed:
raise RuntimeStoreError('permission_denied')