Skip to content

feat(security): Provenance + CapabilityAware + WriteFileTool migration (#480 PR 2/3)#482

Merged
xinhuagu merged 7 commits into
mainfrom
feat/480-provenance-and-tool-migration
May 3, 2026
Merged

feat(security): Provenance + CapabilityAware + WriteFileTool migration (#480 PR 2/3)#482
xinhuagu merged 7 commits into
mainfrom
feat/480-provenance-and-tool-migration

Conversation

@xinhuagu

@xinhuagu xinhuagu commented May 3, 2026

Copy link
Copy Markdown
Owner

Summary

Second of three PRs for #480 (runtime governance Layer 1, foundation for #465).

  • New CapabilityAware interface — opt-in companion to Tool. A tool implements it to advertise structured Capability variants instead of the flat (toolName, level) shape.
  • New Provenance + ProvenanceLink records — the "how did we get here" chain (root prompt, plan step, sub-agent depth, retry history). Optional fields are empty in PR 2; PR 3 wires them through the agent loop.
  • PermissionManager.check(...) now has three entry points sharing one decision/audit pipeline:
    • check(PermissionRequest, String) — legacy shim, unchanged callers see no behavior change.
    • check(Capability, Provenance) — convenience overload for daemon-internal callers that have no originating tool name.
    • check(Capability, Provenance, String allowlistKey, String description) — dispatcher form. Caller passes the originating tool's name as the allowlist key + the rich human description, so existing "always allow X" approvals survive migration and the user prompt stays as detailed as before.
  • StreamingAgentHandler dispatcher uses the 4-arg form when the tool is CapabilityAware; falls back to the legacy PermissionRequest path on bad args (logged) or for non-migrated tools.
  • WriteFileTool is the first tool migrated. Its toCapability(...) checks Files.exists to pick WriteMode.CREATE_NEW vs OVERWRITE, so a create-only policy can refuse overwrites without re-stat'ing the filesystem.

Migration safety

  • Existing per-session "always allow write_file" approvals continue to auto-approve after the tool migrates — pinned by PermissionManagerCapabilityTest.sessionBlanketApprovalSurvivesToolMigrationViaDispatcherKey.
  • Sub-agent path (hasAnySessionApproval(toolName)) keeps working because the dispatcher passes the tool name as the allowlist key.
  • User prompts for migrated tools keep the same rich description that buildToolDescription(...) produces — pinned by fourArgPathPassesCallerSuppliedDescriptionToPolicy.

Test plan

  • :aceclaw-security:test — new PermissionManagerCapabilityTest, ProvenanceTest, ProvenanceLinkTest
  • :aceclaw-tools:test — new WriteFileToolCapabilityTest (CREATE_NEW vs OVERWRITE pair, blank-args rejection)
  • :aceclaw-daemon:test — existing dispatcher integration tests still pass

Out of scope (PR 3)

  • Threading real PromptId / PlanStepId through the agent loop (Provenance optional fields stay empty in PR 2).
  • Audit log v2 with structured Capability / Provenance payloads.
  • Migrating ReadFileTool, EditFileTool, BashExecTool, GlobSearchTool, GrepSearchTool — done piecemeal in follow-ups.

Summary by CodeRabbit

  • New Features

    • Structured capability declarations for tools and a capability-aware routing path.
    • Provenance and provenance-link events for richer audit/context (sessions, plan steps, retries).
  • Refactor

    • Permission pipeline migrated to use structured capability checks while preserving legacy fallbacks.
    • HTTP capability data-flow standardized to bidirectional for all methods.
    • File-write tool moved to capability-based permission reporting.
  • Tests

    • Extensive tests added covering capability routing, provenance, and tool-to-capability translations.

#480 PR 2/3)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@qodo-code-review

Copy link
Copy Markdown
ⓘ You've reached your Qodo monthly free-tier limit. Reviews pause until next month — upgrade your plan to continue now, or link your paid account if you already have one.

@greptile-apps greptile-apps Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Your free trial has ended. If you'd like to continue receiving code reviews, you can add a payment method here.

@coderabbitai

coderabbitai Bot commented May 3, 2026

Copy link
Copy Markdown

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: cb07d1df-ed20-48e8-924d-752a5721b93e

📥 Commits

Reviewing files that changed from the base of the PR and between 8121a39 and 94ee5b5.

📒 Files selected for processing (10)
  • aceclaw-daemon/src/main/java/dev/aceclaw/daemon/StreamingAgentHandler.java
  • aceclaw-daemon/src/main/java/dev/aceclaw/daemon/ToolPermissionRouter.java
  • aceclaw-daemon/src/test/java/dev/aceclaw/daemon/ToolPermissionRouterTest.java
  • aceclaw-security/src/main/java/dev/aceclaw/security/Capability.java
  • aceclaw-security/src/main/java/dev/aceclaw/security/PermissionManager.java
  • aceclaw-security/src/main/java/dev/aceclaw/security/Provenance.java
  • aceclaw-security/src/test/java/dev/aceclaw/security/CapabilityTest.java
  • aceclaw-security/src/test/java/dev/aceclaw/security/PermissionManagerCapabilityTest.java
  • aceclaw-security/src/test/java/dev/aceclaw/security/ProvenanceTest.java
  • aceclaw-tools/src/main/java/dev/aceclaw/tools/WriteFileTool.java
🚧 Files skipped from review as they are similar to previous changes (3)
  • aceclaw-security/src/main/java/dev/aceclaw/security/Provenance.java
  • aceclaw-security/src/test/java/dev/aceclaw/security/PermissionManagerCapabilityTest.java
  • aceclaw-security/src/test/java/dev/aceclaw/security/ProvenanceTest.java

📝 Walkthrough

Walkthrough

Structured permission routing added: tools can implement CapabilityAware to convert JSON args into Capability; PermissionManager gains capability-based check overloads and Provenance types; ToolPermissionRouter centralizes routing with legacy fallbacks; WriteFileTool and daemon handler updated; tests added for capabilities, provenance, routing, and write-file conversion.

Changes

Structured Permission & Provenance System

Layer / File(s) Summary
Data Shape / Types
aceclaw-security/src/main/java/dev/aceclaw/security/Provenance.java, .../ProvenanceLink.java, .../Capability.java
Adds Provenance record with factories (forSession, daemonInternal, fromNullableSessionId) and validations; adds ProvenanceLink sealed interface with PlanStepEntered, SubAgentSpawned, RetryAttempt; adds Capability.allowlistKey() default and LegacyToolUse override; HttpFetch.dataFlow() now returns DataFlow.BOTH.
Tool Contract
aceclaw-security/src/main/java/dev/aceclaw/security/CapabilityAware.java
New public interface CapabilityAware with Capability toCapability(JsonNode args) contract for tools to produce structured capabilities from JSON input.
Core Permission Logic
aceclaw-security/src/main/java/dev/aceclaw/security/PermissionManager.java
Adds check(Capability, Provenance) and check(Capability, Provenance, String allowlistKey, String description) overloads. Legacy check(PermissionRequest, sessionId) becomes a shim that wraps into Capability.LegacyToolUse and delegates. Per-session blanket-approval lookup keyed by allowlistKey; audit logging rewritten to record (sessionId, allowlistKey, risk).
Routing / Wiring
aceclaw-daemon/src/main/java/dev/aceclaw/daemon/ToolPermissionRouter.java, aceclaw-daemon/.../StreamingAgentHandler.java
Adds ToolPermissionRouter.check(...) to route between structured CapabilityAware path and legacy PermissionRequest path; StreamingAgentHandler updated to call router instead of constructing legacy request directly. Fallback to legacy path on conversion exceptions; null capability from toCapability fails fast.
Tool Implementation
aceclaw-tools/src/main/java/dev/aceclaw/tools/WriteFileTool.java
WriteFileTool now implements CapabilityAware and adds toCapability(JsonNode args) that resolves file_path, determines WriteMode (CREATE_NEW vs OVERWRITE), and returns Capability.FileWrite, throwing on indeterminate path existence or invalid args.
Tests
aceclaw-security/src/test/.../PermissionManagerCapabilityTest.java, ProvenanceTest.java, ProvenanceLinkTest.java
aceclaw-daemon/src/test/.../ToolPermissionRouterTest.java, aceclaw-tools/src/test/.../WriteFileToolCapabilityTest.java, aceclaw-security/src/test/.../CapabilityTest.java
New/updated tests: permission manager capability-path parity, session-blanket approval preservation, allowlistKey semantics, provenance factories and validations, provenance-link variant contracts, ToolPermissionRouter branching and fallback behaviors, WriteFileTool.toCapability behavior and validation, and consolidated HttpFetch dataFlow assertions.

Sequence Diagram

sequenceDiagram
    autonumber
    participant Agent as StreamingAgentHandler
    participant Router as ToolPermissionRouter
    participant Tool as WriteFileTool
    participant PM as PermissionManager
    participant Policy as PermissionPolicy

    Agent->>Router: check(tool, inputJson, sessionId, desc, level, pm, mapper)
    Router->>Tool: (if CapabilityAware) toCapability(json)
    alt toCapability succeeds
        Tool-->>Router: Capability
        Router->>PM: check(capability, Provenance.fromNullableSessionId(sessionId), allowlistKey, description)
        PM->>PM: sessionAllowlist lookup by allowlistKey
        alt session approval exists
            PM-->>Router: PermissionDecision.Approved
        else
            PM->>Policy: evaluate(PermissionRequest(allowlistKey, description, risk))
            Policy-->>PM: PermissionDecision
            PM-->>Router: PermissionDecision (and audit)
        end
    else toCapability throws
        Router->>PM: checkLegacy(...) (construct PermissionRequest, pass sessionId)
        PM->>Policy: evaluate(...)
        Policy-->>PM: PermissionDecision
        PM-->>Router: PermissionDecision
    end
    Router-->>Agent: PermissionDecision
    Agent->>Tool: proceed or deny based on decision
Loading

Estimated Code Review Effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly Related Issues

Possibly Related PRs

🚥 Pre-merge checks | ✅ 7 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 21.13% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (7 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: introducing Provenance, CapabilityAware interface, and migrating WriteFileTool as part of a multi-PR security governance initiative.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Block Major Correctness And Security Risks ✅ Passed Pull request implements secure capability-aware permission routing with proper null validation, exception handling with fallback to legacy paths, and comprehensive test coverage for all critical paths.
Require Test Coverage For New Logic ✅ Passed All new business logic in src/main has corresponding test coverage in src/test. Eight test files created/updated with comprehensive coverage of ToolPermissionRouter, PermissionManager capability methods, Provenance classes, and WriteFileTool.toCapability().
No Api Breaking Changes Without Version Bump ✅ Passed All pre-existing public methods remain unchanged in signature; only two new overloaded check() methods added.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/480-provenance-and-tool-migration

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ast-grep (0.42.1)
aceclaw-daemon/src/main/java/dev/aceclaw/daemon/StreamingAgentHandler.java

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
Review rate limit: 0/1 reviews remaining, refill in 60 minutes.

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot added the security label May 3, 2026

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: d46d622501

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +3997 to +4001
var capability = capAware.toCapability(objectMapper.readTree(finalInputJson));
var provenance = Provenance.legacy(sessionId);
decision = permissionManager.check(
capability, provenance, delegate.name(), toolDescription);
} catch (RuntimeException | java.io.IOException toCapErr) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Limit fallback to capability-conversion failures only

The try block currently wraps both toCapability(...) and permissionManager.check(...), so any RuntimeException from the structured permission path (for example, policy/audit/runtime errors) is treated as a conversion failure and silently retried through the legacy PermissionRequest path. In that scenario, a real failure in the new capability-based pipeline is masked and can be downgraded to a less strict decision flow for CapabilityAware tools instead of surfacing the error.

Useful? React with 👍 / 👎.

Codex review on #482: the try block previously wrapped permissionManager.check too, so a runtime error from the structured policy/audit path was silently retried via the legacy path — masking real bugs and potentially downgrading the decision. Narrow the catch to just args-to-Capability conversion.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@xinhuagu

xinhuagu commented May 3, 2026

Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented May 3, 2026

Copy link
Copy Markdown
✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@xinhuagu

xinhuagu commented May 3, 2026

Copy link
Copy Markdown
Owner Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented May 3, 2026

Copy link
Copy Markdown
✅ Actions performed

Full review triggered.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@aceclaw-daemon/src/main/java/dev/aceclaw/daemon/StreamingAgentHandler.java`:
- Around line 3996-4021: The code currently treats a null return from
CapabilityAware.toCapability(...) the same as a parse/exception failure and
falls back to the legacy PermissionRequest path; to fix this, fail fast when
toCapability returns null (since it’s documented non-null) instead of
downgrading to legacy. Concretely, inside the try block where you call
capAware.toCapability(objectMapper.readTree(finalInputJson)) check the returned
capability and, if it is null, throw an IllegalStateException (including
delegate.name() and any context) so the error surfaces; keep the existing catch
for RuntimeException/IOException so real parse/convert exceptions still fall
back to the legacy permission path. Ensure you reference
CapabilityAware.toCapability, the local capability variable, and
permissionManager.check/PermissionRequest code paths when making the change.

In `@aceclaw-security/src/main/java/dev/aceclaw/security/PermissionManager.java`:
- Around line 105-106: The two-arg overload public PermissionDecision
check(Capability capability, Provenance provenance) dereferences capability
before validation; add the same null guards as the four-arg path by calling
Objects.requireNonNull(capability, "capability") and
Objects.requireNonNull(provenance, "provenance") (or equivalent null checks used
elsewhere) before invoking capability.allowlistKey() and
capability.displayLabel(), then forward to the existing check(capability,
provenance, ...) call; this ensures consistent NPE messages and adheres to the
guideline about using Objects.requireNonNull for parameters used in downstream
calls or equals().

In
`@aceclaw-security/src/test/java/dev/aceclaw/security/PermissionManagerCapabilityTest.java`:
- Around line 120-133: The test currently uses APPROVE which masks whether
Provenance.daemonInternal() bypasses session allowlist; change the
PermissionManager construction from APPROVE to DENY (use the DENY policy
constant) so the global policy would deny by default, keep the
pm.approveForSession("sess-X", "FileRead") call to simulate a session approval,
call pm.check(new Capability.FileRead(Path.of("/etc/hosts")),
Provenance.daemonInternal()) as before, and update the assertion to expect
PermissionDecision.Denied (e.g.,
assertThat(decision).isInstanceOf(PermissionDecision.Denied.class)) to prove
daemonInternal() skips the session allowlist.

In `@aceclaw-tools/src/main/java/dev/aceclaw/tools/WriteFileTool.java`:
- Around line 163-170: The toCapability method uses Files.exists(filePath) to
decide WriteMode, which treats indeterminate existence (e.g., permission errors)
as CREATE_NEW; update to check three states instead: compute filePath via
resolveFilePath(args.get("file_path").asText()), then if Files.exists(filePath)
use WriteMode.OVERWRITE, else if Files.notExists(filePath) use
WriteMode.CREATE_NEW, otherwise throw an IllegalArgumentException (or similar)
to reject indeterminate cases before constructing new
Capability.FileWrite(filePath, mode).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: c7ad6d57-dd6b-4353-83c8-7565db52ecb5

📥 Commits

Reviewing files that changed from the base of the PR and between 68e8346 and 8121a39.

📒 Files selected for processing (11)
  • aceclaw-daemon/src/main/java/dev/aceclaw/daemon/StreamingAgentHandler.java
  • aceclaw-security/src/main/java/dev/aceclaw/security/Capability.java
  • aceclaw-security/src/main/java/dev/aceclaw/security/CapabilityAware.java
  • aceclaw-security/src/main/java/dev/aceclaw/security/PermissionManager.java
  • aceclaw-security/src/main/java/dev/aceclaw/security/Provenance.java
  • aceclaw-security/src/main/java/dev/aceclaw/security/ProvenanceLink.java
  • aceclaw-security/src/test/java/dev/aceclaw/security/PermissionManagerCapabilityTest.java
  • aceclaw-security/src/test/java/dev/aceclaw/security/ProvenanceLinkTest.java
  • aceclaw-security/src/test/java/dev/aceclaw/security/ProvenanceTest.java
  • aceclaw-tools/src/main/java/dev/aceclaw/tools/WriteFileTool.java
  • aceclaw-tools/src/test/java/dev/aceclaw/tools/WriteFileToolCapabilityTest.java

Comment thread aceclaw-daemon/src/main/java/dev/aceclaw/daemon/StreamingAgentHandler.java Outdated
Comment thread aceclaw-tools/src/main/java/dev/aceclaw/tools/WriteFileTool.java
xinhuagu and others added 4 commits May 3, 2026 18:26
- StreamingAgentHandler: fail fast when toCapability() returns null (contract violation), only fall back to legacy on actual exception.

- PermissionManager.check(Capability, Provenance): add Objects.requireNonNull guards matching the 4-arg path.

- WriteFileTool.toCapability: reject indeterminate filesystem state (Files.exists==Files.notExists==false) instead of silently classifying as CREATE_NEW.

- PermissionManagerCapabilityTest.daemonInternalProvenanceSkipsAllowlistLookup: switch policy to DENY so the assertion actually proves the bypass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…bypass)

Codex P1 from PR #481 review (deferred at the time): GET/HEAD requests still ship URL query strings, headers, and cookies — those are real exfiltration vectors. Classifying them as INGRESS-only let a 'block-egress' policy be bypassed by encoding payload in a GET querystring. Single answer (BOTH) for every method removes the whole class of bypass; finer-grained per-payload inspection belongs in PolicyEngine (#465 Scope #2).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…onRouter

Provenance.legacy → fromNullableSessionId. The previous name implied legacy-only callers, but the dispatcher uses it too (PR 3 will fill in the chain). The new name describes what callers actually have — a possibly-null raw String id — without diluting the migration grep signal.

ToolPermissionRouter extracts the structured-vs-legacy permission branching from StreamingAgentHandler so the contract is testable without standing up the agent loop. New ToolPermissionRouterTest pins all six branches: structured path, legacy path, toCapability throw → fallback, toCapability null → fail-fast, policy errors propagate, null sessionId works.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replace the conversionThrew flag + double-null-check with an early-return from the catch block plus a small checkLegacy helper. Same behavior, but each path now has a single return and the previously unreachable (conversionThrew && capability != null) branch disappears.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 9737bd9b33

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

PermissionManager permissionManager,
ObjectMapper mapper) {
Objects.requireNonNull(delegate, "delegate");
Objects.requireNonNull(inputJson, "inputJson");

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Allow null tool args in permission routing

ToolPermissionRouter.check now hard-fails with Objects.requireNonNull(inputJson), but the surrounding execution path intentionally tolerates missing/invalid tool args (e.g., PermissionAwareTool.execute catches parse errors and continues, and ContentBlock.ToolUse can carry nullable input). With this guard, any tool call that arrives without an arguments payload throws an NPE before permission evaluation or legacy fallback, turning a previously recoverable flow into an unhandled failure. Please treat null args as an empty object (or route directly to legacy) instead of rejecting at the router boundary.

Useful? React with 👍 / 👎.

Codex P2 on #482: ContentBlock.ToolUse and PermissionAwareTool's parse path both legitimately deliver null/malformed args — the previous dispatcher absorbed those via the try-catch around mapper.readTree. My refactor added Objects.requireNonNull(inputJson) at the router boundary, which converted that recoverable flow into an NPE crash. Drop the guard, document the contract, add a test that null inputJson falls back to legacy without reaching toCapability.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant