Skip to content

Fix verify-all packaging truth, OpenAPI snapshot, and Pydantic warnings#11

Merged
slucerodev merged 6 commits intomainfrom
repair/verify-all-openapi-pydantic-20260330
Mar 31, 2026
Merged

Fix verify-all packaging truth, OpenAPI snapshot, and Pydantic warnings#11
slucerodev merged 6 commits intomainfrom
repair/verify-all-openapi-pydantic-20260330

Conversation

@slucerodev
Copy link
Copy Markdown
Owner

@slucerodev slucerodev commented Mar 30, 2026

Summary

  • Make exoarmur verify-all package-aware so it works from an installed package and gracefully skips repo-only assets when absent
  • Align the OpenAPI snapshot with the current generated schema
  • Remove deprecated Pydantic v2 config patterns from execution-boundary models
  • Update docs to point at the console-script entry point

Validation

  • PYTHONPATH=src python3 -m pytest tests/test_cli_env.py tests/test_demo_standalone.py -q
  • PYTHONPATH=src python3 -m pytest tests/test_schema_snapshots.py::TestSchemaSnapshots::test_openapi_snapshot_unchanged -q
  • PYTHONPATH=src python3 -m exoarmur.cli verify-all --fast
  • Focused execution-boundary suite with PydanticDeprecatedSince20 filtering ran cleanly

Notes

  • test-results.xml was intentionally left local and not committed
  • Branch pushed safely; main was not modified directly

Summary by CodeRabbit

Release Notes v0.3.0

  • New Features

    • Added standalone governance proof demo and proof bundle artifact generation
    • Introduced deterministic output UIs for governance verification workflows
  • Documentation

    • Clarified governance contract model from "immutable" to "locked" via repository policy and regression gates
    • Updated demo and validation procedures with standalone proof approach
    • Renamed Phase 6 verification to Phase 6 reality run
  • Infrastructure

    • Strengthened reproducibility with deterministic, locked dependency installations
    • Added environment parity validation for CI consistency
    • Improved test stability classification and reporting

Copy link
Copy Markdown

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

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: 4e86026b77

ℹ️ 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".

payload_ref,
existing_audit_id,
),
recorded_at=utc_now(),
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 Preserve stable timestamp on idempotency hits

On an idempotency hit, this now returns recorded_at=utc_now() while reusing the same audit_id, so two retries of the same logical event produce different audit records. That breaks deterministic replay/evidence expectations and can cause inconsistent serialized outputs for the same idempotency key across runs. The previous implementation derived a stable timestamp from deterministic inputs; this path should similarly keep recorded_at stable (or reuse the original stored record timestamp).

Useful? React with 👍 / 👎.


correlation_id: str
replay_timestamp: datetime = field(default_factory=_deterministic_replay_timestamp)
replay_timestamp: datetime = field(default_factory=utc_now)
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 Make replay report timestamp deterministic again

Changing ReplayReport.replay_timestamp to utc_now makes replay output vary for identical input audit streams, because _save_report persists this field directly. This undermines deterministic replay artifacts and causes avoidable diffs/hash churn even when reconstructed state is identical. The timestamp should be derived deterministically (or fixed) so repeated replays of the same correlation produce stable report content.

Useful? React with 👍 / 👎.

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 30, 2026

Caution

Review failed

Pull request was closed or merged during review

📝 Walkthrough

Walkthrough

This pull request introduces deterministic environment enforcement for CI/CD, replaces the V2 restrained autonomy demo with a standalone governance proof example, establishes a new stability testing framework with environment parity validation, updates contract terminology from "immutable" to "locked," and standardizes Python/dependency installation across workflows using pinned versions and lock files.

Changes

Cohort / File(s) Summary
CI/CD Workflow Standardization
.github/workflows/core-invariant-gates.yml, deploy.yml, documentation.yml, multi-platform-tests.yml, observability.yml, phase-0d-boundary-enforcement.yml, security-scan.yml, v2-demo-smoke.yml
Pinned Python to 3.12.3, replaced requirements.txt installs with pip install -r requirements.lock, added pip==24.0 version pinning, added --no-deps -e . editable installs, added environment parity verification steps, renamed V2 demo jobs to standalone demo, updated demo validation markers and artifact paths.
Documentation Terminology Updates
CONTRIBUTING.md, OPEN_CORE_BOUNDARIES.md, README.md, README_v0.3.0.md, RELEASE_REPRODUCIBILITY.md, ROADMAP.md, SECURITY.md, VALIDATE.md, docs/ARCHITECTURE.md, docs/ARCHITECTURE_SIMPLE.md, docs/CONSTITUTION.md, docs/CONTRACTS.md, docs/DESIGN_PRINCIPLES.md, docs/EXECUTION_GOVERNANCE_ARCHITECTURE.md, docs/EXOARMUR_SYSTEMS_PAPER.md, docs/GOVERNANCE.md, docs/HN_SHOW_POST_v0.2.0.md, docs/MODULE_SEPARATION.md, docs/ORGANISM_PRINCIPLES.md, docs/PHASE_STATUS.md, docs/README.md, docs/RELEASE_NOTES_v1.0.0-beta.md, docs/REPLAY_PROTOCOL.md
Replaced "immutable" with "locked" for V1 contracts, updated replayability claims to reference "recorded" decisions, rephrased from "exact reconstruction" to "deterministic reconstruction of recorded processes," adjusted threat modeling language to emphasize "tamper-evident" over absolute immutability, narrowed determinism guarantees to "supported governance inputs."
Stability Testing Framework
src/exoarmur/stability/ (new package), src/exoarmur/stability/__init__.py, asyncio_policy.py, classifier.py, env_parity.py, reporting.py
New module providing event loop policy snapshots, environment parity validation against lock files, pytest classification of failures (ASYNC/ENVIRONMENT/DETERMINISM/TEST_DESIGN), and structured StabilityReport generation with JSON serialization.
Pydantic Model Configuration Migration
src/exoarmur/execution_boundary_v2/approvals/approval_models.py, models/action_intent.py, execution_dispatch.py, execution_proof_bundle.py, execution_trace.py, policy_decision.py, policy/policy_models.py
Migrated from inner class Config to model_config = ConfigDict(...) pattern, removed unused json_encoders customizations, standardized extra="forbid" and str_strip_whitespace=True settings.
Audit and Timestamp Handling
src/exoarmur/audit/audit_logger.py, execution_boundary_v2/pipeline/proxy_pipeline.py, replay/replay_engine.py
Replaced deterministic timestamp generation (deterministic_timestamp(...)) with runtime timestamps (utc_now()), removing field-derived hash-based timestamp computation in favor of actual execution-time timestamps.
CLI and Verification Updates
src/exoarmur/cli.py, src/exoarmur/main.py, tests/conftest.py, conftest.py
Added repository root detection for installed-package vs repo-local modes, integrated standalone demo proof validation, added event loop policy initialization, seeded random module for deterministic behavior, updated demo CLI to default to standalone scenario.
Standalone Demo Implementation
examples/demo_standalone.py, examples/demo_standalone_proof_bundle.json, examples/quickstart_replay.py
New standalone execution-boundary denial demo with PathBoundaryPolicyDecisionPoint policy enforcer, guarded filesystem executor, proof bundle generation with replay hash and audit records, example JSON artifact showing denied action with evidence.
V2 Demo Replacement and Migration
scripts/demo_v2_restrained_autonomy.py (removed), tests/test_v2_restrained_autonomy.py, tests/test_demo_standalone.py, tests/test_cli_env.py
Removed V2 restrained autonomy script, replaced with standalone demo in tests, updated CLI tests to target standalone demo, added test coverage for proof bundle validation and marker checking.
Docker and Deployment
Dockerfile, docker-compose.yml
Updated base Python from 3.9-slim to 3.12-slim, implemented two-stage build, added PYTHONHASHSEED=0 and other determinism environment variables, switched from nats service to exoarmur-demo service, updated healthcheck endpoints and routing configuration.
Demo API and Scenario Scripts
scripts/demo_api.py, scripts/demo_scenario.py, scripts/demo_web_server.py, scripts/demo/demo_api.py, scripts/demo/demo_scenario.py, scripts/demo/demo_web_server.py, scripts/demo/demo_identity_containment.py, scripts/demo/demo_v2_restrained_autonomy.py
New FastAPI and HTTP servers exposing replay, consensus, and Byzantine test endpoints, demo scenario orchestration with deterministic hashing, web UI serving with canonical event conversion and multi-node verification.
Environment and Determinism Validation
scripts/check_determinism.py, scripts/check_core_determinism.py, scripts/infra/check_determinism.py, scripts/infra/check_core_determinism.py, scripts/infra/stability_ci.py, scripts/infra/verify_env_parity.py
New determinism scanning tools that detect non-deterministic patterns (datetime.now, random calls, unsorted json.dumps), environment parity validation against locked dependencies, CI stability runner for multi-run test validation with flake detection.
Validation and Experiment Scripts
scripts/external_validation.py, scripts/output_consistency_check.py, scripts/failure_visibility_validation.py, scripts/generate_golden_artifacts.py, scripts/exoarmur_production_drift_demo.py, scripts/external_user_simulation.py, scripts/narrative_clarity_validation.py, scripts/observability_demo.py, scripts/ai_agent_verification_server.py, scripts/integrity_verification_server.py, scripts/deploy_demo.py, scripts/stabilization_complete.py, scripts/test_api_functionality.py, scripts/test_demo_api.py, plus scripts/validation/* and scripts/experiments/* variants
Comprehensive validation, demo, and experiment suites covering replay determinism, multi-node consensus verification, Byzantine fault injection, production drift simulation, user experience testing, clarity validation, and API functionality verification; includes corresponding UI HTML pages for web-based validation.
Repository Structure and Build Config
pyproject.toml, docs/Makefile, docs/conf.py, docs/index.md, docs/BUILD_AND_TEST.md, docs/DEVELOPMENT_SETUP.md, docs/REPO_STRUCTURE.md, docs/REVIEW_CHECKLIST.md, docs/RUNBOOK_V2_DEMO.md, README_MINIMAL.md
Added pytest11 plugin entry point for exoarmur.stability.classifier, removed pytest-benchmark dependency, added Sphinx documentation build files, updated verification commands to use exoarmur verify-all entrypoint, updated repo structure documentation to include examples/ directory and label V2 demo as legacy.
OpenAPI Schema
artifacts/openapi_v1.json
Removed ctx and input properties from ValidationError schema, narrowing the error response structure.
Test Infrastructure
tests/test_env_parity.py, tests/test_event_loop_policy.py, tests/test_stability_classifier.py
New test modules validating environment parity checking, event loop policy management, and failure classification logic.
HTML UI Examples
examples/ai_agent_verification_ui.html, examples/demo_ui.html, examples/external_validation_ui.html, examples/integrity_verification_ui.html
New standalone HTML dashboards for AI agent verification, deterministic governance runtime, external validation trust signals, and system integrity verification with client-side orchestration of replay/consensus/Byzantine validation flows.

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120+ minutes

Possibly related PRs

  • PR #9: Modifies timestamp handling in demo scripts and audit logging (related code-level changes to scripts/demo_v2_restrained_autonomy.py and audit record emission patterns).
  • PR #5: Updates CLI subprocess handling, audit logger behavior, and test invocation patterns that overlap with this PR's CLI and stability module changes.

Poem

🐰 A rabbit hopped through the code today,
Locking contracts in a deterministic way,
From "immutable" dreams to "locked" so tight,
Replay hashes flowing, verified right.
Tests now stable, dashboards bright,
This governance proof burns ever slight! 🔐✨

✨ 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 repair/verify-all-openapi-pydantic-20260330

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 14

Caution

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

⚠️ Outside diff range comments (3)
src/exoarmur/audit/audit_logger.py (1)

175-193: ⚠️ Potential issue | 🟠 Major

Return original persisted record on idempotency hit to preserve timestamp consistency.

Line 40 sets a fresh recorded_at=utc_now() when idempotency is hit, causing duplicate submissions to return different timestamps for the same audit ID. This breaks replay consistency.

The proposed fix referencing self.audit_records won't work—records are only stored in that cache after the idempotency check passes (line 72). When idempotency hits at line 31, the original record is never added to the in-memory cache. The original record was persisted to NATS JetStream/KV store and needs to be retrieved from there to return with its original timestamp.

The fix should query the persistent store to fetch and return the original AuditRecordV1 object with its original recorded_at value, rather than creating a new record with a fresh timestamp.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/exoarmur/audit/audit_logger.py` around lines 175 - 193, On idempotency
hit (when existing_audit_id is truthy) don't synthesize a new AuditRecordV1 with
recorded_at=utc_now(); instead fetch and return the original persisted
AuditRecordV1 from the persistent store (the same JetStream/KV retrieval path
used elsewhere in this class) by audit_id or idempotency_key and return that
object so the original recorded_at is preserved; replace the manual
AuditRecordV1 construction in the existing_audit_id branch with a call to the
class's persistent-fetch method and return its result (do not rely on
self.audit_records or utc_now()).
docs/CONSTITUTION.md (1)

181-191: ⚠️ Potential issue | 🟡 Minor

Use the console-script entry point in this snippet.

This block still documents python3 src/cli.py ..., which won't work once the package is installed normally. Please switch the remaining commands here to exoarmur ... so the constitution matches the packaged workflow.

Suggested change
 # Verify all constitutional invariants
-python3 src/cli.py verify-all
+exoarmur verify-all

 # Check specific invariants
 python3 -m pytest tests/ -k "v2_disabled"     # V1 immutability
 python3 -m pytest tests/ -m "sensitive"       # Boundary gate
-python3 src/cli.py demo  # Safe defaults
+exoarmur demo  # Safe defaults
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/CONSTITUTION.md` around lines 181 - 191, Replace direct python
invocations of the CLI with the console-script entry point: change commands
using "python3 src/cli.py verify-all" and "python3 src/cli.py demo" to use
"exoarmur verify-all" and "exoarmur demo", and change any test/demo invocation
guidance (e.g., running the packaged example script such as "python3
examples/demo_standalone.py") to invoke the installed CLI where appropriate;
update the documented pytest lines only if they should reference the package
entry (leave pytest commands as-is if they run tests directly). Ensure
references to src/cli.py are removed and the entry point "exoarmur" is used
throughout.
.github/workflows/multi-platform-tests.yml (1)

141-149: ⚠️ Potential issue | 🟠 Major

Exercise verify-all outside the checkout after the wheel install.

This job still never covers the regression this PR fixes: verify-all running from an installed package without repository-only assets nearby. Importing exoarmur from the repo workspace won't catch that, so please add a smoke run from a temp directory before switching back to editable.

Suggested change
     - name: Test package installation
       run: |
         python -m pip install dist/*.whl
+        python -c "import subprocess, tempfile; subprocess.run(['exoarmur', 'verify-all', '--fast'], cwd=tempfile.mkdtemp(), check=True)"
         python -c "import exoarmur; print(f'ExoArmur version: {exoarmur.__version__}')"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/multi-platform-tests.yml around lines 141 - 149, After the
wheel install step, add a new run that moves to a fresh temporary directory and
executes the installed CLI's verify-all command to ensure the wheel works
outside the repo (i.e., after "python -m pip install dist/*.whl" run something
like: TMPDIR=$(mktemp -d) && cd "$TMPDIR" && exoarmur verify-all or the
equivalent python -m invocation), placing this new step before the "Test
editable installation" step so the check runs from outside the checkout;
reference the existing step name "Test package installation" and the CLI/command
"exoarmur verify-all" when adding the step.
🧹 Nitpick comments (4)
src/exoarmur/main.py (1)

64-64: Move event-loop policy initialization to FastAPI lifespan/startup hook.

This call at line 64 executes when the module is imported by tests, not just during service startup. Since many test files import exoarmur.main to access the app object, this causes unexpected global state mutation at import time. Configure it in FastAPI's lifespan context manager instead to ensure it runs only when the application actually starts.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/exoarmur/main.py` at line 64, The top-level call to
ensure_default_event_loop_policy() must be removed from module import and
invoked inside the FastAPI application lifespan/startup hook so it runs only
during app startup; locate the app object (e.g., app = FastAPI(...)) and either
define an async lifespan context manager or a startup event handler (using
`@app.on_event`("startup") or asynccontextmanager-based lifespan) and call
ensure_default_event_loop_policy() there before other startup actions; delete
the original module-level call to ensure_default_event_loop_policy() so tests
importing exoarmur.main no longer mutate global event-loop policy at import
time.
scripts/stability_ci.py (1)

66-72: Consider adding a timeout to prevent CI hangs.

The subprocess call doesn't specify a timeout. If a test hangs indefinitely, the CI job will also hang until it's killed by the CI runner's global timeout (if any).

Proposed improvement
     cmd = [sys.executable, "-m", "pytest", "-q", *pytest_args]
     print(f"\n=== Stability run {run_index}/{total_runs} ===")
     print(" ".join(cmd))
-    completed = subprocess.run(cmd, env=env, check=False)
+    completed = subprocess.run(cmd, env=env, check=False, timeout=3600)  # 1 hour timeout
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@scripts/stability_ci.py` around lines 66 - 72, The subprocess.run call
(completed = subprocess.run(cmd, env=env, check=False)) can hang indefinitely;
add a timeout argument (configurable via an env var like STABILITY_TIMEOUT or a
default, e.g., 1800s) and wrap the call in a try/except catching
subprocess.TimeoutExpired, so if the timeout is reached you log/raise a clear
error (include run_index and report_path context), terminate/cleanup
appropriately and return a non-zero exit code instead of hanging; ensure the
rest of the function still checks report_path and uses completed.returncode when
completed exists.
src/exoarmur/cli.py (1)

30-33: Bare except clause could hide errors.

The bare except: on line 32 catches all exceptions including KeyboardInterrupt and SystemExit. Consider catching a specific exception type.

♻️ Proposed fix
     try:
         sys.stdout.reconfigure(encoding='utf-8')
         sys.stderr.reconfigure(encoding='utf-8')
-    except:
+    except (AttributeError, OSError):
         pass  # Fallback if reconfigure fails
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/exoarmur/cli.py` around lines 30 - 33, Replace the bare except in the
stdout/stderr reconfigure block with a specific exception handler to avoid
swallowing system-exiting signals; update the try/except around
sys.stdout.reconfigure and sys.stderr.reconfigure to catch only likely
exceptions (e.g., except (AttributeError, ValueError):) and optionally add a
brief comment or debug log, referencing the sys.stdout.reconfigure and
sys.stderr.reconfigure calls so the change is applied to that exact block.
src/exoarmur/stability/classifier.py (1)

119-119: Use explicit conversion flag per Ruff suggestion.

The static analysis tool suggests using an explicit conversion flag instead of str(longrepr). This is a minor style improvement.

♻️ Proposed fix
-    report.longrepr = f"{prefix}{str(longrepr)}"
+    report.longrepr = f"{prefix}{longrepr!s}"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/exoarmur/stability/classifier.py` at line 119, The f-string currently
uses str(longrepr) when setting report.longrepr; replace the explicit str() call
with the f-string conversion flag to satisfy Ruff (use the !s conversion) so
update the assignment in classifier.py where report.longrepr is set (the
expression using prefix and longrepr) to use {longrepr!s} instead of
str(longrepr).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In @.github/workflows/deploy.yml:
- Line 276: The workflow references the create_release step as create_release
(underscore) but later uses steps.create-release.outputs.upload_url (hyphen),
causing a runtime failure; update the incorrect reference to use the correct
step ID (steps.create_release.outputs.upload_url) or alternatively rename the
step ID to create-release to match—locate the step ID create_release and the
usage steps.create-release.outputs.upload_url and make them consistent so both
use the same identifier.
- Around line 191-194: The CI jobs "deploy-test" and "deploy-production" run pip
installs (python -m pip install -r requirements.lock and python -m pip install
--no-deps -e .) but never check out the repository, so requirements.lock and the
package root are missing; add a first step that uses actions/checkout@v6 to both
the deploy-test and deploy-production jobs (place the checkout step before any
python -m pip install steps) so the workspace contains requirements.lock and the
project files before installing.

In @.github/workflows/documentation.yml:
- Around line 39-49: The push/pull_request path filters in the workflow are
missing the new files that these jobs depend on; update the workflow's path
filters to include requirements.lock, scripts/verify_env_parity.py, and
scripts/check_markdown_links.py so changes to those files trigger the workflow;
locate the job steps like "Install dependencies" and "Verify locked environment"
in .github/workflows/documentation.yml and add those filenames to the paths list
for both push and pull_request triggers (also apply the same change to the other
similar block referenced around lines 123-129).
- Around line 105-108: The CI step named "Test demo scripts" is masking failures
by appending "|| echo 'Demo script needs environment setup'" to the command;
remove the "|| echo ..." so the command is simply "python
examples/demo_standalone.py" (allowing non-zero exits to fail the step) or
alternatively set an explicit continue-on-error flag if you truly want to allow
failures—update the run command in that step (the line containing "python
examples/demo_standalone.py || echo ...") to stop swallowing errors so Validate
Examples correctly fails on script errors.

In `@docs/ARCHITECTURE.md`:
- Around line 5-6: The doc mixes the term "Locked" in the header "V1 Core
(Locked Cognition Loop)" with the older phrase "immutability guarantees"; update
the occurrence(s) of "immutability guarantees" to use the consistent term
"Locked Cognition Loop" (or "Locked" where brevity is used) so the architecture
contract uses the same terminology throughout—search for the phrase
"immutability guarantees" and replace it with "Locked Cognition Loop" (matching
the header casing) across the file.

In `@docs/REPO_STRUCTURE.md`:
- Around line 158-160: The documented path for the standalone demo is incorrect:
update the REPO_STRUCTURE entry so that `demo_standalone.py` is referenced under
the actual location `examples/demo_standalone.py` (or change the tree if you
prefer to move the file), and ensure the list items for `demo_standalone.py` and
`demo_v2_restrained_autonomy.py` reflect their real locations (e.g.,
`examples/demo_standalone.py` and `scripts/demo_v2_restrained_autonomy.py` if
that is the case) so the README matches the repository layout.

In `@OPEN_CORE_BOUNDARIES.md`:
- Around line 180-183: Replace the repository script invocation "python3
examples/demo_standalone.py" with the package console entry point so
installed-package users can run the demo (e.g. "open-core-boundaries
demo-standalone" or the project’s documented CLI name); alternatively use the
module form "python -m open_core_boundaries.demo_standalone" to run the same
demo without relying on an examples/ tree, and update the surrounding docs to
reference the CLI entry point instead of the examples/demo_standalone.py path.

In `@scripts/check_markdown_links.py`:
- Around line 66-79: The script currently treats an empty markdown_files result
from _iter_markdown_files(args.paths) as success; update the logic after
collecting markdown_files so that if markdown_files is empty you print a clear
error (e.g., "No Markdown files found for paths: {args.paths}" or similar) and
return a non-zero exit code (1) instead of proceeding to validation; keep the
existing broken_links handling (scan_file(markdown_file) and subsequent
behavior) unchanged but ensure the final success message only runs when
markdown_files is non-empty and no broken_links were found.

In `@src/exoarmur/audit/audit_logger.py`:
- Line 14: The local function utc_now in this module shadows the imported
exoarmur.clock.utc_now and prevents tests from injecting a deterministic clock;
remove the local def utc_now() so all calls (e.g., places that call utc_now at
audit_logger functions around the usages) resolve to the imported utc_now from
exoarmur.clock, ensuring set_clock()/DeterministicClock works as intended and no
other references need changing.

In `@src/exoarmur/cli.py`:
- Around line 308-316: The code assumes repo checkout by computing repo_root =
Path(__file__).resolve().parents[2] and then directly launching examples (used
in the scenario == 'standalone' && replay is None branch), which will crash for
installed packages; modify the branch to call and use _discover_repo_root() to
locate the repo root (falling back to a clear, user-friendly error if not
found), then build script_path from that discovered root (e.g.,
examples/demo_standalone.py and scripts/demo_v2_restrained_autonomy.py) and call
subprocess.run as before; ensure you reference and replace usages of repo_root
and script_path in this logic and emit a helpful message when
_discover_repo_root() returns None or the target script file doesn't exist.

In `@src/exoarmur/replay/cli.py`:
- Around line 132-134: The parsing of timestamps in the replay CLI uses
raw_timestamp = data.get('recorded_at') or data.get('timestamp', '') then
datetime.fromisoformat(...) in the block that sets self.recorded_at and
self.timestamp; add an explicit guard that checks if raw_timestamp is truthy
before calling datetime.fromisoformat, and if not raise a ValueError (or custom
error) that includes the event_id from data (e.g., data.get('event_id')) so the
caller/logger gets a clear, targeted message about the missing timestamp instead
of a generic exception and dropped record.

In `@src/exoarmur/replay/replay_engine.py`:
- Line 26: The replay metadata currently uses utc_now(), making timestamps
non-deterministic; replace those utc_now() calls (both occurrences) with the
deterministic timestamp factory used by the replay engine (e.g., the injected
report timestamp factory/timestamp_factory or the replay clock accessor such as
self.report_timestamp_factory()/self.timestamp_factory().now()), ensuring the
code calls that deterministic provider for report timestamps rather than
wall-clock utc_now().

In `@src/exoarmur/stability/env_parity.py`:
- Around line 187-193: The function default_expected_python_version() currently
returns the runtime Python (sys.version.split()[0]), making
validate_environment_parity's Python check a no-op; either change
default_expected_python_version() to return the fixed CI/expected Python string
(e.g., "3.11.8") so the validator compares against a real expected version, or
if the intent is to treat the default as "no expectation", update the function
docstring to state it intentionally allows any runtime Python and leave the
validator logic as-is; locate and update the default_expected_python_version
function and any docs referencing it to reflect the chosen behavior.

In `@tests/test_event_loop_policy.py`:
- Around line 8-12: The test
test_ensure_default_event_loop_policy_installs_default_policy calls
ensure_default_event_loop_policy() which mutates the global asyncio policy and
doesn't restore it; update the test to capture the current policy snapshot via
current_event_loop_policy_snapshot() before calling
ensure_default_event_loop_policy(), run the assertions, then restore the
original policy (e.g., via asyncio.set_event_loop_policy(...) or the test helper
that re-applies the snapshot) so is_explicit_default_event_loop_policy() and
other tests are not affected; ensure you reference the same snapshot object you
captured and restore it in a finally/teardown-style block to guarantee
restoration on failures.

---

Outside diff comments:
In @.github/workflows/multi-platform-tests.yml:
- Around line 141-149: After the wheel install step, add a new run that moves to
a fresh temporary directory and executes the installed CLI's verify-all command
to ensure the wheel works outside the repo (i.e., after "python -m pip install
dist/*.whl" run something like: TMPDIR=$(mktemp -d) && cd "$TMPDIR" && exoarmur
verify-all or the equivalent python -m invocation), placing this new step before
the "Test editable installation" step so the check runs from outside the
checkout; reference the existing step name "Test package installation" and the
CLI/command "exoarmur verify-all" when adding the step.

In `@docs/CONSTITUTION.md`:
- Around line 181-191: Replace direct python invocations of the CLI with the
console-script entry point: change commands using "python3 src/cli.py
verify-all" and "python3 src/cli.py demo" to use "exoarmur verify-all" and
"exoarmur demo", and change any test/demo invocation guidance (e.g., running the
packaged example script such as "python3 examples/demo_standalone.py") to invoke
the installed CLI where appropriate; update the documented pytest lines only if
they should reference the package entry (leave pytest commands as-is if they run
tests directly). Ensure references to src/cli.py are removed and the entry point
"exoarmur" is used throughout.

In `@src/exoarmur/audit/audit_logger.py`:
- Around line 175-193: On idempotency hit (when existing_audit_id is truthy)
don't synthesize a new AuditRecordV1 with recorded_at=utc_now(); instead fetch
and return the original persisted AuditRecordV1 from the persistent store (the
same JetStream/KV retrieval path used elsewhere in this class) by audit_id or
idempotency_key and return that object so the original recorded_at is preserved;
replace the manual AuditRecordV1 construction in the existing_audit_id branch
with a call to the class's persistent-fetch method and return its result (do not
rely on self.audit_records or utc_now()).

---

Nitpick comments:
In `@scripts/stability_ci.py`:
- Around line 66-72: The subprocess.run call (completed = subprocess.run(cmd,
env=env, check=False)) can hang indefinitely; add a timeout argument
(configurable via an env var like STABILITY_TIMEOUT or a default, e.g., 1800s)
and wrap the call in a try/except catching subprocess.TimeoutExpired, so if the
timeout is reached you log/raise a clear error (include run_index and
report_path context), terminate/cleanup appropriately and return a non-zero exit
code instead of hanging; ensure the rest of the function still checks
report_path and uses completed.returncode when completed exists.

In `@src/exoarmur/cli.py`:
- Around line 30-33: Replace the bare except in the stdout/stderr reconfigure
block with a specific exception handler to avoid swallowing system-exiting
signals; update the try/except around sys.stdout.reconfigure and
sys.stderr.reconfigure to catch only likely exceptions (e.g., except
(AttributeError, ValueError):) and optionally add a brief comment or debug log,
referencing the sys.stdout.reconfigure and sys.stderr.reconfigure calls so the
change is applied to that exact block.

In `@src/exoarmur/main.py`:
- Line 64: The top-level call to ensure_default_event_loop_policy() must be
removed from module import and invoked inside the FastAPI application
lifespan/startup hook so it runs only during app startup; locate the app object
(e.g., app = FastAPI(...)) and either define an async lifespan context manager
or a startup event handler (using `@app.on_event`("startup") or
asynccontextmanager-based lifespan) and call ensure_default_event_loop_policy()
there before other startup actions; delete the original module-level call to
ensure_default_event_loop_policy() so tests importing exoarmur.main no longer
mutate global event-loop policy at import time.

In `@src/exoarmur/stability/classifier.py`:
- Line 119: The f-string currently uses str(longrepr) when setting
report.longrepr; replace the explicit str() call with the f-string conversion
flag to satisfy Ruff (use the !s conversion) so update the assignment in
classifier.py where report.longrepr is set (the expression using prefix and
longrepr) to use {longrepr!s} instead of str(longrepr).
🪄 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: defaults

Review profile: CHILL

Plan: Pro

Run ID: 2fbd0ce0-0fd0-414c-a517-a16f173c48eb

📥 Commits

Reviewing files that changed from the base of the PR and between 32906bd and fe276d6.

⛔ Files ignored due to path filters (1)
  • requirements.lock is excluded by !**/*.lock
📒 Files selected for processing (71)
  • .github/workflows/core-invariant-gates.yml
  • .github/workflows/deploy.yml
  • .github/workflows/documentation.yml
  • .github/workflows/multi-platform-tests.yml
  • .github/workflows/observability.yml
  • .github/workflows/phase-0d-boundary-enforcement.yml
  • .github/workflows/security-scan.yml
  • .github/workflows/v2-demo-smoke.yml
  • CONTRIBUTING.md
  • OPEN_CORE_BOUNDARIES.md
  • README.md
  • README_v0.3.0.md
  • RELEASE_REPRODUCIBILITY.md
  • ROADMAP.md
  • SECURITY.md
  • VALIDATE.md
  • artifacts/openapi_v1.json
  • conftest.py
  • docs/ARCHITECTURE.md
  • docs/ARCHITECTURE_SIMPLE.md
  • docs/BUILD_AND_TEST.md
  • docs/CONSTITUTION.md
  • docs/CONTRACTS.md
  • docs/DESIGN_PRINCIPLES.md
  • docs/DEVELOPMENT_SETUP.md
  • docs/EXECUTION_GOVERNANCE_ARCHITECTURE.md
  • docs/EXOARMUR_SYSTEMS_PAPER.md
  • docs/GOVERNANCE.md
  • docs/HN_SHOW_POST_v0.2.0.md
  • docs/MODULE_SEPARATION.md
  • docs/Makefile
  • docs/ORGANISM_PRINCIPLES.md
  • docs/PHASE_STATUS.md
  • docs/README.md
  • docs/RELEASE_NOTES_v1.0.0-beta.md
  • docs/REPLAY_PROTOCOL.md
  • docs/REPO_STRUCTURE.md
  • docs/REVIEW_CHECKLIST.md
  • docs/RUNBOOK_V2_DEMO.md
  • docs/conf.py
  • docs/index.md
  • examples/demo_standalone.py
  • examples/demo_standalone_proof_bundle.json
  • pyproject.toml
  • scripts/check_markdown_links.py
  • scripts/stability_ci.py
  • scripts/verify_env_parity.py
  • src/exoarmur/audit/audit_logger.py
  • src/exoarmur/cli.py
  • src/exoarmur/execution_boundary_v2/approvals/approval_models.py
  • src/exoarmur/execution_boundary_v2/models/action_intent.py
  • src/exoarmur/execution_boundary_v2/models/execution_dispatch.py
  • src/exoarmur/execution_boundary_v2/models/execution_proof_bundle.py
  • src/exoarmur/execution_boundary_v2/models/execution_trace.py
  • src/exoarmur/execution_boundary_v2/models/policy_decision.py
  • src/exoarmur/execution_boundary_v2/pipeline/proxy_pipeline.py
  • src/exoarmur/execution_boundary_v2/policy/policy_models.py
  • src/exoarmur/main.py
  • src/exoarmur/replay/cli.py
  • src/exoarmur/replay/replay_engine.py
  • src/exoarmur/stability/__init__.py
  • src/exoarmur/stability/asyncio_policy.py
  • src/exoarmur/stability/classifier.py
  • src/exoarmur/stability/env_parity.py
  • src/exoarmur/stability/reporting.py
  • tests/conftest.py
  • tests/test_cli_env.py
  • tests/test_demo_standalone.py
  • tests/test_env_parity.py
  • tests/test_event_loop_policy.py
  • tests/test_stability_classifier.py
💤 Files with no reviewable changes (1)
  • artifacts/openapi_v1.json

Comment on lines +191 to 194
python -m pip install --upgrade pip==24.0
python -m pip install -r requirements.lock
python -m pip install --no-deps -e .

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
rg -n -C3 'deploy-test:|deploy-production:|actions/checkout|requirements\.lock|--no-deps -e \.' .github/workflows/deploy.yml

Repository: slucerodev/ExoArmur-Core

Length of output: 2766


deploy-test and deploy-production jobs are missing repository checkout

Lines 191-194 and 222-224 attempt to install from requirements.lock and -e ., but neither job checks out the repository. Without actions/checkout, these files do not exist in the runner, causing the jobs to fail.

Add actions/checkout@v6 as the first step in both jobs:

Proposed patch
  deploy-test:
    name: Deploy to Test PyPI
    runs-on: ubuntu-latest
    needs: [validate, build, security-scan]

    steps:
+   - name: Checkout code
+     uses: actions/checkout@v6
+
    - name: Download build artifacts
      uses: actions/download-artifact@v4

  deploy-production:
    name: Deploy to PyPI
    runs-on: ubuntu-latest
    needs: [validate, build, security-scan]

    steps:
+   - name: Checkout code
+     uses: actions/checkout@v6
+
    - name: Download build artifacts
      uses: actions/download-artifact@v4
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/deploy.yml around lines 191 - 194, The CI jobs
"deploy-test" and "deploy-production" run pip installs (python -m pip install -r
requirements.lock and python -m pip install --no-deps -e .) but never check out
the repository, so requirements.lock and the package root are missing; add a
first step that uses actions/checkout@v6 to both the deploy-test and
deploy-production jobs (place the checkout step before any python -m pip install
steps) so the workspace contains requirements.lock and the project files before
installing.

echo "EOF" >> $GITHUB_OUTPUT

- name: Create Release
id: create_release
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
rg -n 'id:\s*create_release|steps\.create_release\.outputs\.upload_url|steps\.create-release\.outputs\.upload_url' .github/workflows/deploy.yml

Repository: slucerodev/ExoArmur-Core

Length of output: 238


Line 302 uses incorrect step reference that will fail at runtime

Step ID on line 276 is create_release, but line 302 references steps.create-release.outputs.upload_url (with hyphen). This mismatch will cause the source distribution upload to fail. Line 292 correctly uses steps.create_release.outputs.upload_url (with underscore).

Proposed fix
-        upload_url: ${{ steps.create-release.outputs.upload_url }}
+        upload_url: ${{ steps.create_release.outputs.upload_url }}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
id: create_release
upload_url: ${{ steps.create_release.outputs.upload_url }}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/deploy.yml at line 276, The workflow references the
create_release step as create_release (underscore) but later uses
steps.create-release.outputs.upload_url (hyphen), causing a runtime failure;
update the incorrect reference to use the correct step ID
(steps.create_release.outputs.upload_url) or alternatively rename the step ID to
create-release to match—locate the step ID create_release and the usage
steps.create-release.outputs.upload_url and make them consistent so both use the
same identifier.

Comment on lines 39 to +49
- name: Install dependencies
run: |
python -m pip install --upgrade pip
python -m pip install -e .
python -m pip install sphinx sphinx-rtd-theme sphinx-autodoc-typehints myst-parser
python -m pip install --upgrade pip==24.0
python -m pip install -r requirements.lock
python -m pip install --no-deps -e .

- name: Verify locked environment
run: |
python scripts/verify_env_parity.py \
--expected-python-version 3.12.3 \
--expected-platform Linux
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Add the new workflow inputs to the path filters.

These jobs now depend on requirements.lock, scripts/verify_env_parity.py, and scripts/check_markdown_links.py, but the push / pull_request filters still ignore them. A change to any of those files can break docs CI without triggering this workflow.

Suggested change
   push:
     branches: [ main, develop ]
     paths:
       - 'docs/**'
       - 'README.md'
       - 'examples/**'
+      - 'requirements.lock'
+      - 'scripts/check_markdown_links.py'
+      - 'scripts/verify_env_parity.py'
       - 'pyproject.toml'
   pull_request:
     branches: [ main ]
     paths:
       - 'docs/**'
       - 'README.md'
       - 'examples/**'
+      - 'requirements.lock'
+      - 'scripts/check_markdown_links.py'
+      - 'scripts/verify_env_parity.py'
       - 'pyproject.toml'

Also applies to: 123-129

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/documentation.yml around lines 39 - 49, The
push/pull_request path filters in the workflow are missing the new files that
these jobs depend on; update the workflow's path filters to include
requirements.lock, scripts/verify_env_parity.py, and
scripts/check_markdown_links.py so changes to those files trigger the workflow;
locate the job steps like "Install dependencies" and "Verify locked environment"
in .github/workflows/documentation.yml and add those filenames to the paths list
for both push and pull_request triggers (also apply the same change to the other
similar block referenced around lines 123-129).

Comment on lines 105 to +108
- name: Test demo scripts
run: |
# Test V2 restrained autonomy demo
python scripts/demo_v2_restrained_autonomy.py --operator-decision deny || echo "Demo script needs environment setup"
# Test standalone governance demo
python examples/demo_standalone.py || echo "Demo script needs environment setup"
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Don't turn demo failures into success here.

|| echo ... makes the step pass on a non-zero exit, so the Validate Examples job no longer validates examples/demo_standalone.py.

Suggested change
     - name: Test demo scripts
       run: |
         # Test standalone governance demo
-        python examples/demo_standalone.py || echo "Demo script needs environment setup"
+        python examples/demo_standalone.py
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/documentation.yml around lines 105 - 108, The CI step
named "Test demo scripts" is masking failures by appending "|| echo 'Demo script
needs environment setup'" to the command; remove the "|| echo ..." so the
command is simply "python examples/demo_standalone.py" (allowing non-zero exits
to fail the step) or alternatively set an explicit continue-on-error flag if you
truly want to allow failures—update the run command in that step (the line
containing "python examples/demo_standalone.py || echo ...") to stop swallowing
errors so Validate Examples correctly fails on script errors.

Comment on lines +5 to +6
### V1 Core (Locked Cognition Loop)
The V1 core is the autonomous decision-making pipeline that is protected by repository policy and regression gates:
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Complete the terminology migration to avoid contradictory guarantees.

After switching to “Locked” at Lines 5–6/36/143, the file still uses “immutability guarantees” at Line 192. Please align that wording too so the architecture contract reads consistently.

📝 Suggested doc follow-up
- - Protected by immutability guarantees
+ - Protected by repository policy and regression gates

Also applies to: 36-36, 143-143

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/ARCHITECTURE.md` around lines 5 - 6, The doc mixes the term "Locked" in
the header "V1 Core (Locked Cognition Loop)" with the older phrase "immutability
guarantees"; update the occurrence(s) of "immutability guarantees" to use the
consistent term "Locked Cognition Loop" (or "Locked" where brevity is used) so
the architecture contract uses the same terminology throughout—search for the
phrase "immutability guarantees" and replace it with "Locked Cognition Loop"
(matching the header casing) across the file.

Comment on lines +308 to +316
repo_root = Path(__file__).resolve().parents[2]

result = subprocess.run(cmd, cwd=repo_root, env=env)
if scenario == 'standalone' and replay is None:
script_path = repo_root / "examples" / "demo_standalone.py"
result = subprocess.run(
[sys.executable, str(script_path)],
cwd=repo_root,
env=_script_env(os.environ.copy()),
)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Potential crash when demo is run from installed package without repo checkout.

The demo command uses repo_root = Path(__file__).resolve().parents[2] directly without checking _discover_repo_root(). When run from an installed package (no repo checkout), accessing examples/demo_standalone.py or scripts/demo_v2_restrained_autonomy.py will fail with a confusing error.

Consider using _discover_repo_root() and providing a clear error message when the demo cannot be run from an installed package.

🛠️ Proposed fix
 def demo(scenario: str, operator_decision: str, replay: Optional[str]):
     """Run demonstration scenarios"""
     click.echo(f"🚀 ExoArmur Demo: {scenario}")
-    repo_root = Path(__file__).resolve().parents[2]
+    repo_root = _discover_repo_root()
+    if repo_root is None:
+        click.echo("❌ Demo scenarios require a repository checkout and are not available in the installed package.")
+        sys.exit(1)

     if scenario == 'standalone' and replay is None:
         script_path = repo_root / "examples" / "demo_standalone.py"
🧰 Tools
🪛 Ruff (0.15.7)

[error] 312-312: subprocess call: check for execution of untrusted input

(S603)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/exoarmur/cli.py` around lines 308 - 316, The code assumes repo checkout
by computing repo_root = Path(__file__).resolve().parents[2] and then directly
launching examples (used in the scenario == 'standalone' && replay is None
branch), which will crash for installed packages; modify the branch to call and
use _discover_repo_root() to locate the repo root (falling back to a clear,
user-friendly error if not found), then build script_path from that discovered
root (e.g., examples/demo_standalone.py and
scripts/demo_v2_restrained_autonomy.py) and call subprocess.run as before;
ensure you reference and replace usages of repo_root and script_path in this
logic and emit a helpful message when _discover_repo_root() returns None or the
target script file doesn't exist.

Comment on lines +132 to +134
raw_timestamp = data.get('recorded_at') or data.get('timestamp', '')
self.recorded_at = datetime.fromisoformat(raw_timestamp.replace('Z', '+00:00'))
self.timestamp = self.recorded_at
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Add explicit guard for missing timestamp fields before parsing.

At Lines 132–134, an empty/missing recorded_at+timestamp still fails via generic exception flow and drops the record with limited context. Validate upfront and raise a targeted error message including event_id.

🛠️ Suggested hardening
-                    raw_timestamp = data.get('recorded_at') or data.get('timestamp', '')
-                    self.recorded_at = datetime.fromisoformat(raw_timestamp.replace('Z', '+00:00'))
+                    raw_timestamp = data.get('recorded_at') or data.get('timestamp')
+                    if not raw_timestamp:
+                        raise ValueError(
+                            f"Missing recorded_at/timestamp for event_id={data.get('event_id', '<unknown>')}"
+                        )
+                    self.recorded_at = datetime.fromisoformat(str(raw_timestamp).replace('Z', '+00:00'))
                     self.timestamp = self.recorded_at
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
raw_timestamp = data.get('recorded_at') or data.get('timestamp', '')
self.recorded_at = datetime.fromisoformat(raw_timestamp.replace('Z', '+00:00'))
self.timestamp = self.recorded_at
raw_timestamp = data.get('recorded_at') or data.get('timestamp')
if not raw_timestamp:
raise ValueError(
f"Missing recorded_at/timestamp for event_id={data.get('event_id', '<unknown>')}"
)
self.recorded_at = datetime.fromisoformat(str(raw_timestamp).replace('Z', '+00:00'))
self.timestamp = self.recorded_at
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/exoarmur/replay/cli.py` around lines 132 - 134, The parsing of timestamps
in the replay CLI uses raw_timestamp = data.get('recorded_at') or
data.get('timestamp', '') then datetime.fromisoformat(...) in the block that
sets self.recorded_at and self.timestamp; add an explicit guard that checks if
raw_timestamp is truthy before calling datetime.fromisoformat, and if not raise
a ValueError (or custom error) that includes the event_id from data (e.g.,
data.get('event_id')) so the caller/logger gets a clear, targeted message about
the missing timestamp instead of a generic exception and dropped record.

Comment on lines +187 to +193
def default_expected_python_version() -> str:
"""Return the exact Python version the primary invariant gate expects.

This defaults to the version currently used by the core invariant workflow.
"""

return sys.version.split()[0]
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

default_expected_python_version() returns the current runtime version, not a fixed CI expectation.

The function uses sys.version.split()[0] which returns whatever Python version is currently running. This means the "expected" version will always match the "actual" version in validate_environment_parity, making the Python version check a no-op when using this default.

If the intent is to validate against a fixed CI Python version (e.g., 3.11.8), this should return a constant rather than the runtime version. If the intent is to allow any Python version when no explicit version is specified, the docstring should clarify this.

💡 Consider clarifying intent or using a fixed version

If CI expects a specific version:

 def default_expected_python_version() -> str:
     """Return the exact Python version the primary invariant gate expects."""
-    return sys.version.split()[0]
+    return "3.11.8"  # Or read from a config file

Or if dynamic matching is intentional, update the docstring:

 def default_expected_python_version() -> str:
-    """Return the exact Python version the primary invariant gate expects.
-
-    This defaults to the version currently used by the core invariant workflow.
-    """
+    """Return the current Python version.
+
+    When no explicit version is provided, parity validation uses the current
+    runtime version, effectively skipping the Python version check.
+    """
     return sys.version.split()[0]
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def default_expected_python_version() -> str:
"""Return the exact Python version the primary invariant gate expects.
This defaults to the version currently used by the core invariant workflow.
"""
return sys.version.split()[0]
def default_expected_python_version() -> str:
"""Return the current Python version.
When no explicit version is provided, parity validation uses the current
runtime version, effectively skipping the Python version check.
"""
return sys.version.split()[0]
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/exoarmur/stability/env_parity.py` around lines 187 - 193, The function
default_expected_python_version() currently returns the runtime Python
(sys.version.split()[0]), making validate_environment_parity's Python check a
no-op; either change default_expected_python_version() to return the fixed
CI/expected Python string (e.g., "3.11.8") so the validator compares against a
real expected version, or if the intent is to treat the default as "no
expectation", update the function docstring to state it intentionally allows any
runtime Python and leave the validator logic as-is; locate and update the
default_expected_python_version function and any docs referencing it to reflect
the chosen behavior.

Comment on lines +8 to +12
def test_ensure_default_event_loop_policy_installs_default_policy():
snapshot = ensure_default_event_loop_policy()

assert snapshot.class_name == current_event_loop_policy_snapshot().class_name
assert is_explicit_default_event_loop_policy() is True
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify that this test mutates event-loop policy and does not restore it.
rg -n -C2 'ensure_default_event_loop_policy\(|set_event_loop_policy\(' tests/test_event_loop_policy.py
# Expected:
# - A call to ensure_default_event_loop_policy(...)
# - No restoration call to asyncio.set_event_loop_policy(...)

Repository: slucerodev/ExoArmur-Core

Length of output: 282


🏁 Script executed:

cat -n tests/test_event_loop_policy.py

Repository: slucerodev/ExoArmur-Core

Length of output: 585


🏁 Script executed:

find . -name asyncio_policy.py -type f

Repository: slucerodev/ExoArmur-Core

Length of output: 110


🏁 Script executed:

cat -n ./src/exoarmur/stability/asyncio_policy.py

Repository: slucerodev/ExoArmur-Core

Length of output: 2229


Restore event-loop policy after the test to prevent cross-test contamination.

Line 9 calls ensure_default_event_loop_policy(), which mutates the global asyncio policy via asyncio.set_event_loop_policy() but never restores it. This can make subsequent tests order-dependent.

Proposed fix
+import asyncio
+
 from exoarmur.stability.asyncio_policy import (
     current_event_loop_policy_snapshot,
     ensure_default_event_loop_policy,
     is_explicit_default_event_loop_policy,
 )
 
 
 def test_ensure_default_event_loop_policy_installs_default_policy():
-    snapshot = ensure_default_event_loop_policy()
-
-    assert snapshot.class_name == current_event_loop_policy_snapshot().class_name
-    assert is_explicit_default_event_loop_policy() is True
+    previous_policy = asyncio.get_event_loop_policy()
+    try:
+        snapshot = ensure_default_event_loop_policy()
+        assert snapshot.class_name == current_event_loop_policy_snapshot().class_name
+        assert is_explicit_default_event_loop_policy() is True
+    finally:
+        asyncio.set_event_loop_policy(previous_policy)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def test_ensure_default_event_loop_policy_installs_default_policy():
snapshot = ensure_default_event_loop_policy()
assert snapshot.class_name == current_event_loop_policy_snapshot().class_name
assert is_explicit_default_event_loop_policy() is True
import asyncio
from exoarmur.stability.asyncio_policy import (
current_event_loop_policy_snapshot,
ensure_default_event_loop_policy,
is_explicit_default_event_loop_policy,
)
def test_ensure_default_event_loop_policy_installs_default_policy():
previous_policy = asyncio.get_event_loop_policy()
try:
snapshot = ensure_default_event_loop_policy()
assert snapshot.class_name == current_event_loop_policy_snapshot().class_name
assert is_explicit_default_event_loop_policy() is True
finally:
asyncio.set_event_loop_policy(previous_policy)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/test_event_loop_policy.py` around lines 8 - 12, The test
test_ensure_default_event_loop_policy_installs_default_policy calls
ensure_default_event_loop_policy() which mutates the global asyncio policy and
doesn't restore it; update the test to capture the current policy snapshot via
current_event_loop_policy_snapshot() before calling
ensure_default_event_loop_policy(), run the assertions, then restore the
original policy (e.g., via asyncio.set_event_loop_policy(...) or the test helper
that re-applies the snapshot) so is_explicit_default_event_loop_policy() and
other tests are not affected; ensure you reference the same snapshot object you
captured and restore it in a finally/teardown-style block to guarantee
restoration on failures.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

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

⚠️ Outside diff range comments (1)
.github/workflows/multi-platform-tests.yml (1)

117-125: ⚠️ Potential issue | 🟠 Major

Test the built wheel before switching back to editable mode.

The package-install step only proves import exoarmur works. The first CLI smoke test is after Line 124 reinstalls -e ., so a broken wheel console-script entry point or installed-package path can still pass this workflow.

📦 Suggested adjustment
     - name: Test package installation
       run: |
         python -m pip install dist/*.whl
+        exoarmur --help
         python -c "import exoarmur; print(f'ExoArmur version: {exoarmur.__version__}')"

     - name: Test editable installation
       run: |
         python -m pip install --no-deps -e .
         exoarmur --help
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/multi-platform-tests.yml around lines 117 - 125, The
workflow currently imports the package after installing the wheel but only runs
the CLI smoke test after reinstalling editable (-e .), so a broken wheel entry
point can be missed; modify the "Test package installation" step (and/or add a
new step immediately after it) to run the console-script smoke test (invoke the
installed CLI, e.g., run the same exoarmur --help or python -m exoarmur --help)
before the "Test editable installation" step to validate the wheel's entry point
and installed-package path.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In @.github/workflows/multi-platform-tests.yml:
- Line 91: The workflow smoke test currently assigns the return of
PluginRegistry.discover_providers() to providers but discover_providers()
returns None; instead call registry.discover_providers() for its side effects
and then read the discovered list from the registry instance (e.g.,
registry.providers or registry.get_providers() if available). Update the command
to instantiate PluginRegistry, invoke registry.discover_providers(), then
compute len(registry.providers) (or len(registry.get_providers())) and print
that so the job actually validates plugin discovery; reference
PluginRegistry.discover_providers and the registry.providers/get_providers
accessors when making the change.

---

Outside diff comments:
In @.github/workflows/multi-platform-tests.yml:
- Around line 117-125: The workflow currently imports the package after
installing the wheel but only runs the CLI smoke test after reinstalling
editable (-e .), so a broken wheel entry point can be missed; modify the "Test
package installation" step (and/or add a new step immediately after it) to run
the console-script smoke test (invoke the installed CLI, e.g., run the same
exoarmur --help or python -m exoarmur --help) before the "Test editable
installation" step to validate the wheel's entry point and installed-package
path.
🪄 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: defaults

Review profile: CHILL

Plan: Pro

Run ID: b3635d59-c237-41e8-b7fa-6f0d1d520760

📥 Commits

Reviewing files that changed from the base of the PR and between fe276d6 and 5bc4fd5.

⛔ Files ignored due to path filters (1)
  • requirements.lock is excluded by !**/*.lock
📒 Files selected for processing (3)
  • .github/workflows/multi-platform-tests.yml
  • .github/workflows/observability.yml
  • pyproject.toml
🚧 Files skipped from review as they are similar to previous changes (1)
  • pyproject.toml

- name: Run performance benchmarks
run: |
pytest -q tests/test_performance.py --benchmark-only || echo "Performance tests not yet implemented"
python -c "from exoarmur.plugins.registry import PluginRegistry; registry = PluginRegistry(); providers = registry.discover_providers(); print(f'Plugin discovery works: {len(providers)} providers found')"
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Fix the plugin discovery smoke test.

Line 91 assigns the result of discover_providers() to providers, but PluginRegistry.discover_providers() in src/exoarmur/plugins/registry.py:36-70 returns None. len(providers) will raise immediately, so this job never validates discovery at all.

🐛 Suggested fix
-        python -c "from exoarmur.plugins.registry import PluginRegistry; registry = PluginRegistry(); providers = registry.discover_providers(); print(f'Plugin discovery works: {len(providers)} providers found')"
+        python -c "from exoarmur.plugins.registry import PluginRegistry; registry = PluginRegistry(); registry.discover_providers(); print('Plugin discovery works')"
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
python -c "from exoarmur.plugins.registry import PluginRegistry; registry = PluginRegistry(); providers = registry.discover_providers(); print(f'Plugin discovery works: {len(providers)} providers found')"
python -c "from exoarmur.plugins.registry import PluginRegistry; registry = PluginRegistry(); registry.discover_providers(); print('Plugin discovery works')"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/multi-platform-tests.yml at line 91, The workflow smoke
test currently assigns the return of PluginRegistry.discover_providers() to
providers but discover_providers() returns None; instead call
registry.discover_providers() for its side effects and then read the discovered
list from the registry instance (e.g., registry.providers or
registry.get_providers() if available). Update the command to instantiate
PluginRegistry, invoke registry.discover_providers(), then compute
len(registry.providers) (or len(registry.get_providers())) and print that so the
job actually validates plugin discovery; reference
PluginRegistry.discover_providers and the registry.providers/get_providers
accessors when making the change.

- add multi-node execution agreement verification
- add fault injection testing for execution integrity
- reinforce canonical hashing and replay guarantees
- expand invariant test coverage

Files:
- src/exoarmur/replay/byzantine_fault_injection.py
- src/exoarmur/replay/multi_node_verifier.py
- src/exoarmur/replay/__init__.py
- src/exoarmur/replay/canonical_utils.py
- src/exoarmur/replay/cli.py
- src/exoarmur/replay/event_envelope.py
- src/exoarmur/replay/replay_engine.py
- tests/test_invariants.py
- tests/test_replay_engine.py
- tests/test_multi_node_verifier.py
- tests/test_byzantine_fault_injection.py
- tests/artifacts/demo_byzantine_results.json
- tests/artifacts/demo_canonical_events.json
- tests/artifacts/demo_multi_node_hashes.json
- tests/artifacts/demo_replay_output.json
- tests/artifacts/golden_manifest.json
…eline

- feature-flagged experimental execution layer
- approval and dispatch pipeline scaffolding
- strict isolation from deterministic core

Files:
- src/exoarmur/demo_v2_restrained_autonomy.py (deleted from core)
- production agent drift reproduction harness
- validation and observability scripts
- demo and UI examples for execution verification
- script layer organized into demo/validation/infra/experiments
- infrastructure updates for stability and deployment

Files added: 46 new files, 7 modified files
- scripts/demo/ (6 files)
- scripts/validation/ (5 files)
- scripts/infra/ (9 files)
- scripts/experiments/ (12 files)
- examples/ (4 UI files)
- Docker and deployment updates
"""Serve a static file"""
file_path = Path(__file__).parent / filename

if not file_path.exists():

Check failure

Code scanning / CodeQL

Uncontrolled data used in path expression High

This path depends on a
user-provided value
.

Copilot Autofix

AI about 3 hours ago

To fix this, the static file serving logic must ensure that any user-controlled path is constrained to a specific, intended directory. The standard pattern is: (1) define a root directory for static files, (2) join the user-provided path to that root, (3) normalize the result (e.g., with Path.resolve() or os.path.normpath), and (4) verify that the normalized path is still within the root directory before opening it. If the check fails, respond with an error (404 / 403) instead of serving the file.

In this code, the best minimal fix is to adjust serve_file to treat Path(__file__).parent as the static root, resolve both the root and the candidate path to absolute, normalized paths, and then ensure the candidate path is a child of the root. We should not change the external behavior for valid in-root paths; we only add a safety check and slightly improve robustness. Concretely:

  • In serve_file, compute base_dir = Path(__file__).parent.resolve() once and then file_path = (base_dir / filename).resolve().
  • Before using file_path, verify that it is contained within base_dir. In Python 3.9+, Path.is_relative_to is the simplest; if that may not be available, a fallback could compare str(file_path).startswith(str(base_dir)), but using is_relative_to is more robust and explicit.
  • If the containment check fails, return a 403 (or 404) and do not attempt to open the file.
  • Keep the rest of the logic (existence check, reading, headers) as-is.

All required tools (pathlib.Path) are already imported at the top of scripts/ai_agent_verification_server.py, so no new imports or helper methods are needed. All edits are confined to the serve_file method around the construction and use of file_path.

Suggested changeset 1
scripts/ai_agent_verification_server.py

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/scripts/ai_agent_verification_server.py b/scripts/ai_agent_verification_server.py
--- a/scripts/ai_agent_verification_server.py
+++ b/scripts/ai_agent_verification_server.py
@@ -69,8 +69,23 @@
     
     def serve_file(self, filename, content_type):
         """Serve a static file"""
-        file_path = Path(__file__).parent / filename
+        base_dir = Path(__file__).parent.resolve()
+        file_path = (base_dir / filename).resolve()
         
+        # Prevent directory traversal: ensure requested path is within base_dir
+        try:
+            # Path.is_relative_to is available in Python 3.9+
+            if not file_path.is_relative_to(base_dir):
+                self.send_error(403, "Access to the requested file is forbidden")
+                return
+        except AttributeError:
+            # Fallback for older Python versions
+            base_dir_str = str(base_dir)
+            file_path_str = str(file_path)
+            if not (file_path_str == base_dir_str or file_path_str.startswith(base_dir_str + os.sep)):
+                self.send_error(403, "Access to the requested file is forbidden")
+                return
+        
         if not file_path.exists():
             self.send_error(404, f"File not found: {filename}")
             return
EOF
@@ -69,8 +69,23 @@

def serve_file(self, filename, content_type):
"""Serve a static file"""
file_path = Path(__file__).parent / filename
base_dir = Path(__file__).parent.resolve()
file_path = (base_dir / filename).resolve()

# Prevent directory traversal: ensure requested path is within base_dir
try:
# Path.is_relative_to is available in Python 3.9+
if not file_path.is_relative_to(base_dir):
self.send_error(403, "Access to the requested file is forbidden")
return
except AttributeError:
# Fallback for older Python versions
base_dir_str = str(base_dir)
file_path_str = str(file_path)
if not (file_path_str == base_dir_str or file_path_str.startswith(base_dir_str + os.sep)):
self.send_error(403, "Access to the requested file is forbidden")
return

if not file_path.exists():
self.send_error(404, f"File not found: {filename}")
return
Copilot is powered by AI and may make mistakes. Always verify output.
return

try:
with open(file_path, 'rb') as f:

Check failure

Code scanning / CodeQL

Uncontrolled data used in path expression High

This path depends on a
user-provided value
.

Copilot Autofix

AI about 3 hours ago

In general, paths derived from HTTP request URLs must be normalized and restricted to a specific static root directory, ensuring that clients cannot escape that directory using .. segments or absolute paths. The standard pattern in Python is to (a) define a static root, (b) build the candidate path under that root, (c) normalize/resolve it, and (d) verify that it is still within the root before opening it.

For this code, the minimal, non-breaking fix is:

  • Define a static root directory, e.g. the directory containing this script (or a static subdirectory if desired).
  • In serve_file, instead of simply doing Path(__file__).parent / filename, build the path under the root: root / filename.
  • Normalize that path using .resolve() (or os.path.realpath / os.path.normpath).
  • Check that the normalized path is still inside the root by using Path.is_relative_to (Python 3.9+) or an equivalent relative_to-with-try/except pattern.
  • If the path is outside the root, return a 403/404 rather than opening it.

All changes are confined to scripts/ai_agent_verification_server.py, specifically in the serve_file method. No new imports are needed because pathlib.Path is already imported.

Concretely:

  • Modify serve_file to:
    • Set root_dir = Path(__file__).parent.
    • Compute file_path = (root_dir / filename).resolve().
    • Reject the request if file_path is not within root_dir.
  • Keep the rest of the method (existence check, opening and sending the file) unchanged.
Suggested changeset 1
scripts/ai_agent_verification_server.py

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/scripts/ai_agent_verification_server.py b/scripts/ai_agent_verification_server.py
--- a/scripts/ai_agent_verification_server.py
+++ b/scripts/ai_agent_verification_server.py
@@ -69,7 +69,16 @@
     
     def serve_file(self, filename, content_type):
         """Serve a static file"""
-        file_path = Path(__file__).parent / filename
+        # Restrict served files to the directory containing this script
+        root_dir = Path(__file__).parent.resolve()
+        file_path = (root_dir / filename).resolve()
+
+        # Prevent directory traversal: ensure file_path is within root_dir
+        try:
+            file_path.relative_to(root_dir)
+        except ValueError:
+            self.send_error(403, "Access to the requested file is forbidden")
+            return
         
         if not file_path.exists():
             self.send_error(404, f"File not found: {filename}")
EOF
@@ -69,7 +69,16 @@

def serve_file(self, filename, content_type):
"""Serve a static file"""
file_path = Path(__file__).parent / filename
# Restrict served files to the directory containing this script
root_dir = Path(__file__).parent.resolve()
file_path = (root_dir / filename).resolve()

# Prevent directory traversal: ensure file_path is within root_dir
try:
file_path.relative_to(root_dir)
except ValueError:
self.send_error(403, "Access to the requested file is forbidden")
return

if not file_path.exists():
self.send_error(404, f"File not found: {filename}")
Copilot is powered by AI and may make mistakes. Always verify output.
"""Serve a static file"""
file_path = Path(__file__).parent / filename

if not file_path.exists():

Check failure

Code scanning / CodeQL

Uncontrolled data used in path expression High

This path depends on a
user-provided value
.

Copilot Autofix

AI about 3 hours ago

In general terms, the fix is to ensure that any path derived from user input is normalized and constrained to a known safe directory before it is used to access the filesystem. That means: (1) define a static document root for static files, (2) resolve or normalize filename against that root, (3) verify the resolved path is still under the root (preventing .. or absolute-path escape), and (4) reject or 404 any request that fails the check.

For this specific code, the best fix with minimal behavior change is to update serve_file to:

  • Define a static_root = Path(__file__).parent (or a subdirectory if desired).
  • Compute safe_path = (static_root / filename).resolve() so path segments like .. are removed and symlinks are resolved.
  • Check safe_path.is_file() and verify that static_root is a parent of safe_path using safe_path.is_relative_to(static_root) on Python 3.9+ or a manual prefix check on safe_path.parts.
  • If the check fails, return a 404 (or 403) instead of opening the file.
    This change is localized to serve_file in scripts/demo/demo_web_server.py and doesn’t alter how other parts of the server call it. No new external libraries are needed; we only use pathlib which is already imported.
Suggested changeset 1
scripts/demo/demo_web_server.py

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/scripts/demo/demo_web_server.py b/scripts/demo/demo_web_server.py
--- a/scripts/demo/demo_web_server.py
+++ b/scripts/demo/demo_web_server.py
@@ -70,22 +70,30 @@
     
     def serve_file(self, filename, content_type):
         """Serve a static file"""
-        file_path = Path(__file__).parent / filename
-        
-        if not file_path.exists():
-            self.send_error(404, f"File not found: {filename}")
-            return
-        
+        static_root = Path(__file__).parent
         try:
+            # Resolve the requested path against the static root to prevent directory traversal
+            file_path = (static_root / filename).resolve()
+            # Ensure the resolved path is within the static root
+            try:
+                file_path.relative_to(static_root)
+            except ValueError:
+                self.send_error(404, f"File not found: {filename}")
+                return
+
+            if not file_path.is_file():
+                self.send_error(404, f"File not found: {filename}")
+                return
+
             with open(file_path, 'rb') as f:
                 content = f.read()
-            
+
             self.send_response(200)
             self.send_header('Content-type', content_type)
             self.send_header('Content-length', str(len(content)))
             self.end_headers()
             self.wfile.write(content)
-            
+
         except Exception as e:
             self.send_error(500, f"Error serving file: {e}")
     
EOF
@@ -70,22 +70,30 @@

def serve_file(self, filename, content_type):
"""Serve a static file"""
file_path = Path(__file__).parent / filename

if not file_path.exists():
self.send_error(404, f"File not found: {filename}")
return

static_root = Path(__file__).parent
try:
# Resolve the requested path against the static root to prevent directory traversal
file_path = (static_root / filename).resolve()
# Ensure the resolved path is within the static root
try:
file_path.relative_to(static_root)
except ValueError:
self.send_error(404, f"File not found: {filename}")
return

if not file_path.is_file():
self.send_error(404, f"File not found: {filename}")
return

with open(file_path, 'rb') as f:
content = f.read()

self.send_response(200)
self.send_header('Content-type', content_type)
self.send_header('Content-length', str(len(content)))
self.end_headers()
self.wfile.write(content)

except Exception as e:
self.send_error(500, f"Error serving file: {e}")

Copilot is powered by AI and may make mistakes. Always verify output.
return

try:
with open(file_path, 'rb') as f:

Check failure

Code scanning / CodeQL

Uncontrolled data used in path expression High

This path depends on a
user-provided value
.

Copilot Autofix

AI about 3 hours ago

In general, paths built from untrusted input must be normalized and checked to ensure they stay within a designated safe root directory before use. Here, we should treat the directory containing demo_web_server.py as the static root (or a subdirectory of it), normalize the combined path using Path.resolve() or os.path.normpath, and then verify that the resulting path is still inside that root. If the check fails, we should return a 403/404 instead of opening the file.

The best minimal-impact fix is to harden serve_file so that it (1) defines a base_dir = Path(__file__).parent.resolve(), (2) resolves the requested filename against this base using file_path = (base_dir / filename).resolve(), and (3) checks if not str(file_path).startswith(str(base_dir) + os.sep) (or, more robustly, compares path components using Path.relative_to). If file_path is not within base_dir, the method should send an error response and return without opening the file. This preserves existing behavior for legitimate relative paths like '../demo_ui.html' that still resolve under the script directory, but blocks attempts to escape outside that directory. The only code to change is inside serve_file in scripts/demo/demo_web_server.py; we can reuse the existing os and Path imports, so no new imports or helpers are required.

Suggested changeset 1
scripts/demo/demo_web_server.py

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/scripts/demo/demo_web_server.py b/scripts/demo/demo_web_server.py
--- a/scripts/demo/demo_web_server.py
+++ b/scripts/demo/demo_web_server.py
@@ -70,8 +70,20 @@
     
     def serve_file(self, filename, content_type):
         """Serve a static file"""
-        file_path = Path(__file__).parent / filename
-        
+        base_dir = Path(__file__).parent.resolve()
+        try:
+            file_path = (base_dir / filename).resolve()
+        except Exception:
+            self.send_error(400, "Invalid file path")
+            return
+
+        # Ensure the resolved path is within the base directory
+        try:
+            file_path.relative_to(base_dir)
+        except ValueError:
+            self.send_error(403, "Access to the requested path is forbidden")
+            return
+
         if not file_path.exists():
             self.send_error(404, f"File not found: {filename}")
             return
EOF
@@ -70,8 +70,20 @@

def serve_file(self, filename, content_type):
"""Serve a static file"""
file_path = Path(__file__).parent / filename

base_dir = Path(__file__).parent.resolve()
try:
file_path = (base_dir / filename).resolve()
except Exception:
self.send_error(400, "Invalid file path")
return

# Ensure the resolved path is within the base directory
try:
file_path.relative_to(base_dir)
except ValueError:
self.send_error(403, "Access to the requested path is forbidden")
return

if not file_path.exists():
self.send_error(404, f"File not found: {filename}")
return
Copilot is powered by AI and may make mistakes. Always verify output.
"""Serve a static file"""
file_path = Path(__file__).parent / filename

if not file_path.exists():

Check failure

Code scanning / CodeQL

Uncontrolled data used in path expression High

This path depends on a
user-provided value
.

Copilot Autofix

AI about 3 hours ago

In general, to fix this kind of issue you must ensure that any path derived from user input is constrained to a safe directory. This usually means: (1) join the user-supplied segment with a fixed base directory, (2) normalize the result (e.g., resolve ..), and (3) verify that the normalized path is still within the base directory. If this check fails, return an error instead of accessing the file.

For this code, the best fix with minimal functional change is to harden serve_file so it treats Path(__file__).parent as the root of all static files and rejects any request that would escape that directory. We can do this by:

  • Computing base_dir = Path(__file__).parent.resolve().
  • Joining with the requested filename and resolving: resolved_path = (base_dir / filename).resolve().
  • Checking resolved_path.is_file() and that it is within base_dir using resolved_path.is_relative_to(base_dir) (Python 3.9+) or a prefix check on parts; since we should not assume features beyond standard library but we can assume a reasonably recent Python, using is_relative_to keeps the change minimal and clear.
  • If the check fails, respond with 404 or 403 instead of opening the file.

We only need to modify the serve_file method in scripts/demo_web_server.py. No new imports are necessary because Path is already imported from pathlib. All behavior for safe in-directory paths remains the same; only attempts to traverse outside the base directory will be blocked.


Suggested changeset 1
scripts/demo_web_server.py

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/scripts/demo_web_server.py b/scripts/demo_web_server.py
--- a/scripts/demo_web_server.py
+++ b/scripts/demo_web_server.py
@@ -70,13 +70,15 @@
     
     def serve_file(self, filename, content_type):
         """Serve a static file"""
-        file_path = Path(__file__).parent / filename
-        
-        if not file_path.exists():
-            self.send_error(404, f"File not found: {filename}")
-            return
-        
+        base_dir = Path(__file__).parent.resolve()
         try:
+            # Resolve the requested path relative to the base directory
+            file_path = (base_dir / filename).resolve()
+            # Ensure the resolved path is within the base directory
+            if not file_path.is_file() or not file_path.is_relative_to(base_dir):
+                self.send_error(404, f"File not found: {filename}")
+                return
+            
             with open(file_path, 'rb') as f:
                 content = f.read()
             
EOF
@@ -70,13 +70,15 @@

def serve_file(self, filename, content_type):
"""Serve a static file"""
file_path = Path(__file__).parent / filename

if not file_path.exists():
self.send_error(404, f"File not found: {filename}")
return

base_dir = Path(__file__).parent.resolve()
try:
# Resolve the requested path relative to the base directory
file_path = (base_dir / filename).resolve()
# Ensure the resolved path is within the base directory
if not file_path.is_file() or not file_path.is_relative_to(base_dir):
self.send_error(404, f"File not found: {filename}")
return

with open(file_path, 'rb') as f:
content = f.read()

Copilot is powered by AI and may make mistakes. Always verify output.
return

try:
with open(file_path, 'rb') as f:

Check failure

Code scanning / CodeQL

Uncontrolled data used in path expression High

This path depends on a
user-provided value
.

Copilot Autofix

AI about 3 hours ago

In general, to fix uncontrolled path usage, you must constrain user-controlled paths to a safe directory. This is typically done by (1) choosing a static root directory, (2) normalizing the combined path (os.path.normpath or Path.resolve()), and (3) verifying that the normalized path is still within the root directory before opening the file. Any path that would escape the root must be rejected.

For this code, the best fix with minimal behavior change is to treat Path(__file__).parent as the static document root and ensure all requested files remain within this directory. We can do this entirely inside serve_file, which is already the central helper for serving files. The steps are:

  • Compute a fixed base_dir = Path(__file__).parent.resolve().
  • Build the requested path as file_path = (base_dir / filename).resolve() to normalize and eliminate .. segments.
  • Check if not str(file_path).startswith(str(base_dir) + os.sep) (or the equivalent using Path logic). If the check fails, return a 403 error (or 404) and do not access the file.
  • Proceed with the existing existence check and file reading only if the path is within base_dir.

This change is localized to serve_file in scripts/external_validation_server.py around lines 67–76. No new external libraries are needed; pathlib.Path and os are already imported.

Suggested changeset 1
scripts/external_validation_server.py

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/scripts/external_validation_server.py b/scripts/external_validation_server.py
--- a/scripts/external_validation_server.py
+++ b/scripts/external_validation_server.py
@@ -66,7 +66,16 @@
     
     def serve_file(self, filename, content_type):
         """Serve a static file"""
-        file_path = Path(__file__).parent / filename
+        # Restrict served files to the directory containing this script
+        base_dir = Path(__file__).parent.resolve()
+        file_path = (base_dir / filename).resolve()
+
+        # Prevent directory traversal: ensure the resolved path is under base_dir
+        try:
+            file_path.relative_to(base_dir)
+        except ValueError:
+            self.send_error(403, "Access to the requested resource is forbidden")
+            return
         
         if not file_path.exists():
             self.send_error(404, f"File not found: {filename}")
EOF
@@ -66,7 +66,16 @@

def serve_file(self, filename, content_type):
"""Serve a static file"""
file_path = Path(__file__).parent / filename
# Restrict served files to the directory containing this script
base_dir = Path(__file__).parent.resolve()
file_path = (base_dir / filename).resolve()

# Prevent directory traversal: ensure the resolved path is under base_dir
try:
file_path.relative_to(base_dir)
except ValueError:
self.send_error(403, "Access to the requested resource is forbidden")
return

if not file_path.exists():
self.send_error(404, f"File not found: {filename}")
Copilot is powered by AI and may make mistakes. Always verify output.
"""Serve a static file"""
file_path = Path(__file__).parent / filename

if not file_path.exists():

Check failure

Code scanning / CodeQL

Uncontrolled data used in path expression High

This path depends on a
user-provided value
.

Copilot Autofix

AI about 3 hours ago

In general, to fix this issue we must ensure that any path derived from user input is constrained to a safe root directory before accessing the filesystem. This typically means (1) defining an explicit static root directory, (2) combining the root with the user-supplied component, (3) normalizing the result to remove .. and similar constructs, and (4) checking that the normalized path still resides within the static root. Requests that fail this check should return a 403 or 404 instead of reading the file.

For this code, the best minimal change is to treat Path(__file__).parent as the static root and restrict all serve_file access to files under that directory. We can do this by:

  • Building base_dir = Path(__file__).parent.resolve() once inside serve_file.
  • Building file_path = (base_dir / filename).resolve() instead of Path(__file__).parent / filename.
  • Verifying that file_path is within base_dir, using a safe prefix check such as os.path.commonpath([base_dir, file_path]) == str(base_dir) or file_path.is_relative_to(base_dir) (if Python ≥3.9 is guaranteed). Since we should not assume a specific Python minor version, using os.path.commonpath is safer.
  • If the check fails, respond with an error (for example, 403 “Forbidden” or 400 “Invalid path”) and do not open the file.

All changes are confined to the serve_file method in scripts/integrity_verification_server.py; we only need to adjust how file_path is computed and add the containment check. The existing imports already include os and Path, so no new imports are required.

Suggested changeset 1
scripts/integrity_verification_server.py

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/scripts/integrity_verification_server.py b/scripts/integrity_verification_server.py
--- a/scripts/integrity_verification_server.py
+++ b/scripts/integrity_verification_server.py
@@ -68,7 +68,13 @@
     
     def serve_file(self, filename, content_type):
         """Serve a static file"""
-        file_path = Path(__file__).parent / filename
+        base_dir = Path(__file__).parent.resolve()
+        file_path = (base_dir / filename).resolve()
+
+        # Ensure the requested file is within the base_dir to prevent path traversal
+        if os.path.commonpath([str(base_dir), str(file_path)]) != str(base_dir):
+            self.send_error(403, "Forbidden: invalid file path")
+            return
         
         if not file_path.exists():
             self.send_error(404, f"File not found: {filename}")
EOF
@@ -68,7 +68,13 @@

def serve_file(self, filename, content_type):
"""Serve a static file"""
file_path = Path(__file__).parent / filename
base_dir = Path(__file__).parent.resolve()
file_path = (base_dir / filename).resolve()

# Ensure the requested file is within the base_dir to prevent path traversal
if os.path.commonpath([str(base_dir), str(file_path)]) != str(base_dir):
self.send_error(403, "Forbidden: invalid file path")
return

if not file_path.exists():
self.send_error(404, f"File not found: {filename}")
Copilot is powered by AI and may make mistakes. Always verify output.
return

try:
with open(file_path, 'rb') as f:

Check failure

Code scanning / CodeQL

Uncontrolled data used in path expression High

This path depends on a
user-provided value
.

Copilot Autofix

AI about 3 hours ago

In general, paths derived from user input should be constrained to a fixed, known-safe directory and validated after normalization to prevent directory traversal or access to unexpected files. The usual pattern is:

  1. Define a static root directory for serving files.
  2. Normalize the combination of root + requested path (e.g., with Path.resolve() or os.path.realpath).
  3. Verify that the normalized path is still under the intended root (e.g., using is_relative_to on Python 3.9+, or an equivalent prefix check).
  4. Reject requests that fail this check with a 403 or 404.

For this code, the best minimal-impact fix is to make IntegrityVerificationHandler serve static files only from a dedicated directory (e.g., the directory containing integrity_verification_server.py) and enforce that all requested paths, after normalization, remain within that directory. We do not need to change how do_GET calls serve_file, only to harden serve_file’s implementation. Concretely:

  • Inside serve_file, compute a base_dir = Path(__file__).parent.resolve().
  • Build resolved_path = (base_dir / filename).resolve(); this will collapse .. segments and handle symlinks.
  • Verify resolved_path is inside base_dir. On Python < 3.9, we can implement this via resolved_path.relative_to(base_dir) in a try/except ValueError block.
  • If the check fails, return a 403 (or 404) without touching the filesystem.
  • Otherwise, continue as before, but using resolved_path (renamed from file_path) instead of the unvalidated join.

This keeps existing functionality (serving files alongside the script and the main UI file) while preventing path traversal outside that directory. All changes are limited to scripts/integrity_verification_server.py within the shown snippet, and require no new imports beyond pathlib.Path, which is already present.

Suggested changeset 1
scripts/integrity_verification_server.py

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/scripts/integrity_verification_server.py b/scripts/integrity_verification_server.py
--- a/scripts/integrity_verification_server.py
+++ b/scripts/integrity_verification_server.py
@@ -68,14 +68,24 @@
     
     def serve_file(self, filename, content_type):
         """Serve a static file"""
-        file_path = Path(__file__).parent / filename
+        base_dir = Path(__file__).parent.resolve()
+        try:
+            # Normalize the requested path and ensure it stays within base_dir
+            resolved_path = (base_dir / filename).resolve()
+            # Prevent directory traversal by enforcing base_dir as a prefix
+            resolved_path.relative_to(base_dir)
+        except (ValueError, RuntimeError):
+            # ValueError: resolved_path is not within base_dir
+            # RuntimeError: potential resolution issues (e.g., cycles)
+            self.send_error(403, "Access to the requested resource is not allowed")
+            return
         
-        if not file_path.exists():
+        if not resolved_path.exists():
             self.send_error(404, f"File not found: {filename}")
             return
         
         try:
-            with open(file_path, 'rb') as f:
+            with open(resolved_path, 'rb') as f:
                 content = f.read()
             
             self.send_response(200)
EOF
@@ -68,14 +68,24 @@

def serve_file(self, filename, content_type):
"""Serve a static file"""
file_path = Path(__file__).parent / filename
base_dir = Path(__file__).parent.resolve()
try:
# Normalize the requested path and ensure it stays within base_dir
resolved_path = (base_dir / filename).resolve()
# Prevent directory traversal by enforcing base_dir as a prefix
resolved_path.relative_to(base_dir)
except (ValueError, RuntimeError):
# ValueError: resolved_path is not within base_dir
# RuntimeError: potential resolution issues (e.g., cycles)
self.send_error(403, "Access to the requested resource is not allowed")
return

if not file_path.exists():
if not resolved_path.exists():
self.send_error(404, f"File not found: {filename}")
return

try:
with open(file_path, 'rb') as f:
with open(resolved_path, 'rb') as f:
content = f.read()

self.send_response(200)
Copilot is powered by AI and may make mistakes. Always verify output.
"""Serve a static file"""
file_path = Path(__file__).parent / filename

if not file_path.exists():

Check failure

Code scanning / CodeQL

Uncontrolled data used in path expression High

This path depends on a
user-provided value
.

Copilot Autofix

AI about 3 hours ago

In general, the fix is to constrain any file path derived from self.path so it cannot escape a designated “web root” directory. We should normalize the joined path (for example with Path.resolve() or os.path.normpath) relative to a chosen base directory, then verify that the resulting path is indeed inside that base directory before opening it. If the check fails, return a 403/404 instead of serving the file.

For this file, the least intrusive fix is to update serve_file so it: (1) defines a base_dir = Path(__file__).parent.resolve(), (2) resolves the user-influenced filename against that base (e.g. (base_dir / filename).resolve()), and (3) checks file_path.is_file() and that base_dir is a parent of file_path (for example via file_path.is_relative_to(base_dir) on Python 3.9+, or by comparing path parts manually). If the resolved path is outside base_dir or not a file, we return 404. This preserves existing semantics (serving files from the script directory) but blocks directory traversal and access to files elsewhere on the filesystem. We only need to modify the serve_file method in scripts/validation/external_validation_server.py; no new imports are strictly needed since Path is already imported.


Suggested changeset 1
scripts/validation/external_validation_server.py

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/scripts/validation/external_validation_server.py b/scripts/validation/external_validation_server.py
--- a/scripts/validation/external_validation_server.py
+++ b/scripts/validation/external_validation_server.py
@@ -65,12 +65,22 @@
             self.send_error(404, "API endpoint not found")
     
     def serve_file(self, filename, content_type):
-        """Serve a static file"""
-        file_path = Path(__file__).parent / filename
-        
-        if not file_path.exists():
+        """Serve a static file safely within the server directory"""
+        base_dir = Path(__file__).parent.resolve()
+        try:
+            file_path = (base_dir / filename).resolve()
+        except Exception:
+            self.send_error(400, "Invalid file path")
+            return
+
+        # Ensure the resolved path is within the base directory to prevent traversal
+        if not str(file_path).startswith(str(base_dir) + os.sep):
             self.send_error(404, f"File not found: {filename}")
             return
+
+        if not file_path.is_file():
+            self.send_error(404, f"File not found: {filename}")
+            return
         
         try:
             with open(file_path, 'rb') as f:
EOF
@@ -65,12 +65,22 @@
self.send_error(404, "API endpoint not found")

def serve_file(self, filename, content_type):
"""Serve a static file"""
file_path = Path(__file__).parent / filename

if not file_path.exists():
"""Serve a static file safely within the server directory"""
base_dir = Path(__file__).parent.resolve()
try:
file_path = (base_dir / filename).resolve()
except Exception:
self.send_error(400, "Invalid file path")
return

# Ensure the resolved path is within the base directory to prevent traversal
if not str(file_path).startswith(str(base_dir) + os.sep):
self.send_error(404, f"File not found: {filename}")
return

if not file_path.is_file():
self.send_error(404, f"File not found: {filename}")
return

try:
with open(file_path, 'rb') as f:
Copilot is powered by AI and may make mistakes. Always verify output.
return

try:
with open(file_path, 'rb') as f:

Check failure

Code scanning / CodeQL

Uncontrolled data used in path expression High

This path depends on a
user-provided value
.

Copilot Autofix

AI about 3 hours ago

To fix the problem, we must ensure that any path derived from the HTTP request is validated so that it cannot escape a designated static directory. The general pattern is: define a static root directory, join the user-supplied relative path to that root, normalize the resulting path (for example with Path.resolve() or os.path.normpath), and then verify that the normalized path is still inside the static root. If that check fails, we should reject the request with a 403 or 400 error instead of serving the file.

For this file, the least invasive fix is to (1) define a STATIC_ROOT directory based on Path(__file__).parent, and (2) update serve_file so that it always resolves the requested path against STATIC_ROOT, normalizes it, and checks that file_path.is_relative_to(STATIC_ROOT) (Python 3.9+) or an equivalent prefix comparison for older versions. We should also adjust do_GET to avoid treating arbitrary paths as HTML by default and instead let serve_file determine content type from the suffix (for example by using mimetypes.guess_type) while keeping existing behavior for the root (/) unchanged. All these changes are local to scripts/validation/external_validation_server.py: add a STATIC_ROOT constant near the top, import mimetypes, and update serve_file and its caller so untrusted path components are properly constrained.

Suggested changeset 1
scripts/validation/external_validation_server.py

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/scripts/validation/external_validation_server.py b/scripts/validation/external_validation_server.py
--- a/scripts/validation/external_validation_server.py
+++ b/scripts/validation/external_validation_server.py
@@ -14,10 +14,14 @@
 from urllib.parse import urlparse, parse_qs
 import threading
 import webbrowser
+import mimetypes
 
 # Add src to path
 sys.path.insert(0, str(Path(__file__).parent.parent / "src"))
 
+# Root directory for serving static files
+STATIC_ROOT = Path(__file__).parent.resolve()
+
 from exoarmur.replay.replay_engine import ReplayEngine
 from exoarmur.replay.multi_node_verifier import MultiNodeReplayVerifier
 from exoarmur.replay.byzantine_fault_injection import (
@@ -48,8 +48,9 @@
         elif parsed_path.path == '/api/health':
             self.serve_health()
         else:
-            # Try to serve static files
-            self.serve_file(parsed_path.path.lstrip('/'), 'text/html')
+            # Try to serve static files rooted at STATIC_ROOT
+            requested_path = parsed_path.path.lstrip('/')
+            self.serve_file(requested_path)
     
     def do_POST(self):
         """Handle POST requests"""
@@ -64,14 +65,36 @@
         else:
             self.send_error(404, "API endpoint not found")
     
-    def serve_file(self, filename, content_type):
-        """Serve a static file"""
-        file_path = Path(__file__).parent / filename
+    def serve_file(self, filename, content_type=None):
+        """Serve a static file safely rooted at STATIC_ROOT."""
+        # Construct path relative to STATIC_ROOT and resolve to eliminate ".."
+        requested_path = STATIC_ROOT / filename
+        try:
+            file_path = requested_path.resolve()
+        except FileNotFoundError:
+            self.send_error(404, f"File not found: {filename}")
+            return
         
-        if not file_path.exists():
+        # Ensure the resolved path is within STATIC_ROOT to prevent traversal
+        try:
+            is_subpath = file_path.is_relative_to(STATIC_ROOT)
+        except AttributeError:
+            # For Python versions < 3.9, emulate is_relative_to
+            is_subpath = str(file_path).startswith(str(STATIC_ROOT) + os.sep) or file_path == STATIC_ROOT
+        
+        if not is_subpath:
+            self.send_error(403, "Access to the requested resource is forbidden")
+            return
+        
+        if not file_path.exists() or not file_path.is_file():
             self.send_error(404, f"File not found: {filename}")
             return
         
+        # Guess content type if not explicitly provided
+        if content_type is None:
+            guessed_type, _ = mimetypes.guess_type(str(file_path))
+            content_type = guessed_type or 'application/octet-stream'
+        
         try:
             with open(file_path, 'rb') as f:
                 content = f.read()
EOF
@@ -14,10 +14,14 @@
from urllib.parse import urlparse, parse_qs
import threading
import webbrowser
import mimetypes

# Add src to path
sys.path.insert(0, str(Path(__file__).parent.parent / "src"))

# Root directory for serving static files
STATIC_ROOT = Path(__file__).parent.resolve()

from exoarmur.replay.replay_engine import ReplayEngine
from exoarmur.replay.multi_node_verifier import MultiNodeReplayVerifier
from exoarmur.replay.byzantine_fault_injection import (
@@ -48,8 +48,9 @@
elif parsed_path.path == '/api/health':
self.serve_health()
else:
# Try to serve static files
self.serve_file(parsed_path.path.lstrip('/'), 'text/html')
# Try to serve static files rooted at STATIC_ROOT
requested_path = parsed_path.path.lstrip('/')
self.serve_file(requested_path)

def do_POST(self):
"""Handle POST requests"""
@@ -64,14 +65,36 @@
else:
self.send_error(404, "API endpoint not found")

def serve_file(self, filename, content_type):
"""Serve a static file"""
file_path = Path(__file__).parent / filename
def serve_file(self, filename, content_type=None):
"""Serve a static file safely rooted at STATIC_ROOT."""
# Construct path relative to STATIC_ROOT and resolve to eliminate ".."
requested_path = STATIC_ROOT / filename
try:
file_path = requested_path.resolve()
except FileNotFoundError:
self.send_error(404, f"File not found: {filename}")
return

if not file_path.exists():
# Ensure the resolved path is within STATIC_ROOT to prevent traversal
try:
is_subpath = file_path.is_relative_to(STATIC_ROOT)
except AttributeError:
# For Python versions < 3.9, emulate is_relative_to
is_subpath = str(file_path).startswith(str(STATIC_ROOT) + os.sep) or file_path == STATIC_ROOT

if not is_subpath:
self.send_error(403, "Access to the requested resource is forbidden")
return

if not file_path.exists() or not file_path.is_file():
self.send_error(404, f"File not found: {filename}")
return

# Guess content type if not explicitly provided
if content_type is None:
guessed_type, _ = mimetypes.guess_type(str(file_path))
content_type = guessed_type or 'application/octet-stream'

try:
with open(file_path, 'rb') as f:
content = f.read()
Copilot is powered by AI and may make mistakes. Always verify output.
@slucerodev slucerodev merged commit d3a6a01 into main Mar 31, 2026
14 of 24 checks passed
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