Fix verify-all packaging truth, OpenAPI snapshot, and Pydantic warnings#11
Conversation
There was a problem hiding this comment.
💡 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(), |
There was a problem hiding this comment.
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 👍 / 👎.
src/exoarmur/replay/replay_engine.py
Outdated
|
|
||
| correlation_id: str | ||
| replay_timestamp: datetime = field(default_factory=_deterministic_replay_timestamp) | ||
| replay_timestamp: datetime = field(default_factory=utc_now) |
There was a problem hiding this comment.
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 👍 / 👎.
|
Caution Review failedPull request was closed or merged during review 📝 WalkthroughWalkthroughThis 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
Estimated code review effort🎯 5 (Critical) | ⏱️ ~120+ minutes Possibly related PRs
Poem
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
|
There was a problem hiding this comment.
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 | 🟠 MajorReturn 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_recordswon'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
AuditRecordV1object with its originalrecorded_atvalue, 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 | 🟡 MinorUse 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 toexoarmur ...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 | 🟠 MajorExercise
verify-alloutside the checkout after the wheel install.This job still never covers the regression this PR fixes:
verify-allrunning from an installed package without repository-only assets nearby. Importingexoarmurfrom 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.mainto 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: Bareexceptclause could hide errors.The bare
except:on line 32 catches all exceptions includingKeyboardInterruptandSystemExit. 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
⛔ Files ignored due to path filters (1)
requirements.lockis 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.ymlCONTRIBUTING.mdOPEN_CORE_BOUNDARIES.mdREADME.mdREADME_v0.3.0.mdRELEASE_REPRODUCIBILITY.mdROADMAP.mdSECURITY.mdVALIDATE.mdartifacts/openapi_v1.jsonconftest.pydocs/ARCHITECTURE.mddocs/ARCHITECTURE_SIMPLE.mddocs/BUILD_AND_TEST.mddocs/CONSTITUTION.mddocs/CONTRACTS.mddocs/DESIGN_PRINCIPLES.mddocs/DEVELOPMENT_SETUP.mddocs/EXECUTION_GOVERNANCE_ARCHITECTURE.mddocs/EXOARMUR_SYSTEMS_PAPER.mddocs/GOVERNANCE.mddocs/HN_SHOW_POST_v0.2.0.mddocs/MODULE_SEPARATION.mddocs/Makefiledocs/ORGANISM_PRINCIPLES.mddocs/PHASE_STATUS.mddocs/README.mddocs/RELEASE_NOTES_v1.0.0-beta.mddocs/REPLAY_PROTOCOL.mddocs/REPO_STRUCTURE.mddocs/REVIEW_CHECKLIST.mddocs/RUNBOOK_V2_DEMO.mddocs/conf.pydocs/index.mdexamples/demo_standalone.pyexamples/demo_standalone_proof_bundle.jsonpyproject.tomlscripts/check_markdown_links.pyscripts/stability_ci.pyscripts/verify_env_parity.pysrc/exoarmur/audit/audit_logger.pysrc/exoarmur/cli.pysrc/exoarmur/execution_boundary_v2/approvals/approval_models.pysrc/exoarmur/execution_boundary_v2/models/action_intent.pysrc/exoarmur/execution_boundary_v2/models/execution_dispatch.pysrc/exoarmur/execution_boundary_v2/models/execution_proof_bundle.pysrc/exoarmur/execution_boundary_v2/models/execution_trace.pysrc/exoarmur/execution_boundary_v2/models/policy_decision.pysrc/exoarmur/execution_boundary_v2/pipeline/proxy_pipeline.pysrc/exoarmur/execution_boundary_v2/policy/policy_models.pysrc/exoarmur/main.pysrc/exoarmur/replay/cli.pysrc/exoarmur/replay/replay_engine.pysrc/exoarmur/stability/__init__.pysrc/exoarmur/stability/asyncio_policy.pysrc/exoarmur/stability/classifier.pysrc/exoarmur/stability/env_parity.pysrc/exoarmur/stability/reporting.pytests/conftest.pytests/test_cli_env.pytests/test_demo_standalone.pytests/test_env_parity.pytests/test_event_loop_policy.pytests/test_stability_classifier.py
💤 Files with no reviewable changes (1)
- artifacts/openapi_v1.json
| python -m pip install --upgrade pip==24.0 | ||
| python -m pip install -r requirements.lock | ||
| python -m pip install --no-deps -e . | ||
|
|
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
rg -n -C3 'deploy-test:|deploy-production:|actions/checkout|requirements\.lock|--no-deps -e \.' .github/workflows/deploy.ymlRepository: 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 |
There was a problem hiding this comment.
🧩 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.ymlRepository: 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.
| 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.
| - 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 |
There was a problem hiding this comment.
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).
| - 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" |
There was a problem hiding this comment.
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.
| ### V1 Core (Locked Cognition Loop) | ||
| The V1 core is the autonomous decision-making pipeline that is protected by repository policy and regression gates: |
There was a problem hiding this comment.
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 gatesAlso 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.
| 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()), | ||
| ) |
There was a problem hiding this comment.
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.
| 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 |
There was a problem hiding this comment.
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.
| 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.
| 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] |
There was a problem hiding this comment.
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 fileOr 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.
| 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.
| 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 |
There was a problem hiding this comment.
🧩 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.pyRepository: slucerodev/ExoArmur-Core
Length of output: 585
🏁 Script executed:
find . -name asyncio_policy.py -type fRepository: slucerodev/ExoArmur-Core
Length of output: 110
🏁 Script executed:
cat -n ./src/exoarmur/stability/asyncio_policy.pyRepository: 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.
| 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.
There was a problem hiding this comment.
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 | 🟠 MajorTest the built wheel before switching back to editable mode.
The package-install step only proves
import exoarmurworks. 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
⛔ Files ignored due to path filters (1)
requirements.lockis excluded by!**/*.lock
📒 Files selected for processing (3)
.github/workflows/multi-platform-tests.yml.github/workflows/observability.ymlpyproject.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')" |
There was a problem hiding this comment.
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.
| 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
Show autofix suggestion
Hide autofix suggestion
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, computebase_dir = Path(__file__).parent.resolve()once and thenfile_path = (base_dir / filename).resolve(). - Before using
file_path, verify that it is contained withinbase_dir. In Python 3.9+,Path.is_relative_tois the simplest; if that may not be available, a fallback could comparestr(file_path).startswith(str(base_dir)), but usingis_relative_tois 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.
| @@ -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 |
| return | ||
|
|
||
| try: | ||
| with open(file_path, 'rb') as f: |
Check failure
Code scanning / CodeQL
Uncontrolled data used in path expression High
Show autofix suggestion
Hide autofix suggestion
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
staticsubdirectory if desired). - In
serve_file, instead of simply doingPath(__file__).parent / filename, build the path under the root:root / filename. - Normalize that path using
.resolve()(oros.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 equivalentrelative_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_fileto:- Set
root_dir = Path(__file__).parent. - Compute
file_path = (root_dir / filename).resolve(). - Reject the request if
file_pathis not withinroot_dir.
- Set
- Keep the rest of the method (existence check, opening and sending the file) unchanged.
| @@ -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}") |
| """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
Show autofix suggestion
Hide autofix suggestion
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 thatstatic_rootis a parent ofsafe_pathusingsafe_path.is_relative_to(static_root)on Python 3.9+ or a manual prefix check onsafe_path.parts. - If the check fails, return a 404 (or 403) instead of opening the file.
This change is localized toserve_fileinscripts/demo/demo_web_server.pyand doesn’t alter how other parts of the server call it. No new external libraries are needed; we only usepathlibwhich is already imported.
| @@ -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}") | ||
|
|
| return | ||
|
|
||
| try: | ||
| with open(file_path, 'rb') as f: |
Check failure
Code scanning / CodeQL
Uncontrolled data used in path expression High
Show autofix suggestion
Hide autofix suggestion
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.
| @@ -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 |
| """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
Show autofix suggestion
Hide autofix suggestion
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
filenameand resolving:resolved_path = (base_dir / filename).resolve(). - Checking
resolved_path.is_file()and that it is withinbase_dirusingresolved_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, usingis_relative_tokeeps 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.
| @@ -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() | ||
|
|
| return | ||
|
|
||
| try: | ||
| with open(file_path, 'rb') as f: |
Check failure
Code scanning / CodeQL
Uncontrolled data used in path expression High
Show autofix suggestion
Hide autofix suggestion
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 usingPathlogic). 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.
| @@ -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}") |
| """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
Show autofix suggestion
Hide autofix suggestion
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 insideserve_file. - Building
file_path = (base_dir / filename).resolve()instead ofPath(__file__).parent / filename. - Verifying that
file_pathis withinbase_dir, using a safe prefix check such asos.path.commonpath([base_dir, file_path]) == str(base_dir)orfile_path.is_relative_to(base_dir)(if Python ≥3.9 is guaranteed). Since we should not assume a specific Python minor version, usingos.path.commonpathis 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.
| @@ -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}") |
| return | ||
|
|
||
| try: | ||
| with open(file_path, 'rb') as f: |
Check failure
Code scanning / CodeQL
Uncontrolled data used in path expression High
Show autofix suggestion
Hide autofix suggestion
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:
- Define a static root directory for serving files.
- Normalize the combination of root + requested path (e.g., with
Path.resolve()oros.path.realpath). - Verify that the normalized path is still under the intended root (e.g., using
is_relative_toon Python 3.9+, or an equivalent prefix check). - 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 abase_dir = Path(__file__).parent.resolve(). - Build
resolved_path = (base_dir / filename).resolve(); this will collapse..segments and handle symlinks. - Verify
resolved_pathis insidebase_dir. On Python < 3.9, we can implement this viaresolved_path.relative_to(base_dir)in atry/except ValueErrorblock. - If the check fails, return a 403 (or 404) without touching the filesystem.
- Otherwise, continue as before, but using
resolved_path(renamed fromfile_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.
| @@ -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) |
| """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
Show autofix suggestion
Hide autofix suggestion
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.
| @@ -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: |
| return | ||
|
|
||
| try: | ||
| with open(file_path, 'rb') as f: |
Check failure
Code scanning / CodeQL
Uncontrolled data used in path expression High
Show autofix suggestion
Hide autofix suggestion
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.
| @@ -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() |
Summary
exoarmur verify-allpackage-aware so it works from an installed package and gracefully skips repo-only assets when absentValidation
PYTHONPATH=src python3 -m pytest tests/test_cli_env.py tests/test_demo_standalone.py -qPYTHONPATH=src python3 -m pytest tests/test_schema_snapshots.py::TestSchemaSnapshots::test_openapi_snapshot_unchanged -qPYTHONPATH=src python3 -m exoarmur.cli verify-all --fastPydanticDeprecatedSince20filtering ran cleanlyNotes
test-results.xmlwas intentionally left local and not committedSummary by CodeRabbit
Release Notes v0.3.0
New Features
Documentation
Infrastructure