Skip to content

feat(agent): audit writes made from inside a smart action - #426

Merged
bexchauveto merged 1 commit into
mainfrom
feat/audit-trail-internal-hook
Nov 4, 2026
Merged

bexchauveto merged 1 commit into
mainfrom
feat/audit-trail-internal-hook

Conversation

@bexchauveto

@bexchauveto bexchauveto commented Nov 4, 2026 •

Copy link
Copy Markdown
Member

Why

Raised in Slack (Audit Trail v2, Swan demo): a create done inside a smart action via context.datasource.get_collection(...).create is never audited as a create row on the Ruby agent — only the action invocation is.

Root cause: AuditTrail::Capture registered its hooks with the public add_hook, i.e. on the hook layer, which sits above action in the decorators stack. Writes issued from an action's execute go down from the action layer and never cross it.

agent-nodejs already handles this with an internalHook layer below action (decorators-stack.ts, addInternalHook). This PR ports that.

What

  • DecoratorsStack: new @internal_hook layer (a HookCollectionDecorator) placed between chart and action.
  • CollectionCustomizer#add_internal_hook: same signature as add_hook, registers on that layer.
  • AuditTrail::Capture: registers its Before/After Create/Update/Delete hooks with add_internal_hook.
  • AUDIT_TRAIL.md: documents that writes from inside an action are audited under the action's correlation key.

Behavioral changes to verify manually

  • More create/update/delete rows: writes done inside smart actions through Forest are now audited, under the same correlationKey as the action row.
  • Writes emitted by the write decorator (e.g. a create with nested relation data that the write layer splits) now traverse the internal layer too, so they produce one audit row per underlying collection write. Same as Node.
  • Hook ordering: audit hooks now run below every public hook, so the audit "before" hooks see data already patched by user add_hook handlers, and a user after-hook raising can no longer drop the audit confirmation.
  • Renamed fields (unchanged caveat, same as Node): Capture still reads columns from the top-of-stack schema, while the internal layer sees pre-rename names. A collection with a rename_field customization could record those columns as nil. Out of scope here.

Tests

  • collection_customizer_spec: an action creating through context.datasource fires add_internal_hook and not add_hook; a direct create fires both. Verified the first one fails when add_internal_hook is pointed at the public layer.
  • Full suites: datasource_customizer 760 examples, agent 1427 examples, 0 failures. Rubocop clean.

🤖 Generated with Claude Code

Note

Add internal hook layer so writes inside smart actions are audited

  • Adds an internal HookCollectionDecorator layer in DecoratorsStack#initialize, placed below the smart action decorator, so writes made through Forest inside a smart action traverse it (decorators_stack.rb)
  • Adds CollectionCustomizer#add_internal_hook, which mirrors add_hook but registers on the internal layer (collection_customizer.rb)
  • Switches create, update, and delete audit handlers in Capture to register via the internal layer, so action-triggered writes are recorded under the action's correlationKey in the audit trail (capture.rb)
  • Documents the behavior in AUDIT_TRAIL.md: writes that bypass Forest are only represented by the smart-action row
  • Behavioral Change: public hooks (add_hook) no longer fire for writes initiated inside a smart action; only internal hooks run for those, and tests in collection_customizer_spec.rb assert this split

Macroscope summarized de9440a.

The audit trail registered its hooks with the public add_hook, which sits
above the action layer: a create/update/delete made from an action's
execute through context.datasource never reached them, so only the action
row was recorded. Add an internal hook layer below `action` (parity with
agent-nodejs) and register the audit hooks there.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@qltysh

qltysh Bot commented Nov 4, 2026

Copy link
Copy Markdown

Qlty


Coverage Impact

This PR will not change total coverage.

Modified Files with Diff Coverage (3)

RatingFile% DiffUncovered Line #s
Coverage rating: A Coverage rating: A
...est_admin_datasource_customizer/decorators/decorators_stack.rb100.0%
Coverage rating: A Coverage rating: A
...ib/forest_admin_datasource_customizer/collection_customizer.rb100.0%
Coverage rating: A Coverage rating: A
...rest_admin_agent/lib/forest_admin_agent/audit_trail/capture.rb100.0%
Total100.0%
🚦 See full report on Qlty Cloud »

🛟 Help
  • Diff Coverage: Coverage for added or modified lines of code (excludes deleted files). Learn more.

  • Total Coverage: Coverage for the whole repository, calculated as the sum of all File Coverage. Learn more.

  • File Coverage: Covered Lines divided by Covered Lines plus Missed Lines. (Excludes non-executable lines including blank lines and comments.)

    • Indirect Changes: Changes to File Coverage for files that were not modified in this PR. Learn more.

@bexchauveto
bexchauveto marked this pull request as ready for review November 4, 2026 09:43
@matthv

matthv commented Nov 4, 2026

Copy link
Copy Markdown
Member

Forest Validator · Review: ✅ clean

When (UTC) Commit Step Result
04/11 10:40 de9440a Review ✅ clean · run 426-20261104103741-matthv

@matthv matthv left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Approved after /validator run 426-20261104103741-matthv: nothing blocking posted, 0 preferential or code style, 0 suppressed below all.

@bexchauveto
bexchauveto merged commit 13c26c7 into main Nov 4, 2026
33 checks passed
@bexchauveto
bexchauveto deleted the feat/audit-trail-internal-hook branch November 4, 2026 13:36
forest-bot added a commit that referenced this pull request Nov 4, 2026
# [1.48.0](v1.47.0...v1.48.0) (2026-11-04)

### Features

* **agent:** audit writes made from inside a smart action ([#426](#426)) ([13c26c7](13c26c7))
@forest-bot

Copy link
Copy Markdown
Member

🎉 This PR is included in version 1.48.0 🎉

The release is available on GitHub release

Your semantic-release bot 📦🚀

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.

3 participants