Skip to content

feat(build-server): add client+server interaction audit logging#949

Merged
cuioss-oliver merged 15 commits into
mainfrom
feature/marshalld-interaction-audit-logging
Jul 20, 2026
Merged

feat(build-server): add client+server interaction audit logging#949
cuioss-oliver merged 15 commits into
mainfrom
feature/marshalld-interaction-audit-logging

Conversation

@cuioss-oliver

@cuioss-oliver cuioss-oliver commented Jul 19, 2026

Copy link
Copy Markdown
Collaborator

Summary

Add full interaction audit logging to the marshalld build server, on both sides of the client/server boundary, correlated end-to-end via the existing job_id.

  • Client side (build-server-client): every daemon interaction (submit/enqueue, status/wait query, fallback/refused(reason=...) outcome) is now logged to the plan work log via manage-logging — no interaction is silent, and no fallback/refusal lands only at an uncaptured Python log level.
  • Server side (marshalld.py): a structured, append-only interaction log records every request/response, stamped with project_root + plan_id + job_id + timestamp + outcome, extending the existing job-logs/ directory rather than duplicating it.
  • Correlation: the job_id already written to the change-ledger kind=job entry at submit time ties the client work-log entries, the server's interaction-log records, and the ledger entry together end-to-end.
  • Audit affordance: a manage-build-server read verb inspects recorded interactions (status/logs shape).
  • Retention: bounded retention + GC for the interaction log, parallel to the existing journal's retention model, with no secrets (provider secrets, env, args) ever written to any log.

Changes

  • marketplace/bundles/plan-marshall/skills/build-server-client/scripts/build_server.py — client-side interaction logging for submit/status/wait/fallback/refusal paths
  • marketplace/bundles/plan-marshall/skills/build-server-client/SKILL.md — documents the client-side audit-logging contract
  • marketplace/bundles/plan-marshall/skills/manage-build-server/scripts/marshalld.py — server-side structured interaction log wiring
  • marketplace/bundles/plan-marshall/skills/manage-build-server/scripts/_marshalld_audit.py — new interaction-log module (append-only records, retention/GC)
  • marketplace/bundles/plan-marshall/skills/manage-build-server/scripts/manage_build_server.py — new read verb to inspect recorded interactions
  • marketplace/bundles/plan-marshall/skills/manage-build-server/SKILL.md — documents the audit read verb and log structure
  • test/plan-marshall/build-server/test_build_server_client.py — client-side logging assertions
  • test/plan-marshall/build-server/test_interaction_audit_correlation.py — new correlation-id test across client/server/ledger
  • test/plan-marshall/build-server/test_manage_build_server.py — read-verb coverage
  • test/plan-marshall/build-server/test_marshalld_audit.py — new interaction-log module tests
  • test/plan-marshall/script-shared/test_build_execute_factory.py, test/plan-marshall/script-shared/test_build_queue_slot.py — adjusted shared test fixtures

Test Plan

  • Verification command passed (python3 .plan/execute-script.py plan-marshall:build-pyproject:pyproject_build run --command-args "verify")
  • Manual testing completed (if applicable)

Related Issues

None

Summary by CodeRabbit

  • New Features
    • Added interaction audit logging for build submit and wait, with accepted, degraded, and refused outcomes.
    • Added a read-only, project-scoped logs command to inspect recent interaction audit records.
    • Correlates the same daemon-assigned job identifier across work logs, audit records, and the job ledger.
  • Security
    • Enforces secrets discipline in audit entries and sanitizes notation to prevent log/header forging.
  • Reliability
    • logs fails closed with explicit empty results when the audit log is missing or unreadable.
    • Adds bounded retention cleanup for audit history.
  • Tests
    • Expanded end-to-end correlation and non-leakage coverage.

@sourcery-ai sourcery-ai 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.

Sorry @cuioss-oliver, you have reached your weekly rate limit of 500000 diff characters.

Please try again later or upgrade to continue using Sourcery

@coderabbitai

coderabbitai Bot commented Jul 19, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@cuioss-oliver, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 38 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Repository: cuioss/coderabbit/.coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 701fd10e-66a5-4dbf-b552-0c8b78c944fc

📥 Commits

Reviewing files that changed from the base of the PR and between 287b13d and 0e4a729.

⛔ Files ignored due to path filters (1)
  • pw.lock is excluded by !**/*.lock
📒 Files selected for processing (16)
  • marketplace/bundles/plan-marshall/skills/build-server-client/SKILL.md
  • marketplace/bundles/plan-marshall/skills/build-server-client/scripts/build_server.py
  • marketplace/bundles/plan-marshall/skills/manage-build-server/SKILL.md
  • marketplace/bundles/plan-marshall/skills/manage-build-server/scripts/_marshalld_audit.py
  • marketplace/bundles/plan-marshall/skills/manage-build-server/scripts/manage_build_server.py
  • marketplace/bundles/plan-marshall/skills/manage-build-server/scripts/marshalld.py
  • marketplace/bundles/plan-marshall/skills/manage-providers/scripts/_list_providers.py
  • marketplace/bundles/pm-plugin-development/skills/plugin-doctor/scripts/_analyze_markdown.py
  • pyproject.toml
  • test/plan-marshall/build-server/test_build_server_client.py
  • test/plan-marshall/build-server/test_interaction_audit_correlation.py
  • test/plan-marshall/build-server/test_manage_build_server.py
  • test/plan-marshall/build-server/test_marshalld_audit.py
  • test/plan-marshall/script-shared/test_build_execute_factory.py
  • test/plan-marshall/script-shared/test_build_queue_slot.py
  • test/plan-marshall/workflow-integration-github/test_re_review_strategy.py
📝 Walkthrough

Walkthrough

The change adds append-only daemon interaction auditing, client work-log entries for submit and wait outcomes, job correlation across logs and the ledger, control-character sanitization, project-scoped audit inspection, retention cleanup, and deterministic test/tooling updates.

Changes

Interaction audit logging

Layer / File(s) Summary
Audit persistence and retention
marketplace/.../manage-build-server/scripts/_marshalld_audit.py, test/.../test_marshalld_audit.py
Adds JSON-lines audit storage with attribution fields, secret-key rejection, crash-safe reads, file permissions, and retention garbage collection.
Daemon request attribution
marketplace/.../manage-build-server/scripts/marshalld.py, test/.../test_marshalld_audit.py
Records one audit entry per handled request, derives project and job attribution, suppresses audit write errors, and runs cleanup during startup.
Client interaction logging and correlation
marketplace/.../build-server-client/..., test/.../build-server/*, test/.../script-shared/*
Adds INFO/WARNING work-log entries for submit and wait, sanitizes notation, correlates job IDs with ledger rows, and validates fallback routing and secret exclusion.
Project-scoped audit inspection
marketplace/.../manage-build-server/..., test/.../test_manage_build_server.py
Adds the read-only logs command with canonical-root filtering, bounded tails, and explicit absent or unreadable-log reasons.

Repository hygiene and deterministic tests

Layer / File(s) Summary
Typing and test-tool configuration
marketplace/.../_list_providers.py, marketplace/.../_analyze_markdown.py, pyproject.toml
Adds local dictionary annotations, constrains development tool versions, and configures a pytest timeout.
Deterministic integration tests
test/.../script-shared/*, test/.../workflow-integration-github/test_re_review_strategy.py
Forces queue tests through in-process routing and freezes re-review trigger timestamps.

Estimated code review effort: 4 (Complex) | ~45 minutes

🚥 Pre-merge checks | ✅ 2
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: adding client and server interaction audit logging for the build server.

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

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

@gemini-code-assist gemini-code-assist 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.

Code Review

This pull request introduces a central, append-only interaction audit logging system for the marshalld build server. It logs every ping, submit, and wait request to a central interaction-audit.log file, ensuring accountability while maintaining strict secrets discipline by excluding sensitive fields like raw command arguments. Additionally, a new read-only logs verb is added to manage_build_server to allow project-scoped inspection of these logs. Comprehensive unit and integration tests have been added to verify the audit logging, log rotation/GC, and security constraints. No review comments were provided, so there is no feedback to address.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

@cuioss-oliver
cuioss-oliver force-pushed the feature/marshalld-interaction-audit-logging branch from fa9bafd to 596e404 Compare July 20, 2026 06:24

@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: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@marketplace/bundles/plan-marshall/skills/build-server-client/scripts/build_server.py`:
- Around line 466-472: Sanitize the job_id value in the run_wait audit logging
flow before interpolating it into the _audit_log message, applying the same
newline/carriage-return control-character stripping already used for notation.
Preserve the existing job-status, elapsed, and eta formatting.

In
`@marketplace/bundles/plan-marshall/skills/manage-build-server/scripts/marshalld.py`:
- Around line 313-326: Move the _audit_attribution, _audit_job_id, and reason
derivation into the existing try block so all audit-related operations are
covered by the best-effort guard. Keep catching OSError and swallowing it,
ensuring failures from attribution resolution or _interaction_audit.record never
abort request handling.
🪄 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: Repository: cuioss/coderabbit/.coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: ef7fa657-14d8-447d-ad83-92fe90ab643f

📥 Commits

Reviewing files that changed from the base of the PR and between ae78d11 and 596e404.

📒 Files selected for processing (12)
  • marketplace/bundles/plan-marshall/skills/build-server-client/SKILL.md
  • marketplace/bundles/plan-marshall/skills/build-server-client/scripts/build_server.py
  • marketplace/bundles/plan-marshall/skills/manage-build-server/SKILL.md
  • marketplace/bundles/plan-marshall/skills/manage-build-server/scripts/_marshalld_audit.py
  • marketplace/bundles/plan-marshall/skills/manage-build-server/scripts/manage_build_server.py
  • marketplace/bundles/plan-marshall/skills/manage-build-server/scripts/marshalld.py
  • test/plan-marshall/build-server/test_build_server_client.py
  • test/plan-marshall/build-server/test_interaction_audit_correlation.py
  • test/plan-marshall/build-server/test_manage_build_server.py
  • test/plan-marshall/build-server/test_marshalld_audit.py
  • test/plan-marshall/script-shared/test_build_execute_factory.py
  • test/plan-marshall/script-shared/test_build_queue_slot.py

Comment thread marketplace/bundles/plan-marshall/skills/manage-build-server/scripts/marshalld.py Outdated
cuioss-oliver added a commit to cuioss/cuioss-organization that referenced this pull request Jul 20, 2026
…rol (#188)

Two changes to `reusable-pyprojectx-verify.yml`, shipped together.

## (A) Structured build-target inputs

`reusable-maven-build.yml` gives callers named, validated levers over
the build's content (java-versions, maven-profiles-*, enable-sonar,
enable-snapshot-deploy). The pyprojectx equivalent had only
`verify-command` — a raw string interpolated straight into a `run:`
step. Changing the goal set meant hand-writing `'./pw quality-gate &&
./pw module-tests'`: unvalidated, not composable, no meaningful per-repo
defaults.

Replaced with:

| Input | Default | Purpose |
|---|---|---|
| `verify-goals` | `verify` | pw goals run in order, each as `./pw
<goal>` |
| `verify-args` | `''` | extra args appended to every goal invocation
(the profile-equivalent knob, e.g. a module filter) |

Goal tokens are validated against `[a-z][a-z0-9-]*` and the run fails
fast with a clear annotation otherwise. Both values are carried via
`env` and never interpolated into the shell body.

**One deviation from the request:** it asked for each goal to be *its
own step* with an attributable step name. GitHub Actions cannot emit a
dynamic number of steps from a runtime string — steps are static YAML.
The goals therefore run in a single step with a `::group::` per goal;
the loop exits on the first failure and emits `::error::pw goal 'X'
failed`, giving the same attribution a step name would. The alternative
(a fixed static step per known goal) was rejected because it freezes the
goal set in the reusable and loses caller-specified ordering.

## (B) Concurrency control

`reusable-pyprojectx-verify.yml` had no `concurrency:` block. Without it
a push and its pull_request event both ran a full verify on the same
commit and neither was cancelled — observed in cuioss/plan-marshall#949
as two concurrent `verify / verify` jobs grinding on one SHA for ~60
minutes.

The added block is semantically identical to `reusable-maven-build.yml`:
grouped by caller workflow + ref + event, `cancel-in-progress` limited
to `pull_request` so default-branch pushes and merge_group (merge-queue)
runs always run to completion.

This is **complementary to, not overlapping with, the gate job**:
- the gate skips a **redundant** run (a push whose commit an open PR
already covers)
- concurrency cancels a **superseded** run (an older commit on the same
PR)

`merge_group` runs are excluded from both — the gate always builds them,
and they are never cancelled. The gate's footprint filter and push-dedup
logic are otherwise untouched.

## Incidental fix

`schema.json` had a pre-existing trailing comma (in `github-automation`)
that made it **invalid JSON**, silently breaking the published
`yaml-language-server: $schema` editor hint for every consumer. Repaired
here since this PR edits that file.

Worth knowing: `schema.json` is never loaded by `read-config.py` or any
test — it is purely an editor hint, so `additionalProperties: false` is
**not** runtime-enforced. A requested "schema rejects unknown keys" test
was therefore not meaningful; instead a test now asserts the schema
stays parseable and keeps documenting the keys the field registry
actually reads. Wiring real `jsonschema` validation into
`read-config.py` was considered and deliberately deferred — it would
make every consumer repo's `project.yml` a CI failure risk.

## Breaking change — consumer action required

`verify-command` is removed outright: no compatibility shim, no
deprecation path. `cuioss/plan-marshall` is the only consumer and **must
bump its SHA pin** (`@<sha> # v<version>`) for this release, and migrate
any `pyprojectx.verify-command` in its `.github/project.yml` to
`verify-goals` / `verify-args`. This release changes the reusable's
input surface, so pinned consumers are unaffected until they bump.

## Verification

- `./pw verify workflow` — 215 passed (mypy + ruff + pytest)
- Workflow YAML parses
- Goal allowlist rejects `verify; rm -rf /`, `$(id)`, `./pw verify`,
`Verify`, `-x`

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **New Features**
* Verification workflows now support running multiple ordered Pyprojectx
goals.
  * Added shared arguments that are appended to each verification goal.
* Verification goals are validated before execution and stop on the
first failure.
* Pull request verification runs now cancel superseded in-progress runs.

* **Documentation**
* Updated configuration references, workflow guidance, schema
documentation, and examples for the new verification options.

* **Tests**
* Added coverage for defaults, multiple goals, argument handling,
ordering, and schema validation.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---

## Review round 1 — two real defects fixed (2nd commit)

**Input precedence was broken (CodeRabbit, critical).** The registry
default for `verify-goals` was `'verify'`, so the action output was
always non-empty and `config.outputs.X || inputs.X` always
short-circuited to the config side — a caller's `verify-goals` input
could never be honored. Both keys now default to `""` (as
`python-version` already does), restoring `project.yml` > input >
workflow-default. Effective behavior for a caller passing nothing is
unchanged.

**GITHUB_OUTPUT forging (Gemini, high).** Values land as `key=value`
lines, so a newline in `project.yml` could emit a second line and
override an unrelated output. Goals now have whitespace collapsed
(injected text becomes a token the workflow's allowlist rejects loudly);
args are dropped wholesale if any token is unsafe, rather than partially
stripped. Shell injection was already blocked by the allowlist + `env`
passing.

⚠️ **Pre-existing, not fixed here:** the same precedence bug affects
`cache-dependency-glob` (registry default `uv.lock`) and
`upload-artifacts-on-failure` (`false`) — caller inputs for those are
silently ignored today. Out of scope for this PR; worth a follow-up.

---------

Co-authored-by: Oliver Wolff <23139298+cuioss@users.noreply.github.com>
Co-authored-by: Claude <noreply@anthropic.com>
@cuioss-oliver
cuioss-oliver force-pushed the feature/marshalld-interaction-audit-logging branch from 596e404 to b44a2b2 Compare July 20, 2026 12:22

@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.

🧹 Nitpick comments (1)
test/plan-marshall/build-server/test_marshalld_audit.py (1)

184-202: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

GC window tests never exercise the real now_utc_iso().

Both timestamps here are produced via monkeypatch.setattr(audit_mod, 'now_utc_iso', lambda: '...'), so this test (and every other test in the file) only proves _parse_iso_epoch/_is_expired work against a hand-crafted string — it never proves the real, unmocked now_utc_iso() from file_ops actually round-trips through _parse_iso_epoch. Given gc()'s correctness silently degrades to "never prunes anything" on a format mismatch (see companion comment on _marshalld_audit.py), consider adding one unmocked test asserting _parse_iso_epoch(now_utc_iso()) is not None.

Suggested additional test
def test_now_utc_iso_round_trips_through_parse_iso_epoch():
    """Guards the (currently convention-only) format contract between
    file_ops.now_utc_iso() and _marshalld_audit._parse_iso_epoch."""
    assert audit_mod._parse_iso_epoch(audit_mod.now_utc_iso()) is not None
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/plan-marshall/build-server/test_marshalld_audit.py` around lines 184 -
202, Add an unmocked test near test_gc_removes_only_records_past_the_window that
calls audit_mod.now_utc_iso() and asserts audit_mod._parse_iso_epoch(...) is not
None, preserving the existing mocked timestamp tests.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@test/plan-marshall/build-server/test_marshalld_audit.py`:
- Around line 184-202: Add an unmocked test near
test_gc_removes_only_records_past_the_window that calls audit_mod.now_utc_iso()
and asserts audit_mod._parse_iso_epoch(...) is not None, preserving the existing
mocked timestamp tests.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository: cuioss/coderabbit/.coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 54ff457c-643c-40f8-873d-90d90bd952a6

📥 Commits

Reviewing files that changed from the base of the PR and between 596e404 and ddfdb1e.

⛔ Files ignored due to path filters (1)
  • pw.lock is excluded by !**/*.lock
📒 Files selected for processing (16)
  • marketplace/bundles/plan-marshall/skills/build-server-client/SKILL.md
  • marketplace/bundles/plan-marshall/skills/build-server-client/scripts/build_server.py
  • marketplace/bundles/plan-marshall/skills/manage-build-server/SKILL.md
  • marketplace/bundles/plan-marshall/skills/manage-build-server/scripts/_marshalld_audit.py
  • marketplace/bundles/plan-marshall/skills/manage-build-server/scripts/manage_build_server.py
  • marketplace/bundles/plan-marshall/skills/manage-build-server/scripts/marshalld.py
  • marketplace/bundles/plan-marshall/skills/manage-providers/scripts/_list_providers.py
  • marketplace/bundles/pm-plugin-development/skills/plugin-doctor/scripts/_analyze_markdown.py
  • pyproject.toml
  • test/plan-marshall/build-server/test_build_server_client.py
  • test/plan-marshall/build-server/test_interaction_audit_correlation.py
  • test/plan-marshall/build-server/test_manage_build_server.py
  • test/plan-marshall/build-server/test_marshalld_audit.py
  • test/plan-marshall/script-shared/test_build_execute_factory.py
  • test/plan-marshall/script-shared/test_build_queue_slot.py
  • test/plan-marshall/workflow-integration-github/test_re_review_strategy.py
🚧 Files skipped from review as they are similar to previous changes (6)
  • test/plan-marshall/script-shared/test_build_execute_factory.py
  • marketplace/bundles/plan-marshall/skills/build-server-client/SKILL.md
  • marketplace/bundles/plan-marshall/skills/manage-build-server/scripts/manage_build_server.py
  • test/plan-marshall/build-server/test_manage_build_server.py
  • marketplace/bundles/plan-marshall/skills/build-server-client/scripts/build_server.py
  • marketplace/bundles/plan-marshall/skills/manage-build-server/scripts/marshalld.py

cuioss-oliver added a commit that referenced this pull request Jul 20, 2026
…best-effort attribution, gc format guard

Three valid findings from the PR #949 review, all against this plan's own diff:

- CWE-117 (Major): `run_wait` interpolated the client-supplied `--job-id` into the
  line-oriented work log unsanitized, so a `\n`/`\r` in job_id could forge a fake
  header line — the same vector already closed for the executor notation. Extract a
  single `_sanitize_for_log` seam (notation reuses it) and apply it to job_id.

- Stability (Major): `_audit_interaction` derived attribution (`_audit_attribution`
  → `canonicalize_root` → `Path.resolve()`, which can raise `OSError` on a symlink
  loop) OUTSIDE the best-effort try. A raise there propagated out of
  `handle_request` and aborted an otherwise-successful request, contradicting the
  "a disk failure must never abort request handling" contract. Move the whole
  derivation inside the guard and broaden it so the entire audit path is best-effort.

- Test guard (Trivial): every gc test mocked `now_utc_iso`, so none proved the real
  unmocked output round-trips through `_parse_iso_epoch` — a format drift would
  silently degrade gc() to "never prunes". Add one unmocked round-trip assertion.

Regression tests added for all three: a wait-path control-char job_id header-forge
guard mirroring the submit-path one, a direct sync `_audit_interaction`
attribution-failure-is-swallowed test, and the unmocked timestamp round-trip.
Whole-tree `./pw verify` green (135s).

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

@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.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
marketplace/bundles/plan-marshall/skills/manage-build-server/scripts/marshalld.py (1)

456-468: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Wrap startup GC in a best-effort guard
InteractionAudit.gc() already tolerates unreadable log contents, but the prune path still rewrites the file and can raise OSError from atomic_write_file() or chmod(). A cleanup failure here can still stop daemon startup, so suppress OSError around this call or handle it inside gc().

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@marketplace/bundles/plan-marshall/skills/manage-build-server/scripts/marshalld.py`
around lines 456 - 468, Wrap the self._interaction_audit.gc() call in serve()
with best-effort OSError handling so failures from atomic_write_file() or
chmod() do not abort daemon startup. Preserve the existing startup sequence and
continue serving when cleanup fails.
🧹 Nitpick comments (1)
marketplace/bundles/plan-marshall/skills/manage-build-server/scripts/marshalld.py (1)

302-330: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

reason capture misses the error key used by most failure responses.

unknown_op, invalid_job, and missing_job responses set an error key, not reason (only status_payload(STATUS_REFUSED, reason=...) uses reason). So for these common failure paths the audit record gets outcome='error' with reason=None, losing the actual failure detail in the audit trail.

♻️ Proposed fix
-            reason = response.get('reason')
+            reason = response.get('reason') or response.get('error')
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@marketplace/bundles/plan-marshall/skills/manage-build-server/scripts/marshalld.py`
around lines 302 - 330, The _audit_interaction method currently captures only
response['reason'], so failure responses using the error key lose their detail.
Update reason extraction in _audit_interaction to use the response’s error value
when reason is absent, while preserving the existing reason value for responses
that provide it; continue passing the resulting text to InteractionAudit.record.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In
`@marketplace/bundles/plan-marshall/skills/manage-build-server/scripts/marshalld.py`:
- Around line 456-468: Wrap the self._interaction_audit.gc() call in serve()
with best-effort OSError handling so failures from atomic_write_file() or
chmod() do not abort daemon startup. Preserve the existing startup sequence and
continue serving when cleanup fails.

---

Nitpick comments:
In
`@marketplace/bundles/plan-marshall/skills/manage-build-server/scripts/marshalld.py`:
- Around line 302-330: The _audit_interaction method currently captures only
response['reason'], so failure responses using the error key lose their detail.
Update reason extraction in _audit_interaction to use the response’s error value
when reason is absent, while preserving the existing reason value for responses
that provide it; continue passing the resulting text to InteractionAudit.record.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository: cuioss/coderabbit/.coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 75830ba6-10b2-4714-aa50-bbbbb13dcd66

📥 Commits

Reviewing files that changed from the base of the PR and between ddfdb1e and 287b13d.

📒 Files selected for processing (5)
  • marketplace/bundles/plan-marshall/skills/build-server-client/scripts/build_server.py
  • marketplace/bundles/plan-marshall/skills/manage-build-server/scripts/marshalld.py
  • test/plan-marshall/build-server/test_build_server_client.py
  • test/plan-marshall/build-server/test_interaction_audit_correlation.py
  • test/plan-marshall/build-server/test_marshalld_audit.py
🚧 Files skipped from review as they are similar to previous changes (2)
  • test/plan-marshall/build-server/test_interaction_audit_correlation.py
  • marketplace/bundles/plan-marshall/skills/build-server-client/scripts/build_server.py

cuioss-oliver and others added 14 commits July 20, 2026 20:29
…e marshalld daemon

The build-execute routing seam (_build_execute_factory.cmd_run) routes a build to
marshalld when the project is registered AND the daemon is ready, BEFORE reaching
the in-process build-queue-slot path. The queue-slot and factory cmd_run tests
monkeypatch only the in-process path, so on a host where the meta-project is
registered and the daemon is up (as of the PLAN-02 registration-scope work),
cmd_run routed the build to the real daemon and the injected exec recorder / queue
double were never exercised — 8 tests failed with exec_recorder.ran==False, empty
sleeps, and rc==0 instead of 1.

Force the D5 routing seam to signal fallback (_route_to_daemon -> (None,
'no_notation')) in both test setups so these tests exercise the in-process path
they are written for, regardless of daemon state on the host. Pre-existing
hermeticity gap exposed by the live daemon; green in daemon-free CI. Fixed as a
Boy-Scout scope deviation while shipping the interaction-audit-logging plan.

Co-Authored-By: Claude <noreply@anthropic.com>
…dable log

Pre-submission self-review caught an unguarded boundary: InteractionAudit.read_all()
called read_text() with no guard, and gc() (which calls read_all) runs unguarded
from Daemon.serve() at daemon startup. A corrupted or non-UTF-8 interaction-audit.log
would raise OSError/UnicodeDecodeError and crash the whole daemon at startup —
inconsistent with the sibling rotate_log() call in the same serve() window, which is
explicitly best-effort (try/except OSError: pass). The one other caller, run_logs,
guarded only with except OSError, which does not catch UnicodeDecodeError.

Harden read_all() at the single read choke point to fail closed (return []) on both
OSError and UnicodeDecodeError, so gc() and run_logs degrade to an empty view rather
than crashing. Remove the now-dead except OSError guard in run_logs. Add regression
tests asserting read_all() and gc() tolerate a non-UTF-8 corrupt log without raising.

Co-Authored-By: Claude <noreply@anthropic.com>
…ion-audit log

Follow-up to the read_all hardening: removing run_logs' dead except OSError guard
left contract drift — run_logs' docstring still promised a log_unreadable reason
that was no longer emitted, so a present-but-corrupt log silently returned an
unlabelled empty view (self-review finding 14d972).

Add InteractionAudit.read_records_or_none() as the single read+parse choke point:
it returns None for a present-but-unreadable log (OSError or UnicodeDecodeError),
[] for absent or present-and-empty, and the parsed records otherwise. read_all()
delegates and collapses None to [] so gc()/daemon-startup stay crash-safe, while
run_logs keys its explicit log_unreadable reason off the None — restoring the D3
fail-closed-with-named-reason success criterion. Add tests for the log_unreadable
run_logs path and the three-state read_records_or_none contract.

Co-Authored-By: Claude <noreply@anthropic.com>
Self-review finding 261e0c: the `logs --limit` help text ("newest tail") and the
manage-build-server SKILL.md ("newest-first tail") implied the returned records
are newest-first, but read_all() returns oldest-first append order and run_logs'
tail=scoped[-limit:] preserves it, so records[0] is the oldest of the window.
Reword both to state "the N most recent records, ordered oldest-first within the
returned window" so a caller cannot misread records[0] as the most recent.

Co-Authored-By: Claude <noreply@anthropic.com>
…n-audit-logging

Proactive finalize security-audit found two hardening opportunities in the new
interaction-audit-logging surface:

- CWE-117 log injection (6e0f85): build_server.py derived the audit-log notation
  directly from client-controlled command[2] and interpolated it into a free-text,
  line-oriented work-log message (not JSON-escaped, parsed back by a per-line header
  regex). An embedded newline could forge a fake log-entry header into the plan work
  log, reachable even on the degraded handshake-failure path. Strip C0 control chars
  from the notation before it is used for logging/ledger correlation; the daemon still
  receives the original unmodified command, so allowlist verification is unaffected.
- Secrets-discipline backstop (f62d59): InteractionAudit.record enforced its
  no-secrets contract only by docstring convention. Add a closed _FORBIDDEN_EXTRA_KEYS
  set (command/args/argv/spec/env/cwd/exec_path/project_path) that raises ValueError
  immediately, so a future caller cannot silently leak a secret-shaped field into the
  durable audit trail (the plan's hard S4 no-secrets requirement).

Regression tests added for the notation sanitizer, the end-to-end forged-header path,
and every forbidden extra key.

Co-Authored-By: Claude <noreply@anthropic.com>
…op nested event loop

The correlation test drove the daemon by monkeypatching the client's _call_daemon
to `asyncio.run(daemon.handle_request(...))` — nesting a fresh event loop inside a
synchronous client callback per call, for no benefit. Replace it with the sibling
daemon tests' proven-safe shape: one top-level asyncio.run of the daemon dispatch
to obtain the job_id + server audit record, then feed that response back through a
canned client stub so the client work log and change-ledger row carry the same
job_id. Same three-store correlation assertions, no nested loops.

Co-Authored-By: Claude <noreply@anthropic.com>
…1 time-bomb

test_main_re_review_wires_args_and_prints_toon and
test_main_re_review_accepts_sourcery_bot_kind exercise the real cmd_re_review ->
request_fresh_review path, which stamps trigger_time via _now_iso() (the real wall
clock). _match_review only accepts a review whose submitted_at post-dates that
trigger, and both tests hardcode a review at 2026-01-01T00:05:00Z without freezing
_now_iso. While the wall clock was before Jan 2026 the review post-dated "now" and
matched instantly; once the clock passed 2026-01-01 the match can never succeed, so
poll_until busy-loops on CPU (these tests no-op time.sleep) for the full
DEFAULT_CI_TIMEOUT before passing via the timed-out branch. That CPU spin is
survivable on a many-core dev machine but starves a 2-core CI runner — the observed
CI slowdown/"hang", surfaced by pytest-timeout as a >300s failure.

Freeze github_re_review._now_iso to 2026-01-01T00:00:00Z in both tests so the
trigger deterministically precedes the mocked review's 00:05 stamp and the match is
instant. Production code is unchanged and correct — a real trigger IS "now"; only
the tests needed to pin the clock.

Co-Authored-By: Claude <noreply@anthropic.com>
…gate, fix latent mypy findings

Add pytest-timeout (timeout=300, default signal method) so a hung/busy-looping
test surfaces as an attributable >300s failure with a stack instead of an opaque
CI job timeout. Adding it re-resolved the previously all-unpinned [tool.pyprojectx.main]
env, which floated every tool to a new major and broke the gate on unrelated code:

- mypy 1.x -> 2.3.0 flagged pre-existing latent type errors (index/return-type) in
  _analyze_markdown.py and _list_providers.py.
- pytest 8 -> 9 and pytest-xdist -> 3.8 crashed xdist workers ("node down") and
  then loadscope internal-errored (KeyError <WorkerController>), aborting the run.

Cap each below its next major (mypy<2, pytest<9, pytest-xdist<3.8) so adding a dev
tool can no longer silently drag the type-checker or test runner across a major
version — adopting those majors is a deliberate migration, not a transitive
side effect. pw.lock captures the resolved versions for a reproducible env.

Also fix the two latent findings the newer mypy correctly caught (annotate the
widened dict unions as dict[str, Any]) so the files are clean under 1.x and the
eventual 2.x migration alike.

Co-Authored-By: Claude <noreply@anthropic.com>
… CI (asyncio subprocess-cancel)

The correlation test drove the daemon via asyncio.run(daemon.handle_request(submit)),
assuming Scheduler(max_slots=0) would keep the job queued with no subprocess. It does
not: Scheduler clamps to max(1, max_slots), so the job is admitted, _execute spawns a
real run_job subprocess as a fire-and-forget task, and asyncio.run then hangs at
loop-close cancelling that in-flight subprocess transport. The hang is Python-version
specific — reproduced under CI's 3.12 (macOS/Linux kqueue/epoll select(timeout=None)),
invisible under local 3.14 — which is why every local run was green while CI wedged for
~90 min until pytest-timeout finally surfaced it as a >300s failure.

Drive the two seams the correlation test actually needs — daemon job_id assignment
(_scheduler.submit, enqueue only) and the audit-on-dispatch write (_audit_interaction) —
synchronously, with no event loop, no admission, and no subprocess. Verified under
Python 3.12.3 (the CI interpreter): 3 passed in 0.10s, previously hung to timeout.

Co-Authored-By: Claude <noreply@anthropic.com>
…best-effort attribution, gc format guard

Three valid findings from the PR #949 review, all against this plan's own diff:

- CWE-117 (Major): `run_wait` interpolated the client-supplied `--job-id` into the
  line-oriented work log unsanitized, so a `\n`/`\r` in job_id could forge a fake
  header line — the same vector already closed for the executor notation. Extract a
  single `_sanitize_for_log` seam (notation reuses it) and apply it to job_id.

- Stability (Major): `_audit_interaction` derived attribution (`_audit_attribution`
  → `canonicalize_root` → `Path.resolve()`, which can raise `OSError` on a symlink
  loop) OUTSIDE the best-effort try. A raise there propagated out of
  `handle_request` and aborted an otherwise-successful request, contradicting the
  "a disk failure must never abort request handling" contract. Move the whole
  derivation inside the guard and broaden it so the entire audit path is best-effort.

- Test guard (Trivial): every gc test mocked `now_utc_iso`, so none proved the real
  unmocked output round-trips through `_parse_iso_epoch` — a format drift would
  silently degrade gc() to "never prunes". Add one unmocked round-trip assertion.

Regression tests added for all three: a wait-path control-char job_id header-forge
guard mirroring the submit-path one, a direct sync `_audit_interaction`
attribution-failure-is-swallowed test, and the unmocked timestamp round-trip.
Whole-tree `./pw verify` green (135s).

Co-Authored-By: Claude <noreply@anthropic.com>
@cuioss-oliver
cuioss-oliver force-pushed the feature/marshalld-interaction-audit-logging branch from 287b13d to 279aa85 Compare July 20, 2026 18:29
…raction audit

Two valid findings from CodeRabbit's review of the prior loop-back commit
(287b13d), both in marshalld.py:

- Major: Daemon.serve() called self._interaction_audit.gc() unguarded. gc()'s
  prune path rewrites the log via atomic_write_file()/chmod(), either of which can
  raise OSError; an unguarded raise there aborts daemon startup. Wrapped in a
  best-effort OSError guard so a cleanup failure carries the stale records to the
  next start's GC instead of taking the daemon down — same posture as the existing
  _audit_interaction best-effort guard and the sibling rotate_log startup guard.

- Trivial: _audit_interaction captured only response['reason'], but the
  unknown_op / invalid_job / missing_job failure responses set `error`, not
  `reason` — so those records logged outcome='error' with reason=None, losing the
  failure detail. reason now falls back to response['error'].

Regression test asserts the unknown-op audit record keeps its error detail.
Whole-tree ./pw verify green (147s).

Co-Authored-By: Claude <noreply@anthropic.com>
@cuioss-oliver
cuioss-oliver added this pull request to the merge queue Jul 20, 2026
Merged via the queue into main with commit aafcd19 Jul 20, 2026
11 checks passed
@cuioss-oliver
cuioss-oliver deleted the feature/marshalld-interaction-audit-logging branch July 20, 2026 18:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant