Skip to content

Apply firewall redaction on the handle-expansion path #150

@dgenio

Description

@dgenio

Summary

Route HandleStore.expand() output through the same redaction the Firewall applies on
first invocation, so expanded rows cannot return inline PII/secrets that the initial
Frame would have stripped.

Why this matters

expand() enforces grant constraints (max_rows, allowed_fields, scope, principal
binding) but builds its Frame directly from the raw stored dataset and never calls
redact(). A field permitted by allowed_fields can still contain inline secrets
(e.g., a note field with a Bearer token) that the Firewall would normally scrub on
the summary/table path. Expansion is a fully supported workflow (README quickstart),
so this is a reachable leak, not a corner case.

Current evidence

  • handles.py: expand() slices/filters/projects rows and returns a Frame(...) with no redaction call.
  • Raw data is stored pre-firewall: kernel/_invoke.py calls kernel._handles.store(... data=raw_result.data ...) before Firewall.transform.
  • firewall/redaction.py scrubs inline email/phone/card/SSN/Bearer/JWT/API-key/connection-string patterns — none of this runs in expand().
  • docs/security.md "Handle expansion boundary" documents constraint rechecks but not redaction.

External context

Not required for this issue.

Proposed implementation

  1. After field projection and pagination in expand(), pass the resulting rows through
    redact() using the handle's persisted allowed_fields and the firewall's
    max_depth.
  2. Prefer giving HandleStore a reference to a Firewall/redaction callable rather
    than importing redaction directly, to keep one redaction implementation.
  3. Carry redaction warnings into the expansion Frame.warnings.

AI-agent execution notes

  • Inspect first: handles.py, firewall/redaction.py, firewall/transform.py, kernel/__init__.py (expand), tests/test_handles.py, tests/test_firewall_boundary.py.
  • Preserve the existing constraint-recheck and principal-binding logic exactly.
  • Decide injection point so HandleStore does not gain a hard dependency cycle with the firewall package.

Acceptance criteria

  • An expanded Frame never contains an inline secret/PII string that the first-invocation Frame redacted.
  • Redaction warnings appear in the expansion Frame.
  • All existing handle constraint tests still pass.

Test plan

Extend tests/test_firewall_boundary.py to expand a handle whose allowed field holds a
fake secret and assert it is redacted. Run make ci.

Documentation plan

Add a redaction note to the "Handle expansion boundary" section of docs/security.md.

Migration and compatibility notes

Expanded values containing sensitive patterns will now be redacted. Document under
Security. No API change.

Risks and tradeoffs

Slight per-expand CPU cost; acceptable for a security boundary. Wiring a firewall
reference into HandleStore must avoid import cycles.

Suggested labels

security, reliability, testing

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions