V3 Issue #261 — Secure Typed Tool Invocation Implementation Plan
Issue-specific plan for #261. Complete this work before #233.
Goal: Ensure every tool call is authorized against its exact arguments and cannot exceed the configured, surface, interaction, parent, workspace, or assigned-child authority.
Architecture: Keep policy in openkyrozen/security, enforcement and receipts in openkyrozen/agent, and schemas/executors in the existing ToolRegistry. Introduce one immutable approval request and typed runtime entry point; preserve current string APIs only through compatibility adapters until #233 migrates built-ins.
Tech Stack: Python 3.12–3.13, standard library, unittest, existing Go Bubble Tea TUI.
Spec: Approved V3 batch plan, issue #261 (R179–R182, R184–R186).
Global Constraints
- Add no dependency, paid provider call, or persistence of executable source.
- Capability checks remain in the runtime boundary; registry invocation remains capability-only.
- Approval responses remain separate from Strategic Plan approval and user clarification.
- The active grant is the intersection of all authority bounds and cannot increase during renewal or delegation.
- Assigned children may mutate only explicitly assigned paths through enforceable file tools; opaque shell, Git, browser, and custom mutations are denied.
- Preserve existing text APIs and event fields while adding typed adapters.
Review Focus
- Call arguments or registry policy change while approval is pending — bind the approved snapshot to execution and deny stale operations.
- Nested capability overrides or expired-token renewal — renew only the original bounded grant.
- Large or secret-bearing approval payloads — show complete nonsecret details without leaking secret values or silently truncating.
- Paths that resolve through symlinks or change during approval — resolve and recheck within the active workspace and child scope before execution.
- Read-labelled Git commands that write output or invoke external programs — reject those options before execution.
Review follow-up (2026-10-12)
- [x] Redact bearer credentials embedded in structured string values before display and audit persistence.
- [x] Redact bearer credentials in legacy string TUI approval prompts before chunking.
- [x] Preserve every byte of the approved operation while assembling TUI detail chunks, and prevent approval until the complete correlated stream is received.
- [x] Treat an explicitly empty requested capability set as deny-all.
- [x] Keep optional two-argument approval callbacks on the legacy adapter path.
- [x] Re-run regressions on Python 3.12/3.13 and Go; full Python suite, check, lint and docs checks pass.
Task 1: Add immutable typed approval data
Files: openkyrozen/security/approvals.py, tests/test_tool_approvals.py
Interfaces: ApprovalRequest(request_id, call_id, tool_name, arguments, workspace_id, session_id, tool_version, tool_source, capability, risk, side_effects, fingerprint) is frozen; arguments are a JSON-safe defensive snapshot. fingerprint is SHA-256 over canonical JSON for call identity, tool identity/version, workspace/session, and exact arguments.
- [ ] Test defensive copies, invalid/non-JSON arguments, and digest changes when any bound field changes; run and observe failure.
- [ ] Implement the value type with standard-library JSON/hash functions and no SDK imports.
- [ ] Run
python -m unittest tests.test_tool_approvals -vand confirm the new cases pass.
Task 2: Enforce monotonic capability bounds
Files: openkyrozen/security/tool_policy.py, openkyrozen/security/capabilities.py, openkyrozen/agent/preparation.py, openkyrozen/agent/runtime.py, openkyrozen/agent/executor.py, tests/test_capabilities.py
Interfaces: Add effective_tool_capabilities(*bounds) -> frozenset[str]; intersect configured, surface, interaction-mode, parent, and request grants. AgentRuntime.execute_call(session, call: ToolCall, *, capabilities=None, operation_scope="", approve=None) -> ExecutionReceipt receives a structured call. Keep execute() as a named compatibility adapter.
- [ ] Test surface overrides, expired-token renewal, interaction modes, and child grants never widen parent/configured grants; run red.
- [ ] Implement intersection and bounded renewal; preserve existing default profiles and errors.
- [ ] Test call capability denial before approval or executor invocation; run focused and existing capability suites.
Task 3: Bind approval to the exact typed operation
Files: openkyrozen/security/permissions.py, openkyrozen/security/permission_gate.py, openkyrozen/agent/executor.py, openkyrozen/agent/types.py, tests/test_permission_gate.py, tests/test_interaction.py
Interfaces: Add approve_tool_call(request: ApprovalRequest) -> bool; retain old (action, rendered_args) callbacks through an adapter. Runtime canonicalizes aliases and validates the object before building the request. Bind the selected immutable ToolSpec and fingerprint through authorization; recheck current grant and request binding after approval before invoking it.
- [ ] Test typed callback receives the exact nontruncated operation, and changing arguments/spec/grant while pending prevents execution; run red.
- [ ] Test callback exceptions, non-boolean decisions, timeout, stale request IDs, and denied calls produce no executor effects.
- [ ] Implement one approval path that records redacted summaries and the fingerprint, preserving explicit existing approval modes.
- [ ] Keep the temporary legacy-executor conversion after the gate; reject conversions that do not represent the approved object.
- [ ] Run focused authorization/runtime suites and verify no retry or replay.
Task 4: Enforce child scope and argument-sensitive policy
Files: openkyrozen/agent/delegation_runtime.py, openkyrozen/security/permission_gate.py, tests/test_delegation.py, tests/test_permission_gate.py
- [ ] Test writes outside assigned paths, symlink escapes, post-approval path changes, shell/Git/browser/custom mutations, and mutation with no assigned path; run red.
- [ ] Permit child file writes only when the canonical resolved path is explicitly assigned; recheck immediately before execution.
- [ ] Deny opaque child mutations and preserve read-only review plus cancellation behavior.
- [ ] Test Git diff/log/show output-writing and external-execution options are rejected; run focused delegation and permission suites.
Task 5: Preserve compatibility at runtime entry points
Files: openkyrozen/agent/executor.py, openkyrozen/agent/runtime.py, existing MCP/web/TUI caller modules, tests/test_server.py, tests/test_tui_protocol.py
- [ ] Inventory and test callers using
execute(),_run_tool(),approve(action, args), and TUI approval responses. - [ ] Keep existing string entry points as thin adapters that construct the same canonical approval request; do not change tool result semantics in this issue.
- [ ] Reject approval payloads whose
approvedvalue is not a boolean and ignore stale/unmatched correlation IDs. - [ ] Run MCP, web, CLI, TUI protocol, and compatibility tests.
Task 6: Display complete structured approvals in TUI
Files: openkyrozen/interfaces/tui/turns.py, tui/events.go, tui/dialogs.go, tui/input.go, tests/test_tui_protocol.py, tui/main_test.go
- [ ] Test the approval event carries canonical tool name, policy summary, full structured nonsecret arguments, and existing
action/argsfields; verify sensitive values stay redacted. - [ ] Add a scrollable approval view so long structured arguments can be inspected without silent truncation; preserve approve/deny correlation behavior.
- [ ] Reject non-boolean response values in Python and Go protocol paths.
- [ ] Run Python TUI protocol tests and
cd tui && go test ./....
Task 7: Review, verify, and deliver #261
- [ ] Run focused tests on Python 3.12 and 3.13, then
make test-core,make check,make lint,make docs-check, Go tests, and offline agent/subagent/TUI acceptance. - [ ] Run browser acceptance when Playwright is available; report missing browser binaries as BLOCKED, separately from failures.
- [ ] Review the complete diff independently; reproduce and fix material findings with red-green regressions.
- [ ] Commit with Evan’s GPG key; verify locally, push
Evan/v3-261-security-boundary, create a PR against currentmain, and wait for final-head CI/reviews. Do not merge.