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
- 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.
- Prefer giving
HandleStore a reference to a Firewall/redaction callable rather
than importing redaction directly, to keep one redaction implementation.
- 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
Summary
Route
HandleStore.expand()output through the same redaction the Firewall applies onfirst 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, principalbinding) but builds its
Framedirectly from the raw stored dataset and never callsredact(). A field permitted byallowed_fieldscan still contain inline secrets(e.g., a
notefield with a Bearer token) that the Firewall would normally scrub onthe 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 aFrame(...)with no redaction call.kernel/_invoke.pycallskernel._handles.store(... data=raw_result.data ...)beforeFirewall.transform.firewall/redaction.pyscrubs inline email/phone/card/SSN/Bearer/JWT/API-key/connection-string patterns — none of this runs inexpand().docs/security.md"Handle expansion boundary" documents constraint rechecks but not redaction.External context
Not required for this issue.
Proposed implementation
expand(), pass the resulting rows throughredact()using the handle's persistedallowed_fieldsand the firewall'smax_depth.HandleStorea reference to aFirewall/redaction callable ratherthan importing redaction directly, to keep one redaction implementation.
Frame.warnings.AI-agent execution notes
handles.py,firewall/redaction.py,firewall/transform.py,kernel/__init__.py(expand),tests/test_handles.py,tests/test_firewall_boundary.py.HandleStoredoes not gain a hard dependency cycle with the firewall package.Acceptance criteria
Test plan
Extend
tests/test_firewall_boundary.pyto expand a handle whose allowed field holds afake 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
HandleStoremust avoid import cycles.Suggested labels
security, reliability, testing