Skip to content

feat: surface installed hook actions during apm install (closes #316)#1700

Merged
danielmeppiel merged 7 commits into
mainfrom
feat/hook-install-transparency-316-rebased
Jun 9, 2026
Merged

feat: surface installed hook actions during apm install (closes #316)#1700
danielmeppiel merged 7 commits into
mainfrom
feat/hook-install-transparency-316-rebased

Conversation

@danielmeppiel

Copy link
Copy Markdown
Collaborator

Summary

This PR supersedes #409 by @harshitlarl (rebased to resolve conflicts with current main).

Authorship preserved: harshitlarl's original commit (8783a21) is in the git history as author.

What this PR does

  • Emit per-event hook action summaries during apm install for integrated hooks
  • In verbose mode, print the fully rewritten hook JSON that is deployed/merged
  • Rewrite tests to assert display_payloads == on-disk content (security invariant)

Security contract

_build_display_payload takes the post-path-rewrite 'rewritten' dict so
display_payloads faithfully reflects what is written to disk and executed.

Closes #316
Co-authored-by: harshitlarl zipdemonharshits012@gmail.com

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR adds install-time transparency for integrated hook primitives, surfacing per-event hook action summaries during apm install and (in verbose mode) emitting the rewritten hook JSON content that will be deployed/merged. It also introduces unit tests that lock in the security contract that the displayed hook payloads reflect the post-path-rewrite (on-disk / executed) content.

Changes:

  • Add hook display metadata generation in HookIntegrator (flatten hook entries + summarize commands + capture rewritten JSON).
  • Thread display_payloads through IntegrationResult and install output aggregation to emit per-hook-file action summaries (and verbose JSON detail).
  • Add unit tests to ensure display_payloads.rendered_json matches the rewritten content written to disk / merged into config.
Show a summary per file
File Description
tests/unit/test_install_hook_transparency.py Adds unit tests for hook entry flattening, command summarization, and the “display matches rewritten/on-disk content” security invariant.
src/apm_cli/integration/hook_integrator.py Builds per-hook-file display payloads from rewritten hook data and returns them via integration results.
src/apm_cli/integration/base_integrator.py Extends IntegrationResult with a display_payloads field to carry hook transparency metadata.
src/apm_cli/install/services.py Aggregates and emits hook transparency output during install, including verbose-mode rewritten JSON printing.

Copilot's findings

  • Files reviewed: 4/4 changed files
  • Comments generated: 1

Comment on lines +425 to +437
command = ""
for key in ("command", "bash", "powershell"):
value = entry.get(key)
if isinstance(value, str) and value.strip():
command = value.strip()
break
if not command:
return "runs hook command"
for token in command.split():
cleaned = token.strip("\"'")
if "/" in cleaned or cleaned.startswith("."):
return f"runs {cleaned}"
return f"runs {command}"
@danielmeppiel

Copy link
Copy Markdown
Collaborator Author

Shepherd-driver completion advisory

PR #1700 supersedes #409 (@harshitlarl). Authorship preserved in git history.

Convergence summary

Rebase (origin/main rebase of feat/hook-install-transparency-316):

  • Conflicts in src/apm_cli/commands/install.py: HEAD taken (function refactored to services.py)
  • Conflicts in src/apm_cli/integration/hook_integrator.py: both intents merged
  • Pre-rebase HEAD: cd3d547 | Post-rebase: 84cf586 | Final: 1b595b4

Architecture adaptation (main refactored install.py -> services.py + dispatch table):

Security invariant satisfied: display_payloads uses the 'rewritten' dict
(output of _rewrite_hooks_data), which is the same dict serialized to disk.
Tests assert rendered_json == on-disk content.

Folded items

  1. [panel] Defensive .get() on action dict keys in _log_hook_display_payloads (resolved in 1b595b4)
  2. [rebase] Conflict resolution: install.py + hook_integrator.py (resolved in 8783a21)
  3. [rebase] Test rewrite to match new services.py API (resolved in 84cf586)
  4. [rebase] Complexity refactor: extracted _log_hook_display_payloads to keep C901 clean (84cf586)

Deferred items

  1. [panel] _summarize_command URL heuristic edge case (curl https://...) -- scope boundary: edge case improvement to summarization heuristic, not part of the hook transparency contract, open a follow-up if needed

CI evidence

All checks green on final commit (1b595b4):

Lint evidence

ruff check + format: silent | pylint R0801: 10.00/10 | auth-signals: clean

Tests

12 new hook transparency tests pass (including security assertion: display_payloads == on-disk content)
161 existing hook integrator tests pass

Copilot review

Copilot left 1 review summary on #409 (no inline comments accessible). Classified: reviewed and no actionable inline items.

Mergeability

mergeable: MERGEABLE | mergeStateStatus: BLOCKED (awaiting review approval -- branch protection)
head_sha: 1b595b4

@danielmeppiel danielmeppiel added the status/accepted Direction approved, safe to start work. label Jun 8, 2026
@danielmeppiel danielmeppiel self-assigned this Jun 8, 2026
@danielmeppiel danielmeppiel added the panel-review Trigger the apm-review-panel gh-aw workflow label Jun 9, 2026
harshitlarl and others added 7 commits June 9, 2026 16:37
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…yloads

- Rebase feat/hook-install-transparency-316 onto current main
- Adapt hook transparency from old monolithic install.py to refactored
  services.py + dispatch table architecture
- Add display_payloads field to IntegrationResult (base_integrator.py)
  so all integrator results carry the field; HookIntegrator populates it
- Add _iter_hook_entries, _summarize_command, _build_display_payload to
  HookIntegrator; _build_display_payload takes the post-path-rewrite
  'rewritten' dict so display_payloads faithfully reflects what is
  actually written to disk and executed (security requirement from #316)
- Extract _log_hook_display_payloads helper in services.py to keep
  integrate_package_primitives below C901 complexity threshold
- Rewrite test suite to assert display_payloads == on-disk content
  (the security-critical invariant the panel prior raised)

Co-authored-by: harshitlarl <zipdemonharshits012@gmail.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Fold panel advisory: _log_hook_display_payloads used bare dict key
access (_act['event'], _act['summary']) which would throw KeyError on
a malformed payload. Switch to .get() with fallback '?' to align with
the defensive style used on adjacent lines.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The hook transparency summary built display payloads from the
pre-finalization per-file `rewritten` dict, which diverged from what
is actually written and executed: for Claude (schema-strict) the
rendered JSON carried `_apm_source` keys stripped before the disk
write, and for Gemini it showed the pre-transform Copilot schema
instead of the bash->command / timeoutSec->timeout(ms) rewrite that
lands on disk. Additionally `_iter_hook_entries` and
`_summarize_command` only inspected 3 of the 6 HOOK_COMMAND_KEYS, so
hooks using `windows`/`linux`/`osx` keys deployed with zero
transparency output -- the precise failure the feature was built to
prevent. `_summarize_command` could also emit multi-line summaries
when a command contained a newline, breaking log formatting and
enabling log-spoofing.

Defer display-payload construction in the merged-target path until the
JSON config is finalized (Gemini transform applied, schema-strict
`_apm_source` stripped) and build from the same entry objects written
to disk; iterate the full HOOK_COMMAND_KEYS tuple; and collapse
internal whitespace so summaries are always single-line.

addresses CEO follow-ups (Claude/Gemini rendered_json divergence,
3-of-6 HOOK_COMMAND_KEYS blindspot) and Copilot inline on
src/apm_cli/integration/hook_integrator.py:437

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add regression traps for the four folded fixes: OS-specific hook keys
(windows/linux/osx) each surface an action; command summaries collapse
embedded newlines/whitespace to a single line; Claude rendered_json
omits the `_apm_source` key stripped from disk; and Gemini
rendered_json reflects the bash->command / timeoutSec->timeout(ms)
transform actually written to settings.json. Also cover the
user-visible `_log_hook_display_payloads` install logger, which had no
direct test. Each trap was proven by the mutation-break gate: with the
production guards reverted, all four divergence tests fail.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Record the install-time hook action transparency feature under
Unreleased > Added, crediting the original community author.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@danielmeppiel danielmeppiel force-pushed the feat/hook-install-transparency-316-rebased branch from 387386b to 20f56df Compare June 9, 2026 14:38
@danielmeppiel

Copy link
Copy Markdown
Collaborator Author

apm-review-panel advisory (shepherd-driver terminal pass)

The panel's three blocking findings and the test gap were all folded into this PR; CI is green on the rebased head. Recommending the maintainer take this through required review.

Folded in this run

  • (panel / supply-chain-security) Claude rendered_json divergence -- _apm_source markers were present in the displayed payload but stripped before the .claude/settings.json disk write. Display payloads are now built AFTER the schema-strict _apm_source strip, from the same entry refs written to disk -- resolved in d87c923c.
  • (panel / python-architecture) Gemini display_payloads divergence -- display was built from pre-_to_gemini_hook_entries data while disk held the renamed (bash->command), rewrapped, ms-converted schema. Display is now built post-transform from the finalized json_config -- resolved in d87c923c.
  • (panel x4 / supply-chain-security) _iter_hook_entries + _summarize_command only inspected 3 of 6 HOOK_COMMAND_KEYS -- hooks keyed on windows/linux/osx deployed but produced zero transparency output. Both now iterate the full HookIntegrator.HOOK_COMMAND_KEYS tuple -- resolved in d87c923c.
  • (Copilot inline) _summarize_command could emit multi-line summaries (log-spoofing surface); now collapses whitespace via " ".join(command.split()) -- resolved in d87c923c.
  • (panel / test-coverage) _log_hook_display_payloads (the headline user-visible function) had outcome: missing coverage; direct test added -- resolved in 11c40a6c.

Copilot signals reviewed

  • src/apm_cli/integration/hook_integrator.py (_summarize_command) -- LEGIT: multi-line summary log-spoofing; folded (resolved in d87c923c).

Regression-trap evidence (mutation-break gate)

  • OS-specific-keys test -- reverted HOOK_COMMAND_KEYS iteration to the 3-key tuple; test FAILED as expected; guard restored.
  • single-line-summary test -- removed " ".join(...split()); test FAILED; restored.
  • Claude-omits-_apm_source test -- moved display build before the strip; test FAILED; restored.
  • Gemini-transform-reflected test -- built display from pre-transform data; test FAILED; restored.
  • _log_hook_display_payloads test -- broke the action-key .get() path; test FAILED; restored.

Lint contract

uv run --extra dev ruff check src/ tests/ and uv run --extra dev ruff format --check src/ tests/ both silent.

CI

All required checks green on 20f56dfc: Lint, Build & Test Shard 1/2 (Linux), PR Binary Smoke, Analyze (python/actions), CodeQL, gate, Spec conformance gate, NOTICE Drift Check, APM Self-Check, Coverage Combine. 0 CI fix iterations.

Conflict resolution

Rebased onto current main (387386b3 -> 20f56dfc). One conflicting path resolved as a faithful union of both intents:

Pushed with --force-with-lease; @harshitlarl's authorship preserved on the rebased commits.

Mergeability status

PR head SHA iters folds defers Copilot rounds CI mergeable mergeStateStatus
#1700 20f56dfc 1 5 0 1 green MERGEABLE BLOCKED (awaiting required review)

Convergence

1 outer iteration; 1 Copilot round. BLOCKED reflects the pending maintainer review gate only -- no merge conflict, no failing check. Ready for maintainer review.

@danielmeppiel danielmeppiel merged commit d1ad46b into main Jun 9, 2026
13 checks passed
@danielmeppiel danielmeppiel deleted the feat/hook-install-transparency-316-rebased branch June 9, 2026 14:46
sergio-sisternes-epam pushed a commit that referenced this pull request Jun 9, 2026
…arency)

Sync main commits #1700, #1676, #1694, #1710. The #1700 feature (surface
installed hook actions during install) wove display-payload tracking
through code my refactor extracted into sibling helpers; resolve two
conflicts by keeping the extracted structure and porting #1700 semantics:

- services.py: accept the extracted _log_per_kind_results() call; move
  _log_hook_display_payloads into services_integrate.py (avoids circular
  import) and re-export it from services.py; emit hook summaries inside
  _log_per_kind_results.
- hook_integrator._integrate_merged_hooks: accept extracted
  _merge_hook_file_entries / _write_merged_config; thread per-file
  display data out via a new optional capture_entries kwarg on
  _merge_hook_file_entries; build display payloads after
  _write_merged_config finalizes (post _apm_source strip).
- Keep hook_integrator.py <=800 by relocating _iter_hook_entries /
  _summarize_command / _build_display_payload to hook_transforms.py and
  _parse_hook_json to hook_merge.py as thin delegators.

Shadow gate green: ruff, ruff format, pylint R0801 10.00/10 (EXIT=0),
auth-signals, import smoke, 3178 tests pass. All files within 800-line
guard (hook_integrator.py 799, hook_merge.py 766, hook_transforms.py 601,
services.py 690, services_integrate.py 307).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
sergio-sisternes-epam pushed a commit that referenced this pull request Jun 9, 2026
…e integrity)

Sync with main through v0.19.0 (#1715, #1714, #1718, #1719, #1717).

Conflicts resolved:
- CHANGELOG.md: main cut the v0.19.0 release, moving prior Unreleased
  entries into a dated section. Took main's restructure; relocated the
  #1681 "Tightened Stage 2 code-complexity thresholds" entry to the new
  ## [Unreleased] section (released sections stay immutable per Keep a
  Changelog). Took main's #1702/#1710 close-ref for the lockfile entry.
- src/apm_cli/install/services.py: #1718 added per-file skill-bundle
  integrity recording inline at a site my refactor had extracted into
  _log_skill_result. Kept the new _skill_bundle_file_entries helper in
  services.py (sibling of _deployed_path_entry); dropped main's duplicate
  inline _log_hook_display_payloads (already relocated to
  services_integrate.py and re-exported in my #1700 port); accepted the
  _log_skill_result delegator at the call site.
- src/apm_cli/install/services_integrate.py: ported #1718's
  deployed.extend(_skill_bundle_file_entries(tp, ...)) into
  _log_skill_result's deployed-path loop (deployed aliases
  result["deployed_files"]), with a lazy import of the helper.

Shadow gate: no markers, all src <=800 lines, ruff check + format clean,
pylint R0801 EXIT=0, auth-signals clean; 11144 tests pass across the
install/integration suites plus the #1718, #1700, and #1709 targeted sets.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

panel-review Trigger the apm-review-panel gh-aw workflow status/accepted Direction approved, safe to start work.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Hook installation transparency — display hook contents during install

3 participants