Skip to content

Milestone 6: isolated execution and artifacts - #7

Merged
avitus merged 11 commits into
devfrom
codex-milestone-6
Aug 4, 2026
Merged

Milestone 6: isolated execution and artifacts#7
avitus merged 11 commits into
devfrom
codex-milestone-6

Conversation

@avitus

@avitus avitus commented Aug 4, 2026

Copy link
Copy Markdown
Owner

Summary

  • add fake, Docker, and configurable gVisor execution adapters with lease-scoped workspaces, resource limits, default-deny/allowlisted egress, and crash reaping
  • add sandbox.run_command, the in-sandbox Unix tool bridge, tiered credential scrubbing, and bounded output artifactization
  • add filesystem artifact storage, durable metadata, artifact.export, verified tenant-authorized downloads, and cleanup
  • add the real-runtime CircleCI partition and all 11 Milestone 6 gates

Verification

  • make check — 200 static and 78 contract tests, strict mypy/Ruff/docs, 116 cumulative gates
  • DATABASE_URL=... make migrate test-integration — 68 passed on PostgreSQL 16
  • make test-sandbox — 8 passed against Docker, never fake
  • complete non-live partitions — 354 passed; two credentialed live tests excluded

Design decisions for owner review

Proposed ADR-0042 records the Docker-development/gVisor-production split, the stdio-backed in-sandbox bridge, active Docker workspace quota monitoring, artifact commit ordering, and one documentation conflict: the sandbox spec requires seven operator-set profile fields that the pre-existing exhaustive 106-knob inventory omits.

Summary by CodeRabbit

  • New Features

    • Added isolated sandbox execution with resource limits, workspace separation, restricted networking, and secure environment handling.
    • Added command execution and workspace-file export tools.
    • Added streaming artifact storage with integrity checks, tenant isolation, and automatic cleanup.
    • Large tool outputs can now be truncated and preserved as artifacts.
    • Added audited tool bridging for controlled programmatic tool calls.
  • Security

    • Added protections against credential exposure, filesystem escapes, unauthorized network access, and orphaned sandbox processes.
  • Documentation

    • Added Milestone 6 architecture decisions, scope, status, and navigation updates.
  • Tests

    • Expanded sandbox, artifact, bridge, and integration verification coverage, including hosted sandbox checks.

@coderabbitai

coderabbitai Bot commented Aug 4, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 6440f842-4000-4364-8bd2-1aa585ccdac0

📥 Commits

Reviewing files that changed from the base of the PR and between 8b81ec9 and 65daecd.

📒 Files selected for processing (16)
  • docs/adr/0042-milestone-6-sandbox-and-artifact-seams.md
  • docs/plan/current-milestone.md
  • docs/status/project-state.yaml
  • migrations/versions/f2a6d74b9c10_index_general_artifact_expiry.py
  • src/agent_core/adapters/execution/docker.py
  • src/agent_core/adapters/persistence/revision.py
  • src/agent_core/adapters/persistence/sqlalchemy_models.py
  • src/agent_core/api/app.py
  • src/agent_core/application/artifact_writer.py
  • src/agent_core/execution/proxy.py
  • src/agent_core/tools/executor.py
  • tests/contract/test_trajectory_artifact_store_contract.py
  • tests/gates/test_artifact_m6.py
  • tests/gates/test_proxy_m6.py
  • tests/integration/test_artifact_persistence_m6.py
  • tests/security/test_sandbox_runtime_m6.py
🚧 Files skipped from review as they are similar to previous changes (11)
  • docs/plan/current-milestone.md
  • docs/adr/0042-milestone-6-sandbox-and-artifact-seams.md
  • tests/security/test_sandbox_runtime_m6.py
  • src/agent_core/adapters/execution/docker.py
  • docs/status/project-state.yaml
  • tests/integration/test_artifact_persistence_m6.py
  • tests/contract/test_trajectory_artifact_store_contract.py
  • src/agent_core/application/artifact_writer.py
  • tests/gates/test_artifact_m6.py
  • src/agent_core/execution/proxy.py
  • src/agent_core/tools/executor.py

📝 Walkthrough

Walkthrough

Milestone 6 adds lease-scoped sandbox execution, egress controls, programmatic tool bridging, durable artifact storage, artifactized tool output, lifecycle cleanup, security gates, and CircleCI coverage.

Changes

Sandbox execution and artifacts

Layer / File(s) Summary
Execution contracts and configuration
src/agent_core/domain/*, src/agent_core/ports/*, src/agent_core/config.py, src/agent_core/adapters/execution/local_workspace.py
Adds execution, artifact, credential, egress, resource, lease, and workspace contracts with configuration validation and secure workspace traversal.
Sandbox runtime and lifecycle
src/agent_core/execution/*, src/agent_core/adapters/execution/*, execution/sandbox.Dockerfile
Adds fake and Docker-backed execution, isolated workspaces, resource enforcement, egress proxying, bridge relays, digest validation, lease coordination, and stale-environment cleanup.
Artifact persistence and tool integration
src/agent_core/adapters/artifacts/*, src/agent_core/application/*, src/agent_core/adapters/persistence/*, src/agent_core/tools/*
Adds streamed artifact storage, integrity checks, tenant-scoped keys, metadata persistence, artifact export, general retrieval, and bounded tool-output artifactization.
Application wiring and validation
src/agent_core/bootstrap.py, src/agent_core/runtime/*, tests/*, .circleci/config.yml, Makefile, evals/gates/sandbox.yaml, docs/*, mkdocs.yml, pyproject.toml
Wires sandbox and artifact services into composition and maintenance. Adds contract, integration, Docker security, configuration, gate-registry, CI, and milestone documentation coverage.

Estimated code review effort: 5 (Critical) | ~120 minutes

Possibly related PRs

  • avitus/veetbot#2: Extends shared CI and test-tooling workflows with Milestone 6 sandbox verification.
  • avitus/veetbot#5: Shares workspace and sandbox execution boundaries that this PR extends with Docker isolation and security tests.

Poem

A rabbit checks the sandbox gate,
Streams artifacts of every size.
Bridges pass through guarded sockets,
Leases close before cleanup waits.
CI records each verified hop.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 2.22% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely summarizes the pull request's main changes: Milestone 6 isolated execution and artifact support.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch codex-milestone-6

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

@avitus

avitus commented Aug 4, 2026

Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Aug 4, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@avitus

avitus commented Aug 4, 2026

Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Aug 4, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Note

Due to the large number of review comments, Critical severity comments were prioritized as inline comments.

🟠 Major comments (30)
src/agent_core/application/artifact_writer.py-95-118 (1)

95-118: 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

A crash between the store write and the metadata commit leaks an orphaned object.

The code writes content to the store at line 95, then persists metadata at line 114. The except BaseException handler removes the object when the commit raises. It cannot help when the process dies between the two steps. The result is a file on disk with no artifacts row.

Nothing reclaims that file. The expiry sweep enumerates metadata rows, so an object without a row is invisible to it, and the tenant-scoped storage key cannot be rediscovered from the metadata side.

The PR objectives list artifact commit ordering as an ADR-0042 topic. Please confirm the ADR records this window, and add a reconciliation path that walks the store root and deletes objects that have no metadata row and are older than a safety margin.

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

In `@src/agent_core/application/artifact_writer.py` around lines 95 - 118, The
artifact write flow around _store.put and artifacts.create can leave orphaned
objects after a process crash; update ADR-0042 to document this commit-ordering
window and add reconciliation that walks the store root, resolves each object
against artifact metadata, and deletes only unmatched objects older than a
defined safety margin, while preserving tenant scoping and existing cleanup on
commit failure.
src/agent_core/adapters/persistence/memory.py-953-953 (1)

953-953: 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Define one meaning for expires_at is None across the artifact lifecycle. Widening ArtifactRef.expires_at to datetime | None added an is not None guard at each call site, but no site agrees on what a missing expiry means. PublicArtifactService._get_ref treats it as "never expires" and serves the artifact. TrajectoryExportService.read treats it as "not found". The repository sweep and withdrawal paths treat it as "not eligible", so such a row is never reclaimed. artifact_to_domain rejects it as invalid. The combination creates a row that is readable, un-sweepable, and blocks re-export of its run.

  • src/agent_core/adapters/persistence/memory.py#L953-L953: in expire_for_principal, set expires_at = expired_at when it is None, so consent withdrawal always makes the artifact due for the sweep.
  • src/agent_core/application/trajectory_service.py#L195-L197: treat a None expiry as "does not expire" and return the existing artifact, instead of raising ExportStateError for a row the sweep will never remove.

Pick the invariant first. If no artifact may ever lack an expiry, keep expires_at non-optional on ArtifactRef and drop the guards. If a non-expiring tier is intended, record it in ADR-0042 and apply the same reading at every site.

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

In `@src/agent_core/adapters/persistence/memory.py` at line 953, Adopt the
non-expiring interpretation for missing artifact expiry and apply it
consistently: in src/agent_core/adapters/persistence/memory.py lines 953-953,
update expire_for_principal so None is set to expired_at and becomes
sweep-eligible; in src/agent_core/application/trajectory_service.py lines
195-197, update TrajectoryExportService.read to return the existing artifact
when expires_at is None instead of raising ExportStateError. Ensure
PublicArtifactService._get_ref and other lifecycle paths retain the same
interpretation, and document the invariant in ADR-0042.
src/agent_core/tools/executor.py-870-898 (1)

870-898: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

The artifact is truncated at hard_ceiling, but the marker calls it the full output.

Line 871 slices rendered to hard_ceiling, which is four times maximum_output_bytes. Only those bytes are streamed into the artifact. The marker at line 898 then tells the model full output: artifact:<id>.

For any output larger than hard_ceiling, that statement is wrong and the excess bytes are discarded with no record. The elided count at line 897 measures the gap against len(rendered), so it silently reports discarded bytes as if they were preserved in the artifact.

Pick one behavior and make the text match it:

  • Store the entire rendered payload, and rely on the writer's maximum_bytes cap to reject genuinely oversized output.
  • Keep the ceiling, but record the stored size in the metrics and word the marker as a partial capture, for example first N bytes: artifact:<id>.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/agent_core/tools/executor.py` around lines 870 - 898, Align artifact
storage and marker semantics in the output handling around stream,
writer.create, and marker: either stream the complete rendered payload so the
artifact is genuinely full, or retain hard_ceiling while labeling the artifact
as a partial first-N-byte capture and reporting the discarded size through the
existing metrics. Ensure elided reflects bytes omitted from the displayed output
without claiming discarded bytes are available in the artifact.
src/agent_core/tools/executor.py-678-693 (1)

678-693: 🚀 Performance & Scalability | 🟠 Major | 🏗️ Heavy lift

The bridge retry loop polls the database until the tool timeout elapses.

When a bridged tool call requires approval, _dispatch_one raises ApprovalRequiredError, and this loop sleeps 200 ms and retries without a bound. Each iteration performs several database round trips: find_by_idempotency_key, approvals.get_by_action, and a policy evaluation.

asyncio.timeout(tool.spec.timeout_seconds) at line 776 wraps tool.execute, so the loop does terminate. It terminates by exhausting the sandbox tool's entire timeout at 5 iterations per second. A human approval will almost never arrive inside a tool timeout, so the common outcome is a sustained database poll followed by a timeout failure.

Two changes would help:

  • Apply exponential backoff with a cap instead of a fixed 200 ms interval.
  • Return a refusal response to the sandbox as soon as an approval is required, so the script can react rather than block. The bridge response schema already carries status, reason_code, and retryable.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/agent_core/tools/executor.py` around lines 678 - 693, Update the
ApprovalRequiredError handling in the bridge retry loop around _dispatch_one to
return an immediate refusal response to the sandbox using the existing status,
reason_code, and retryable fields, instead of continuing to poll until
tool.spec.timeout_seconds expires. If retry behavior remains necessary, replace
the fixed asyncio.sleep(0.2) with exponential backoff capped at a bounded
maximum, while preserving cancellation handling and the existing successful
dispatch path.
src/agent_core/tools/executor.py-1257-1261 (1)

1257-1261: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

A malformed artifact_id from a tool crashes the run.

UUID(raw_artifact_id) is unguarded. result.artifacts is tool-supplied data, and any tool, including a remote MCP tool, can return a non-UUID string.

_finish runs at line 841, outside the try/except block that normalizes tool exceptions into ToolFailure values. A ValueError raised here therefore escapes the pipeline and fails the whole run with an internal error, instead of failing the single tool call.

Ignore an unparsable value and continue.

🛡️ Proposed change
         artifact_id: UUID | None = None
         if result.artifacts and isinstance(result.artifacts[0], dict):
             raw_artifact_id = result.artifacts[0].get("artifact_id")
             if isinstance(raw_artifact_id, str):
-                artifact_id = UUID(raw_artifact_id)
+                try:
+                    artifact_id = UUID(raw_artifact_id)
+                except ValueError:
+                    logger.warning(
+                        "tool_artifact_id_malformed",
+                        extra={"tool_name": tool.spec.name},
+                    )
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/agent_core/tools/executor.py` around lines 1257 - 1261, Update the
artifact ID extraction in the executor’s _finish flow to catch UUID parsing
failures from tool-supplied artifact_id strings. Ignore invalid values by
leaving artifact_id as None, while preserving valid UUID conversion and allowing
the tool result to continue through normal failure handling.
src/agent_core/application/artifact_writer.py-143-153 (1)

143-153: 📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

for_run drops retention_days and maximum_bytes, so neither is configurable.

BoundArtifactWriter accepts retention_days=30 and maximum_bytes=512 * 1024 * 1024. ArtifactWriterFactory never forwards them, so every writer uses the hardcoded values and deployment configuration cannot change retention or the size cap.

The size cap is also duplicated. FilesystemArtifactStore.__init__ declares its own maximum_bytes default of 512 * 1024 * 1024. If an operator lowers the store cap, the writer still spools the full payload first and the mismatch surfaces as a late ArtifactIntegrityError from put rather than an early rejection.

Accept both values on the factory and forward them, and source them from settings alongside artifact_root.

♻️ Proposed change
 class ArtifactWriterFactory:
     def __init__(
         self,
         uow_factory: UnitOfWorkFactory,
         store: ArtifactStore,
         clock: Clock,
         ids: IdFactory,
+        *,
+        retention_days: int = 30,
+        maximum_bytes: int = 512 * 1024 * 1024,
     ) -> None:
         self._uow_factory = uow_factory
         self._store = store
         self._clock = clock
         self._ids = ids
+        self._retention_days = retention_days
+        self._maximum_bytes = maximum_bytes
@@
             session_id=session_id,
             run_id=run_id,
             origin=origin,
+            retention_days=self._retention_days,
+            maximum_bytes=self._maximum_bytes,
         )
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/agent_core/application/artifact_writer.py` around lines 143 - 153, Update
ArtifactWriterFactory.for_run to accept retention_days and maximum_bytes, source
both values from settings alongside artifact_root, and forward them when
constructing BoundArtifactWriter. Ensure FilesystemArtifactStore and the writer
use the same configured maximum_bytes so oversized payloads are rejected before
full spooling rather than failing later in put.
src/agent_core/tools/executor.py-892-898 (1)

892-898: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

The excerpt can exceed maximum_output_bytes.

head_bytes is 60 percent of the budget and tail_bytes is 20 percent. Together with the marker, the resulting TextPart can be larger than the budget the function exists to enforce. The marker alone is roughly 70 bytes. For a small maximum_output_bytes, the excerpt overshoots, and nothing re-checks the size after the model_copy.

Subtract the marker length from the budget before you split it between head and tail, and clamp the total.

🐛 Proposed change
         budget = tool.spec.maximum_output_bytes
-        head_bytes = int(budget * 0.6)
-        tail_bytes = int(budget * 0.2)
+        marker_template = f"\n[... 0 bytes elided; full output: artifact:{ref.artifact_id} ...]\n"
+        excerpt_budget = max(0, budget - len(marker_template.encode("utf-8")))
+        head_bytes = int(excerpt_budget * 0.75)
+        tail_bytes = excerpt_budget - head_bytes
         head = artifact_bytes[:head_bytes].decode("utf-8", errors="ignore")
         tail = artifact_bytes[-tail_bytes:].decode("utf-8", errors="ignore")
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/agent_core/tools/executor.py` around lines 892 - 898, Update the artifact
excerpt sizing around maximum_output_bytes so the rendered TextPart, including
the elision marker, never exceeds the budget. Reserve space for the marker
before dividing the remaining budget between head and tail, clamp the available
total for small budgets, and recheck or constrain the final assembled output
after the model_copy path using the existing symbols budget, head_bytes,
tail_bytes, and marker.
src/agent_core/adapters/persistence/memory.py-1007-1009 (1)

1007-1009: 🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

Disable sandbox reaping for in-memory storage or report active runs. InMemoryMaintenanceRepository.live_run_leases() always returns an empty set, while the in-memory composition wires sandbox_manager.reap into MaintenanceWorker. The fake reaper removes every environment whose (run_id, lease_epoch) is absent, so maintenance can destroy a sandbox used by an active in-process run.

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

In `@src/agent_core/adapters/persistence/memory.py` around lines 1007 - 1009, The
in-memory composition must not let MaintenanceWorker reap active sandboxes:
update InMemoryMaintenanceRepository.live_run_leases and its
sandbox_manager.reap wiring to either report every active run’s (run_id,
lease_epoch) or disable sandbox reaping for in-memory storage. Preserve normal
maintenance behavior for non-active environments and ensure active in-process
runs remain protected.
src/agent_core/tools/bridge.py-120-136 (1)

120-136: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Handle the StreamReader line-limit error and unexpected dispatch errors.

Two failure paths escape the try block:

  1. reader.readline() raises ValueError when a line exceeds the StreamReader limit, which defaults to 64 KiB. The in-sandbox caller can send an oversized line. The intended 64 KiB rejection at Line 54 never runs, and the handler aborts before the client receives a response.
  2. self._session.handle propagates any non-BridgeProtocolError exception raised by dispatch. The client then blocks on a read that never completes until the socket closes.

Catch both and return a denial frame.

🛠️ Proposed fix
     async def _handle(self, reader: asyncio.StreamReader, writer: asyncio.StreamWriter) -> None:
         try:
-            while request := await reader.readline():
-                try:
-                    response = await self._session.handle(request)
-                except BridgeProtocolError as exc:
-                    response = json.dumps(
-                        {
-                            "status": "denied",
-                            "reason_code": "bridge.protocol_error",
-                            "retryable": False,
-                            "result": {"message": str(exc)},
-                        },
-                        separators=(",", ":"),
-                    ).encode("utf-8")
+            while True:
+                try:
+                    request = await reader.readline()
+                except ValueError:
+                    writer.write(_denial("bridge.request_too_large") + b"\n")
+                    await writer.drain()
+                    return
+                if not request:
+                    return
+                try:
+                    response = await self._session.handle(request)
+                except BridgeProtocolError as exc:
+                    response = _denial("bridge.protocol_error", str(exc))
+                except Exception:
+                    logger.exception("bridge_dispatch_failed")
+                    response = _denial("bridge.internal_error")
                 writer.write(response + b"\n")
                 await writer.drain()
         finally:
             writer.close()
             await writer.wait_closed()

Add the _denial helper and a module logger, and do not include exception detail for the internal-error case.

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

In `@src/agent_core/tools/bridge.py` around lines 120 - 136, Update _handle to
catch StreamReader.readline() ValueError for oversized lines and unexpected
exceptions from self._session.handle, returning a denial frame for both cases
instead of aborting. Add the _denial helper and module logger requested by the
review, use the existing 64 KiB protocol rejection for oversized input, and
return an internal-error denial without exposing exception details; log
unexpected dispatch exceptions through the module logger.
src/agent_core/tools/bridge.py-107-111 (1)

107-111: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Close the window where the bridge socket is not mode 0600.

asyncio.start_unix_server creates and binds the socket at Line 110. The chmod runs afterwards at Line 111, and it is additionally deferred to a worker thread. Between bind and chmod, the socket permissions follow the process umask. A local process can connect during that window and attempt bridge calls. Set the umask around the bind, or create the parent directory with mode 0700 so the socket is unreachable regardless of its own mode.

🔒 Proposed fix
     async def start(self) -> None:
         self._socket_path.parent.mkdir(parents=True, exist_ok=True)
+        os.chmod(self._socket_path.parent, 0o700)
         self._socket_path.unlink(missing_ok=True)
-        self._server = await asyncio.start_unix_server(self._handle, path=self._socket_path)
-        await asyncio.to_thread(os.chmod, self._socket_path, 0o600)
+        previous_umask = os.umask(0o177)
+        try:
+            self._server = await asyncio.start_unix_server(self._handle, path=self._socket_path)
+        finally:
+            os.umask(previous_umask)
+        os.chmod(self._socket_path, 0o600)

Note that os.umask is process-global. If the sandbox lifecycle starts bridges concurrently in one process, prefer the directory-mode approach alone.

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

In `@src/agent_core/tools/bridge.py` around lines 107 - 111, Update Bridge.start
so the Unix socket is protected from creation through chmod, eliminating the
bind-to-chmod window. Prefer ensuring the socket’s parent directory is created
and retained with mode 0700, avoiding process-global os.umask changes when
bridges may start concurrently; keep the final socket chmod as needed.
src/agent_core/tools/bridge.py-62-64 (1)

62-64: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Encode both token values before comparison. A non-ASCII request token raises TypeError in hmac.compare_digest, escapes _handle, and aborts the connection instead of returning an unauthorized response. Compare UTF-8 bytes.

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

In `@src/agent_core/tools/bridge.py` around lines 62 - 64, Update the token
validation in _handle to encode both the request token from loaded.get("token")
and self._token as UTF-8 bytes before passing them to hmac.compare_digest, while
preserving the existing type check and unauthorized response for invalid or
mismatched tokens.
src/agent_core/tools/artifact_export.py-68-69 (1)

68-69: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Guard against missing workspace and artifact writer collaborators.

cast performs no runtime check. If context.workspace or context.artifacts is None, line 71 or line 72 raises AttributeError, which is not converted into a ToolFailure. SandboxRunCommandTool._command in src/agent_core/tools/sandbox_run_command.py at Line 116 checks context.workspace is None before the cast. Apply the same guard here for both collaborators.

🛡️ Proposed guard
-        workspace = cast(WorkspaceHandle, context.workspace)
-        writer = cast(ArtifactWriter, context.artifacts)
+        if context.workspace is None or context.artifacts is None:
+            return ToolResult(
+                ok=False,
+                content=[],
+                failure=ToolFailure(
+                    kind=ToolFailureKind.PRECONDITION_FAILED,
+                    reason_code="tool.workspace_unavailable",
+                    detail="artifact export requires a workspace and an artifact writer",
+                    retryable=False,
+                ),
+            )
+        workspace = cast(WorkspaceHandle, context.workspace)
+        writer = cast(ArtifactWriter, context.artifacts)

Confirm the exact ToolFailureKind member name available in agent_core.domain.tools.

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

In `@src/agent_core/tools/artifact_export.py` around lines 68 - 69, In the
artifact export flow before the casts to WorkspaceHandle and ArtifactWriter,
validate that both context.workspace and context.artifacts are not None,
following the guard pattern in SandboxRunCommandTool._command. When either
collaborator is missing, raise or return a ToolFailure using the exact available
ToolFailureKind member from agent_core.domain.tools, preventing later
AttributeError calls.
src/agent_core/tools/artifact_export.py-20-20 (1)

20-20: 🚀 Performance & Scalability | 🟠 Major | 🏗️ Heavy lift

Stream workspace files in bounded chunks.

read_bounded materializes the complete file, and _one_chunk only re-yields that buffer. This can retain up to 512 MiB per export, multiplied across concurrent runs. Add a chunked workspace-read API and pass its AsyncIterator[bytes] directly to ArtifactWriter.create.

read_bounded raises WorkspaceReadLimitExceededError when the file exceeds _MAX_ARTIFACT_BYTES; ArtifactExportTool.execute does not catch it. Map this exception to a bounded-output ToolFailure.

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

In `@src/agent_core/tools/artifact_export.py` at line 20, Replace the full-buffer
workspace read used by ArtifactExportTool.execute with a chunked workspace-read
API returning AsyncIterator[bytes], and pass that iterator directly to
ArtifactWriter.create instead of routing it through _one_chunk. Preserve the
_MAX_ARTIFACT_BYTES limit, catch WorkspaceReadLimitExceededError in execute, and
convert it into a bounded-output ToolFailure.
src/agent_core/tools/sandbox_run_command.py-137-151 (1)

137-151: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Include context.call_id in the script_hash input.
Parallel executions of the same command produce identical bridge call IDs. The executor includes these IDs in its idempotency keys, so identical bridged calls can share one persisted invocation.

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

In `@src/agent_core/tools/sandbox_run_command.py` around lines 137 - 151, Update
the script_hash input constructed in the bridge dispatch block of
sandbox_run_command to include context.call_id alongside the command arguments
and working directory. Ensure the serialized source changes for distinct call
IDs while preserving deterministic JSON serialization before hashing and
constructing ProgrammaticBridgeSession.
src/agent_core/bootstrap.py-433-434 (1)

433-434: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Restrict sandbox_passthrough to approved non-secret names.

AGENT_SANDBOX_PASSTHROUGH accepts arbitrary names. Its denylist omits DATABASE_URL, AUTH_TOKEN, and arbitrary *_API_KEY variables. Validate the setting against an explicit non-secret allowlist.

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

In `@src/agent_core/bootstrap.py` around lines 433 - 434, Update the bootstrap
configuration handling around sandbox_passthrough to validate names against an
explicit allowlist of approved non-secret environment variables before passing
them to the sandbox. Reject or exclude DATABASE_URL, AUTH_TOKEN, arbitrary
*_API_KEY variables, and any other names not on the allowlist, while preserving
passthrough for approved names.
src/agent_core/runtime/worker.py-183-188 (1)

183-188: 🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

Protect active sandboxes from stale lease snapshots.

SandboxManager.reap treats live_leases as an allowlist. If a worker acquires a lease after live_run_leases() returns, the reaper can destroy its unexpired sandbox. Synchronize lease acquisition with reaping or add a minimum sandbox age/grace period.

InMemoryMaintenanceRepository.live_run_leases() returns an empty set. The current in-memory UOW has queue=None, so MaintenanceWorker fails before invoking the reaper. Do not wire sandbox reaping to memory until it tracks leases or explicitly disables the sweep.

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

In `@src/agent_core/runtime/worker.py` around lines 183 - 188, Update the
MaintenanceWorker sandbox-reaping flow around live_run_leases and
_sweep_sandboxes to prevent stale lease snapshots from deleting newly leased
active sandboxes, using synchronized lease acquisition or an appropriate minimum
sandbox age/grace period. Also keep in-memory execution from invoking reaping
while InMemoryMaintenanceRepository returns no leases and the UOW has
queue=None; explicitly disable or guard the sweep until lease tracking is
available.
src/agent_core/bootstrap.py-426-435 (1)

426-435: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Reject a non-fake SANDBOX_MECHANISM with memory storage. validate_settings does not validate storage. Memory storage therefore selects FakeExecutionEnvironment for configured docker, gvisor, or microvm, including production configurations. The effective sandbox does not match the configured isolation mechanism.

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

In `@src/agent_core/bootstrap.py` around lines 426 - 435, Update the bootstrap
condition that selects FakeExecutionEnvironment so memory storage is accepted
only when SANDBOX_MECHANISM is explicitly “fake”; otherwise preserve the
configured sandbox mechanism, including docker, gvisor, and microvm. Add the
necessary validation in validate_settings or the nearest settings-validation
path so invalid memory-storage combinations are rejected before sandbox
creation.
tests/gates/test_sandbox_m6.py-37-48 (1)

37-48: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Resolve aliases and cover all host process APIs.

ast.unparse(node.func) produces aio.create_subprocess_exec after import asyncio as aio, and spawn after from os import system as spawn. Neither value matches forbidden_calls. os.posix_spawn and os.exec* are also not listed. A runtime or tool change can spawn a host process without a gate finding. Resolve import aliases to canonical modules and reject the remaining process-spawn APIs.

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

In `@tests/gates/test_sandbox_m6.py` around lines 37 - 48, Update the AST gate’s
call analysis to resolve module and symbol aliases from imports to canonical
targets before comparing against forbidden_calls, including aliases such as
asyncio and os.system. Expand forbidden_calls to cover os.posix_spawn and all
os.exec* process APIs, while preserving findings for existing forbidden calls
and reporting the original call location.
tests/gates/test_sandbox_m6.py-53-60 (1)

53-60: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Resolve annotations before enforcing the host-path boundary.

A postponed or forward-reference annotation such as workspace_root: Path | None can be stored as Path | None. The checks at Lines 58 through 60 do not reject that value. A domain field can then expose a host path while this gate passes. Resolve type hints and recursively inspect aliases, unions, and generic arguments for pathlib.Path.

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

In `@tests/gates/test_sandbox_m6.py` around lines 53 - 60, Update the annotation
validation loop over EnvironmentSpec, EnvironmentHandle, ExecutionResult, and
FileChange to resolve type hints before inspection, then recursively traverse
unions, aliases, and generic arguments to detect pathlib.Path even when nested
or expressed as Path | None. Preserve the existing host_path and container_id
name checks while ensuring any resolved host-path type causes the gate to fail.
tests/contract/test_programmatic_bridge_m6.py-19-42 (1)

19-42: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Test the bridge call cap and replay behavior.

Line 23 sets maximum_calls=2, but the test sends only one authorized request. The test sends ordinal 0 only once. It passes if all ordinals map to zero, replay changes the call ID, or the call cap is ignored. Add a replay of ordinal 0, an ordinal 1 request, and a request that exceeds the cap. Assert the stable replay behavior and the cap rejection.

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

In `@tests/contract/test_programmatic_bridge_m6.py` around lines 19 - 42, Extend
the test around ProgrammaticBridgeSession.handle to exercise maximum_calls=2:
replay the authorized ordinal 0 request, send ordinal 1, then send a request
beyond the cap. Assert that replaying ordinal 0 preserves the expected stable
result and call identity behavior, ordinal 1 is accepted with its corresponding
bridge_call_id, and the over-cap request is rejected with the established cap
reason code.
tests/security/test_sandbox_runtime_m6.py-204-238 (1)

204-238: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Test direct network denial in ALLOWLIST mode.

Lines 213 through 224 use proxy-aware urllib.request calls. The raw-socket check at Lines 143 through 160 uses the default-deny policy, not EgressMode.ALLOWLIST. If allowlist mode retains an external route, sandbox code can use a raw socket or a proxy-disabled client to bypass host, port, and private-address policy while this test passes. Add an unproxied public connection probe under the allowlist policy and require it to fail.

Proposed coverage
- import json,urllib.error,urllib.request
+ import json,socket,urllib.error,urllib.request
  out={}
  for name,url in {
    ...
  }.items():
    try: out[name]=urllib.request.urlopen(url,timeout=5).status
    except Exception as exc: out[name]=type(exc).__name__
+ try:
+   direct=socket.create_connection(('1.1.1.1',53),0.5)
+   direct.close()
+   out['raw_public']=True
+ except OSError:
+   out['raw_public']=False
  print(json.dumps(out,sort_keys=True))
 ...
  assert outcomes["allowed"] == 200
+ assert outcomes["raw_public"] is False

ADR-0042 requires the sandbox to have no direct route to the external bridge network.

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

In `@tests/security/test_sandbox_runtime_m6.py` around lines 204 - 238, Extend
test_egress_allowlisted with an unproxied raw-socket or proxy-disabled public
connection probe while retaining the existing ALLOWLIST policy. Execute the
probe in the sandbox and assert it fails, verifying direct external routing is
denied independently of urllib proxy behavior; keep the existing allowlist
outcome and egress-reason assertions intact.
tests/gates/test_artifact_m6.py-175-243 (1)

175-243: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Exercise the bounded artifact branch.

This fixture produces 2,527 serialized bytes with maximum_output_bytes=1000. It stays below the 4,000-byte cap in ToolPipeline._artifactize_large_output. When a result exceeds that cap, src/agent_core/tools/executor.py stores only rendered[:hard_ceiling] but labels the reference as "full output". Users can receive partial bytes under a full-output label.

Use output larger than four times the limit. Update ToolPipeline._artifactize_large_output to store complete bytes or report a partial artifact. Assert that behavior here.

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

In `@tests/gates/test_artifact_m6.py` around lines 175 - 243, Expand
_LargeOutputTool’s fixture output beyond four times maximum_output_bytes so the
bounded artifact branch in ToolPipeline._artifactize_large_output executes.
Update _artifactize_large_output so artifacts storing rendered[:hard_ceiling]
are identified as partial rather than “full output,” or otherwise ensure
complete bytes are stored; then adjust
test_large_tool_output_is_excerpted_and_artifactized assertions to verify the
resulting label and bounded stored size.
evals/gates/sandbox.yaml-62-67 (1)

62-67: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Cover the workspace export path in this hard gate.

The registered test_artifact_checksum creates the artifact through ArtifactWriterFactory.create(). It does not use a workspace or ArtifactExportTool. test_generated_workspace_file_exports_as_authorized_artifact covers the workspace path, but it does not download through the API or compare the SHA-256. A failure between workspace reading and artifact persistence can therefore pass this registered gate.

Add one registered test that covers workspace export, API download, and checksum validation in one flow. Keep the digest-mismatch assertion.

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

In `@evals/gates/sandbox.yaml` around lines 62 - 67, Update the
gate.sandbox.artifact_checksum registration and its coverage so one registered
test exercises ArtifactExportTool with a generated workspace file, downloads the
resulting artifact through the API, and compares its SHA-256 to the workspace
bytes. Preserve the existing digest-mismatch assertion, and ensure the test is
included in the registered gate rather than relying only on
test_generated_workspace_file_exports_as_authorized_artifact.
src/agent_core/execution/egress_core.py-10-24 (1)

10-24: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Reject all non-global and multicast destination addresses.

_DENIED_NETWORKS allows special-use addresses such as 198.18.0.1, 255.255.255.255, and ::. Use address.is_global after IPv4-mapped normalization, and reject multicast addresses separately because ff02::1 has is_global=True. Remove _DENIED_NETWORKS and add denial tests for special-use and multicast IPv4 and IPv6 addresses.

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

In `@src/agent_core/execution/egress_core.py` around lines 10 - 24, The
destination validation should reject every non-global or multicast address
rather than relying on the incomplete _DENIED_NETWORKS list. Remove
_DENIED_NETWORKS, normalize IPv4-mapped addresses before checking
address.is_global, and add a separate multicast rejection so addresses such as
ff02::1 are denied; add denial tests covering special-use and multicast IPv4 and
IPv6 destinations.
src/agent_core/execution/proxy.py-124-128 (1)

124-128: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Restrict the proxy listener to the sandbox network.

The proxy binds 0.0.0.0:3128. src/agent_core/adapters/execution/docker.py starts the proxy container on the default bridge network and then connects it to the per-environment internal network. Any other container on the default bridge network can therefore reach port 3128 and use the proxy to reach the allowlisted destinations, with the tenant and run identity of this sandbox attached to the audit log.

Bind only the address on the internal per-environment network, or resolve the alias egress-proxy at startup and pass that address to asyncio.start_server.

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

In `@src/agent_core/execution/proxy.py` around lines 124 - 128, Update main in
src/agent_core/execution/proxy.py so asyncio.start_server binds only to the
proxy’s address on the per-environment internal network, rather than 0.0.0.0.
Resolve the egress-proxy alias at startup and pass the resolved address to
start_server, preserving port 3128 and the existing request handler and policy
flow.
src/agent_core/execution/bridge_relay.py-25-30 (1)

25-30: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Both stream readers cap bytes after the read, so the asyncio stream limit raises first. Each module defines a 64 KiB cap that equals the default asyncio stream limit and then checks the cap after readline or readuntil returns. The stream layer raises before the check, so the cap branch is dead code and the oversized-input path produces an exception instead of the intended denial or error response. Pass an explicit limit to the server and handle the stream exception in both places.

  • src/agent_core/execution/bridge_relay.py#L25-L30: pass limit to asyncio.start_unix_server, and catch ValueError from readline to return bridge.request_too_large.
  • src/agent_core/execution/proxy.py#L68-L70: pass limit to asyncio.start_server, and catch asyncio.LimitOverrunError from readuntil so the client receives the 502 response.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/agent_core/execution/bridge_relay.py` around lines 25 - 30, The stream
readers currently enforce their size limits only after the asyncio stream layer
rejects oversized input. In src/agent_core/execution/bridge_relay.py lines
25-30, update the server created by the bridge relay handler to pass the
explicit 64 KiB limit to asyncio.start_unix_server and catch ValueError from
reader.readline, returning bridge.request_too_large. In
src/agent_core/execution/proxy.py lines 68-70, pass the same limit to
asyncio.start_server and catch asyncio.LimitOverrunError from reader.readuntil,
preserving the existing 502 response.
src/agent_core/execution/environment.py-10-21 (1)

10-21: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Strengthen the tier-0 refusal beyond a fixed name list.

TIER_ZERO_NAMES names eight specific variables. Any other secret in the parent environment passes through when an operator lists it, for example GITHUB_TOKEN, AZURE_OPENAI_API_KEY, or DATABASE_PASSWORD. load_settings in src/agent_core/config.py already treats every *_API_KEY variable as a credential, which shows such names exist in the parent process environment. The explicit list also carries a deployment-specific entry, VEETBOT_OPENAI_KEY, which shows the list grows by discovery rather than by rule.

Add a pattern rule on top of the explicit list, and refuse names that contain a secret-bearing token.

🛡️ Proposed fix
+_SECRET_NAME_FRAGMENTS = ("SECRET", "TOKEN", "PASSWORD", "CREDENTIAL", "_KEY", "KEY_")
+
+
+def _is_secret_name(name: str) -> bool:
+    upper = name.upper()
+    return upper in TIER_ZERO_NAMES or any(part in upper for part in _SECRET_NAME_FRAGMENTS)
+
+
 def build_sandbox_environment(
     parent: Mapping[str, str],
     passthrough_names: Sequence[str] = (),
     *,
     working_directory: PurePosixPath = _WORKSPACE_ROOT,
     bridge: BridgeEndpoint | None = None,
 ) -> dict[str, str]:
     """Build, never filter, the environment visible to untrusted code."""
 
     requested = set(passthrough_names)
-    forbidden = requested & TIER_ZERO_NAMES
+    forbidden = {name for name in requested if _is_secret_name(name)}
     if forbidden:
         raise ValueError(
             "tier-0 sandbox variables cannot be passed through: " + ", ".join(sorted(forbidden))
         )

Also applies to: 34-47

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

In `@src/agent_core/execution/environment.py` around lines 10 - 21, Extend the
tier-0 environment-variable refusal logic using the existing TIER_ZERO_NAMES
rule: retain explicit names, and also reject variable names containing
secret-bearing tokens such as API_KEY, TOKEN, PASSWORD, SECRET, or CREDENTIAL.
Apply this predicate wherever tier-0 names are checked so undiscovered
credentials are refused consistently.
src/agent_core/adapters/execution/docker.py-688-691 (1)

688-691: 🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

Invalidate the environment after the adapter kills the container.

Both kill paths run docker kill on the long-lived sandbox container, but self._states[environment.environment_id] stays in place. The container is created without --rm, so it stops and stays stopped. Every later docker exec for the same lease then fails and surfaces as ExecutionUnavailable("container runtime operation failed"), including workspace.read, listdir, and _snapshot. One command that exceeds the output cap or the disk cap therefore breaks the whole run lease with an opaque error.

Choose one contract and make it explicit. Either kill only the command process and leave the container alive, or remove the state entry after the container kill so the next request provisions a new environment and callers receive ExecutionRejected("execution environment is gone").

Also applies to: 711-717

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

In `@src/agent_core/adapters/execution/docker.py` around lines 688 - 691,
Invalidate the leased environment in both container-kill paths after calling
_docker("kill", state.container_id). Remove the corresponding self._states entry
using environment.environment_id so subsequent requests provision a new
container and return ExecutionRejected("execution environment is gone") rather
than attempting docker exec on the stopped container.
src/agent_core/execution/manager.py-173-192 (1)

173-192: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Attempt every destroy during release and close.

release_run and close both stop at the first failing destroy. In release_run the finally block clears only the current key, then the exception propagates and the remaining environments for that run stay in self._handles. In close the exception leaves self._handles and self._locks populated. The container, volume, and network for those environments then survive until the reaper runs. Collect the failures and re-raise after all handles are processed.

🛡️ Proposed fix
     async def release_run(self, run_id: UUID) -> None:
         matches = [(key, handle) for key, handle in self._handles.items() if key[1] == run_id]
+        failures: list[BaseException] = []
         for key, handle in matches:
             try:
                 await self._environment.destroy(handle)
+            except Exception as exc:  # noqa: BLE001 - continue releasing every lease
+                failures.append(exc)
             finally:
                 self._handles.pop(key, None)
                 self._locks.pop(key, None)
+        if failures:
+            raise ExceptionGroup("sandbox release failed", failures)
 
@@
     async def close(self) -> None:
+        failures: list[BaseException] = []
         for handle in tuple(self._handles.values()):
-            await self._environment.destroy(handle)
+            try:
+                await self._environment.destroy(handle)
+            except Exception as exc:  # noqa: BLE001 - close every environment
+                failures.append(exc)
         self._handles.clear()
         self._locks.clear()
+        if failures:
+            raise ExceptionGroup("sandbox close failed", failures)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/agent_core/execution/manager.py` around lines 173 - 192, Update
release_run and close to attempt destroy for every matching/all tracked handle,
collecting any exceptions instead of propagating immediately. Preserve
per-handle cleanup in release_run and clear both _handles and _locks in close
after processing, then re-raise the collected failure(s) only once all destroys
have been attempted.
src/agent_core/adapters/execution/docker.py-686-687 (1)

686-687: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Do not swallow asyncio.CancelledError.

The except asyncio.CancelledError clause records KillReason.CANCELLED and then continues to the code after the try block. It never re-raises. The coroutine therefore returns a normal ExecutionResult after the caller cancelled it, so the cancellation is lost and the caller sees a completed command. The following await calls (Line 692, Line 694, Line 697) also run inside a task that already carries a cancellation request, so they can raise CancelledError again at an unpredictable point and leave disk_task, stdout_task, and stderr_task unfinished.

Kill the container, clean up the helper tasks, then re-raise.

🛡️ Proposed fix
         except asyncio.CancelledError:
-            killed_by = KillReason.CANCELLED
+            with suppress(ExecutionUnavailable):
+                await _docker("kill", state.container_id)
+            with suppress(ProcessLookupError):
+                process.kill()
+            disk_monitor_stop.set()
+            for task in (wait_task, exceeded_task, disk_task, stdout_task, stderr_task):
+                task.cancel()
+            raise
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/agent_core/adapters/execution/docker.py` around lines 686 - 687, Update
the asyncio.CancelledError handler in the execution flow around killed_by to
kill the container, await cleanup of disk_task, stdout_task, and stderr_task,
then re-raise the original cancellation instead of continuing to construct an
ExecutionResult. Ensure cleanup completes without masking the propagated
CancelledError.
🟡 Minor comments (5)
src/agent_core/adapters/persistence/memory.py-953-953 (1)

953-953: 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Consent withdrawal now skips artifacts that never expire.

The previous comparison would raise a TypeError for expires_at is None, so the guard is an improvement. But the new branch means an artifact with no expiry survives expire_for_principal untouched. If a non-expiring trajectory artifact ever exists, withdrawing consent will not schedule its deletion.

Consider setting expires_at = expired_at when it is None, so withdrawal always produces a due artifact.

🛡️ Proposed change
-                if row.artifact.expires_at is None or row.artifact.expires_at <= expired_at:
+                if row.artifact.expires_at is not None and row.artifact.expires_at <= expired_at:
                     continue
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/agent_core/adapters/persistence/memory.py` at line 953, The
expire_for_principal flow should schedule deletion for non-expiring artifacts
instead of leaving them unchanged. Update the branch handling
row.artifact.expires_at so a None value assigns expired_at, while preserving the
existing comparison and expiration behavior for artifacts with an expiry.
src/agent_core/tools/executor.py-901-904 (1)

901-904: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

The stdout/stderr truncation mixes byte and character units.

Line 903 measures the value in UTF-8 bytes. Line 904 slices it by character count. For multibyte text, value[: budget // 4] can produce up to four times budget // 4 bytes, so the truncated field can be larger than the check intended to allow.

Truncate on the encoded bytes and decode with errors="ignore", which matches how head and tail are produced above.

🐛 Proposed change
                 value = structured.get(key)
-                if isinstance(value, str) and len(value.encode("utf-8")) > budget // 2:
-                    structured[key] = value[: budget // 4] + "\n[TRUNCATED]"
+                if isinstance(value, str):
+                    encoded = value.encode("utf-8")
+                    if len(encoded) > budget // 2:
+                        head_text = encoded[: budget // 4].decode("utf-8", errors="ignore")
+                        structured[key] = head_text + "\n[TRUNCATED]"
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/agent_core/tools/executor.py` around lines 901 - 904, Update the
stdout/stderr truncation loop to slice the UTF-8 encoded bytes rather than the
original string, then decode the retained bytes with errors="ignore" before
appending "[TRUNCATED]". Keep the existing budget thresholds and apply this
change only to the structured stdout/stderr fields.
src/agent_core/sandbox/limits.yaml-1-11 (1)

1-11: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Add sandbox-specific overlay validation.
sandbox/limits.yaml is registered, and its top-level fields receive structural validation. The validator accepts malformed destinations, unsupported egress.mode values, and non-positive resource limits. Reject these values with ConfigurationError before _compose constructs ResourceLimits and EgressPolicy.

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

In `@src/agent_core/sandbox/limits.yaml` around lines 1 - 11, Extend the
validation for the registered sandbox limits configuration before _compose
constructs ResourceLimits and EgressPolicy: require positive values for each
resource limit, restrict egress.mode to supported values, and validate every
egress.destinations entry against the expected destination format. Raise
ConfigurationError for any invalid value while preserving valid configurations.
tests/contract/test_artifact_writer_provider_contract.py-10-30 (1)

10-30: 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Assert all writer-provider identity fields.

ArtifactWriter.create() persists principal_id, session_id, and origin as well as tenant and run identity. This test only asserts tenant_id and run_id. A provider can discard one of the other values and still pass this contract.

Store the received values by key. Assert the complete expected mapping.

Proposed test change
 class _Provider:
     def __init__(self) -> None:
-        self.bound: tuple[object, ...] | None = None
+        self.bound: dict[str, object] | None = None
 
     def for_run(self, **values: object) -> object:
-        self.bound = tuple(values[key] for key in sorted(values))
+        self.bound = dict(values)
         return object()
 
 ...
     assert provider.bound is not None
-    assert "tenant-a" in provider.bound
-    assert UUID(int=81) in provider.bound
+    assert provider.bound == {
+        "tenant_id": "tenant-a",
+        "principal_id": "user-a",
+        "session_id": UUID(int=80),
+        "run_id": UUID(int=81),
+        "origin": ArtifactOrigin.SANDBOX_EXPORT,
+    }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/contract/test_artifact_writer_provider_contract.py` around lines 10 -
30, Update _Provider.for_run and
test_artifact_writer_provider_binds_run_and_tenant to retain received values by
key instead of an order-dependent tuple, then assert the complete expected
mapping for tenant_id, principal_id, session_id, run_id, and origin.
src/agent_core/adapters/execution/fake.py-60-77 (1)

60-77: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Align _FakeWorkspaceHandle.listdir with the real adapters. Raise NotADirectoryError for a stored file or a path below one. Raise FileNotFoundError for other non-root paths with no stored file at or below them. Add contract cases for both errors.

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

In `@src/agent_core/adapters/execution/fake.py` around lines 60 - 77, The
`_FakeWorkspaceHandle.listdir` method must validate the requested base before
enumerating entries: raise `NotADirectoryError` when the path is a stored file
or lies beneath one, and raise `FileNotFoundError` for non-root paths with no
stored file at or below them. Preserve root and valid-directory listing
behavior, and add contract cases covering both exceptions.

@avitus

avitus commented Aug 4, 2026

Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Aug 4, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 8

Caution

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

⚠️ Outside diff range comments (4)
src/agent_core/execution/manager.py (1)

169-175: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Make bridge capability explicit in SandboxManager.

The built-in adapters support execute_with_bridge, but SandboxManager accepts any _WorkspaceEnvironment. The unchecked cast can still cause AttributeError for an adapter without bridge support. Require _BridgeExecutionEnvironment or return a defined execution failure when bridge support is unavailable.

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

In `@src/agent_core/execution/manager.py` around lines 169 - 175, Update
SandboxManager’s bridge execution path around execute_with_bridge to explicitly
require bridge capability before invoking it: narrow the environment contract to
_BridgeExecutionEnvironment or validate support and return the established
execution failure when unavailable. Remove the unchecked cast-based assumption
while preserving normal execution for environments without a bridge request.
src/agent_core/adapters/execution/docker.py (3)

576-585: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Guard the relay write in _bridge_pump.

The try at line 577 wraps only handler.handle. The writes at lines 584-585 are unguarded. When the relay process exits, process.stdin.write followed by drain() raises BrokenPipeError or ConnectionResetError, and the _bridge_pump task finishes with that exception.

execute_with_bridge then awaits that task at line 654 and suppresses only asyncio.CancelledError, so the connection error propagates out of the finally block and replaces the ExecutionResult returned at line 636. A completed sandbox command is reported as a failure.

🛡️ Proposed fix
         while request := await process.stdout.readline():
             try:
                 response = await handler.handle(request.rstrip(b"\n"))
             except Exception:
                 response = (
                     b'{"status":"unavailable","reason_code":"bridge.internal_error",'
                     b'"retryable":false}'
                 )
-            process.stdin.write(response + b"\n")
-            await process.stdin.drain()
+            try:
+                process.stdin.write(response + b"\n")
+                await process.stdin.drain()
+            except (BrokenPipeError, ConnectionResetError):
+                return

Also widen the suppression at line 653 so no pump failure can mask the command result:

            pump.cancel()
            with suppress(asyncio.CancelledError, OSError):
                await pump
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/agent_core/adapters/execution/docker.py` around lines 576 - 585, Update
`_bridge_pump` to guard the relay response write and `drain()` against `OSError`
so a disconnected relay does not leave the task failed; preserve processing of
subsequent requests only when the write succeeds. In `execute_with_bridge`,
widen the cancelled pump suppression to include `OSError` when awaiting the
pump, ensuring pump failures cannot replace the completed `ExecutionResult`.

794-811: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

An empty post-kill snapshot reports every workspace file as deleted.

Line 794 sets after = {} whenever killed_by is not None. _changes(before, {}) then emits a ChangeKind.DELETED entry for every path present in before.

The kill path does not delete the workspace volume. It kills the container and restarts it at line 776, and the volume survives. So a timed-out or output-limited command on a populated workspace reports the whole workspace as deleted. SandboxRunCommandTool.execute in src/agent_core/tools/sandbox_run_command.py copies files_changed straight into its structured output, so the model receives false deletions.

Line 795 also computes workspace_size as 0 from the empty snapshot, which makes the guard at lines 796-799 unreachable in this state.

Distinguish "no snapshot taken" from "empty workspace":

🐛 Proposed fix
-        after = {} if killed_by is not None else await self._snapshot(state)
+        after = None if killed_by is not None else await self._snapshot(state)
+        workspace_size = 0 if after is None else sum(item[0] for item in after.values())
-        workspace_size = sum(item[0] for item in after.values())
-        if killed_by is None and (
+        if after is not None and killed_by is None and (
             workspace_size > state.specification.limits.workspace_bytes
             or len(after) > state.specification.limits.inodes_max
         ):

Then compute changes only from a real snapshot:

-        changes = self._changes(before, after)
+        changes = () if after is None else self._changes(before, after)

Alternatively take the snapshot after the container restart at line 776, so a killed command still reports its true file changes.

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

In `@src/agent_core/adapters/execution/docker.py` around lines 794 - 811, Update
the post-command snapshot flow around _snapshot, killed_by, and _changes so a
killed command does not substitute an empty mapping for the workspace snapshot.
Preserve the distinction between no snapshot being available and a genuinely
empty workspace, and ensure workspace_size limits are evaluated from a real
snapshot when applicable. Compute files_changed only from a valid snapshot, or
take the snapshot after the container restart, so surviving workspace files are
not reported as deleted.

552-569: 🚀 Performance & Scalability | 🟠 Major | ⚡ Quick win

Reduce the workspace-usage polling rate.

The loop runs one docker exec roughly every 100 ms for the entire command duration. Each invocation spawns a client process on the host and a Python process inside the container. For a command near the 300 s hard cap this reaches thousands of executions.

The in-container probe also consumes a PID slot inside the same container, so it competes with the workload against --pids-limit. The result is measurable CPU overhead plus a risk of skewing the KillReason.PIDS classification at lines 804-810.

Use a coarser interval, and derive it from a named constant so it is tunable.

♻️ Proposed interval change
+_WORKSPACE_POLL_SECONDS = 1.0
+
     `@staticmethod`
     async def _monitor_workspace_limits(
         state: _DockerState, stop: asyncio.Event
     ) -> KillReason | None:
         while not stop.is_set():
             try:
                 raw = await _docker(
                     "exec", state.container_id, "python", "-c", _WORKSPACE_USAGE_SCRIPT
                 )
             except ExecutionUnavailable:
                 if stop.is_set():
                     return None
-                await asyncio.sleep(0.1)
+                await asyncio.sleep(_WORKSPACE_POLL_SECONDS)
                 continue
             size, inodes = (int(value) for value in raw.decode("ascii").split())
             if (
                 size > state.specification.limits.workspace_bytes
                 or inodes > state.specification.limits.inodes_max
             ):
                 return KillReason.DISK
             with suppress(TimeoutError):
-                await asyncio.wait_for(stop.wait(), timeout=0.1)
+                await asyncio.wait_for(stop.wait(), timeout=_WORKSPACE_POLL_SECONDS)

The post-command check at lines 796-803 already catches limit violations that the monitor misses, so a coarser interval does not weaken enforcement. Note that tests/security/test_sandbox_runtime_m6.py::test_limits_enforced asserts KillReason.DISK within an 8 s timeout; confirm the chosen interval keeps that test deterministic.

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

In `@src/agent_core/adapters/execution/docker.py` around lines 552 - 569, Reduce
the workspace-usage monitor polling frequency in the loop that invokes _docker
and waits on stop by replacing the hard-coded 0.1-second timeout and retry delay
with a named, tunable interval constant. Use the same interval consistently for
asyncio.sleep and asyncio.wait_for, choosing a value that preserves
deterministic detection within the test’s 8-second timeout while reducing
container process overhead.
🧹 Nitpick comments (2)
src/agent_core/adapters/artifacts/filesystem.py (1)

132-136: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Make claim restoration failure-tolerant so one bad entry does not abort the whole pass.

_restore_claim suppresses only FileExistsError. Two other cases propagate out of reconcile_orphans and abort the remaining entries:

  • os.link raises FileNotFoundError if another reconciliation pass already removed the claim.
  • os.link raises OSError (for example EPERM or EOPNOTSUPP) on filesystems that do not support hard links.

In both cases the reconciliation callback in src/agent_core/bootstrap.py (Line 527) fails and later objects in the same pass are never examined. Restoration is best-effort by design, so handle these per entry and continue.

♻️ Proposed change to keep the pass going
 def _restore_claim(claim: Path, destination: Path) -> None:
-    with suppress(FileExistsError):
+    with suppress(OSError):
         os.link(claim, destination)
     claim.unlink(missing_ok=True)

If you prefer to keep unexpected link errors visible, log them instead of suppressing them silently.

Also applies to: 153-157, 161-164

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

In `@src/agent_core/adapters/artifacts/filesystem.py` around lines 132 - 136,
Update _restore_claim and the reconcile_orphans restoration path so
FileNotFoundError and other OSError failures from os.link are handled per entry,
allowing reconciliation to continue processing later claims. Preserve successful
restoration and existing FileExistsError behavior; optionally log unexpected
link errors rather than propagating them from the callback.
src/agent_core/tools/executor.py (1)

924-932: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

The structured payload stays unbounded after artifactization.

_artifactize_large_output bounds content and truncates only the stdout and stderr string fields. Any other large field in structured passes through unchanged, and _finish persists it as structured_result (Line 1305). A tool that returns a large list, such as files_changed in src/agent_core/tools/sandbox_run_command.py, can therefore still write an unbounded row to the database even though the declared output budget was exceeded.

Consider bounding the whole serialized structured payload, rather than two known keys.

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

In `@src/agent_core/tools/executor.py` around lines 924 - 932, The structured
payload handling in _artifactize_large_output only truncates stdout and stderr,
allowing other fields such as files_changed to exceed the output budget before
_finish persists structured_result. Replace the field-specific truncation with
whole-payload serialization and enforce the budget on the serialized structured
data, preserving valid structured output while bounding its stored size.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/agent_core/adapters/execution/docker.py`:
- Around line 879-898: Update the parsing logic in the reaper loop to treat an
empty or missing raw_created value as old enough to reap, while preserving the
existing grace-period calculation for valid timestamps. Keep malformed required
fields handled by the existing ValueError/TypeError path, and ensure legacy
containers without the created_at label continue through the cleanup checks
instead of being skipped.
- Around line 209-241: Update the Docker workspace read flow and _read to
prevent symlink traversal: resolve each path component beneath /workspace using
descriptor-based traversal with O_NOFOLLOW, rejecting any symlink in
intermediate or final components. Perform existence, regular-file, and size
validation on the resulting descriptor, then stream bytes from that descriptor
rather than using Path.exists/is_file/stat/open; preserve the existing mapped
exceptions and read-limit behavior.

In `@src/agent_core/adapters/execution/local_workspace.py`:
- Around line 128-141: Wrap each intermediate component open in the workspace
traversal logic around the directory iteration, translating OSError with
errno.ELOOP into WorkspaceEscape just like the final component path in the
existing exception handler. Preserve other OSError behavior and descriptor
cleanup, and add a contract test covering a symlinked directory component that
asserts WorkspaceEscape.

In `@src/agent_core/execution/bridge_relay.py`:
- Around line 55-60: The response relay around responses.readline() must prevent
oversized responses from leaving unread bytes that become the next response. Add
a response-size bound to the bridge relay, or terminate the relay/session
immediately when the readline overrun is detected, while preserving the existing
bridge.response_too_large handling and normal empty-response return behavior.

In `@src/agent_core/execution/manager.py`:
- Around line 178-190: Update release_run and close to coordinate teardown with
execute_for and delegated workspace operations by tracking a closing state and
active lease operations. Prevent new provisioning once close begins, ensure each
handle is destroyed only after its active operations finish, and make close
account for handles provisioned after its initial snapshot.

In `@src/agent_core/tools/artifact_export.py`:
- Around line 63-79: Update the collaborator guard before the WorkspaceHandle
and ArtifactWriter casts to handle _UnavailableCollaborator’s RuntimeError from
getattr(raw_writer, "create", None), treating it as an unavailable writer and
returning the existing INTERNAL tool.internal_error result instead of allowing
execution to raise. Preserve the current callable checks for available
collaborators.

In `@tests/contract/test_execution_environment_contract.py`:
- Around line 113-129: Update SandboxManager.release_run() and close() to
attempt bounded destruction of every handle even when a destroy() raises
asyncio.CancelledError, while retaining ordinary failures for retry and
re-raising cancellation after all cleanup completes. Extend the existing sandbox
cleanup tests with a regression case where the first destroy attempt raises
asyncio.CancelledError, verifying later handles are attempted and cancellation
is propagated.

In `@tests/contract/test_workspace_handle_contract.py`:
- Around line 27-39: Update DockerWorkspaceHandle path resolution to use
descriptor-relative traversal with dir_fd and O_NOFOLLOW for reads, writes,
streams, and directory listings; reject every symlink component with
WorkspaceEscape and map descendants beneath a non-directory ancestor to
NotADirectoryError. In tests/contract/test_workspace_handle_contract.py lines
27-39, run the symlink and FIFO assertions against DockerWorkspaceHandle; in
tests/contract/test_execution_environment_contract.py lines 94-110, run the
descendant-under-file assertion against DockerWorkspaceHandle.

---

Outside diff comments:
In `@src/agent_core/adapters/execution/docker.py`:
- Around line 576-585: Update `_bridge_pump` to guard the relay response write
and `drain()` against `OSError` so a disconnected relay does not leave the task
failed; preserve processing of subsequent requests only when the write succeeds.
In `execute_with_bridge`, widen the cancelled pump suppression to include
`OSError` when awaiting the pump, ensuring pump failures cannot replace the
completed `ExecutionResult`.
- Around line 794-811: Update the post-command snapshot flow around _snapshot,
killed_by, and _changes so a killed command does not substitute an empty mapping
for the workspace snapshot. Preserve the distinction between no snapshot being
available and a genuinely empty workspace, and ensure workspace_size limits are
evaluated from a real snapshot when applicable. Compute files_changed only from
a valid snapshot, or take the snapshot after the container restart, so surviving
workspace files are not reported as deleted.
- Around line 552-569: Reduce the workspace-usage monitor polling frequency in
the loop that invokes _docker and waits on stop by replacing the hard-coded
0.1-second timeout and retry delay with a named, tunable interval constant. Use
the same interval consistently for asyncio.sleep and asyncio.wait_for, choosing
a value that preserves deterministic detection within the test’s 8-second
timeout while reducing container process overhead.

In `@src/agent_core/execution/manager.py`:
- Around line 169-175: Update SandboxManager’s bridge execution path around
execute_with_bridge to explicitly require bridge capability before invoking it:
narrow the environment contract to _BridgeExecutionEnvironment or validate
support and return the established execution failure when unavailable. Remove
the unchecked cast-based assumption while preserving normal execution for
environments without a bridge request.

---

Nitpick comments:
In `@src/agent_core/adapters/artifacts/filesystem.py`:
- Around line 132-136: Update _restore_claim and the reconcile_orphans
restoration path so FileNotFoundError and other OSError failures from os.link
are handled per entry, allowing reconciliation to continue processing later
claims. Preserve successful restoration and existing FileExistsError behavior;
optionally log unexpected link errors rather than propagating them from the
callback.

In `@src/agent_core/tools/executor.py`:
- Around line 924-932: The structured payload handling in
_artifactize_large_output only truncates stdout and stderr, allowing other
fields such as files_changed to exceed the output budget before _finish persists
structured_result. Replace the field-specific truncation with whole-payload
serialization and enforce the budget on the serialized structured data,
preserving valid structured output while bounding its stored size.
🪄 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: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: db6e9a5d-9dac-46be-9cca-d29737806def

📥 Commits

Reviewing files that changed from the base of the PR and between 35b802a and db2eedf.

📒 Files selected for processing (37)
  • docs/adr/0042-milestone-6-sandbox-and-artifact-seams.md
  • docs/status/project-state.yaml
  • src/agent_core/adapters/artifacts/filesystem.py
  • src/agent_core/adapters/execution/docker.py
  • src/agent_core/adapters/execution/fake.py
  • src/agent_core/adapters/execution/local_workspace.py
  • src/agent_core/adapters/persistence/memory.py
  • src/agent_core/adapters/persistence/repositories.py
  • src/agent_core/application/artifact_writer.py
  • src/agent_core/application/public_services.py
  • src/agent_core/bootstrap.py
  • src/agent_core/config.py
  • src/agent_core/execution/bridge_relay.py
  • src/agent_core/execution/egress_core.py
  • src/agent_core/execution/environment.py
  • src/agent_core/execution/manager.py
  • src/agent_core/execution/proxy.py
  • src/agent_core/ports/execution.py
  • src/agent_core/ports/repositories.py
  • src/agent_core/runtime/worker.py
  • src/agent_core/sandbox/limits.yaml
  • src/agent_core/tools/artifact_export.py
  • src/agent_core/tools/bridge.py
  • src/agent_core/tools/executor.py
  • src/agent_core/tools/sandbox_run_command.py
  • tests/contract/test_artifact_repository_contract.py
  • tests/contract/test_artifact_store_contract.py
  • tests/contract/test_artifact_writer_provider_contract.py
  • tests/contract/test_execution_environment_contract.py
  • tests/contract/test_programmatic_bridge_m6.py
  • tests/contract/test_workspace_handle_contract.py
  • tests/gates/test_api_m5.py
  • tests/gates/test_artifact_m6.py
  • tests/gates/test_policy_m4.py
  • tests/gates/test_sandbox_m6.py
  • tests/security/test_sandbox_runtime_m6.py
  • tests/unit/test_config.py
🚧 Files skipped from review as they are similar to previous changes (15)
  • src/agent_core/sandbox/limits.yaml
  • tests/contract/test_artifact_writer_provider_contract.py
  • tests/contract/test_artifact_repository_contract.py
  • tests/gates/test_artifact_m6.py
  • docs/status/project-state.yaml
  • src/agent_core/tools/sandbox_run_command.py
  • src/agent_core/ports/execution.py
  • src/agent_core/application/public_services.py
  • src/agent_core/adapters/execution/fake.py
  • src/agent_core/runtime/worker.py
  • src/agent_core/adapters/persistence/repositories.py
  • src/agent_core/application/artifact_writer.py
  • tests/security/test_sandbox_runtime_m6.py
  • src/agent_core/bootstrap.py
  • src/agent_core/execution/egress_core.py

Comment thread src/agent_core/adapters/execution/docker.py Outdated
Comment thread src/agent_core/adapters/execution/docker.py Outdated
Comment thread src/agent_core/adapters/execution/local_workspace.py
Comment thread src/agent_core/execution/bridge_relay.py
Comment thread src/agent_core/execution/manager.py Outdated
Comment thread src/agent_core/tools/artifact_export.py
Comment thread tests/contract/test_execution_environment_contract.py
Comment thread tests/contract/test_workspace_handle_contract.py
@avitus

avitus commented Aug 4, 2026

Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Aug 4, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 5

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

Inline comments:
In `@src/agent_core/execution/manager.py`:
- Around line 249-259: Update release_run so run_id is added to _released_runs
under _condition before acquiring _teardown_lock, preventing new B operations
while release_run(B) waits on unrelated teardown for A. Preserve the existing
serialized active-operation drain, handle snapshot, and _destroy_matches
sequence.
- Around line 172-174: Make the provisioning handoff in _handle_for()
cancellation-safe by avoiding an await while transferring the newly provisioned
handle into _handles, or by ensuring cancellation cleans up the handle and
removes its uncached _locks entry before _operation() exits. Preserve correct
release_run() and close() behavior for handles returned by provision(),
including when provision() raises.

In `@tests/contract/test_execution_environment_contract.py`:
- Around line 157-183: Update SandboxManager.release_run() and close() to catch
and defer caller cancellation while waiting for active operations and completing
_destroy_matches(), then re-raise CancelledError only after all sandbox teardown
attempts finish. In tests/contract/test_execution_environment_contract.py lines
157-183, add blocked-execution cancellation tests covering both methods and
verify teardown completes before cancellation propagates. In
docs/adr/0042-milestone-6-sandbox-and-artifact-seams.md lines 45-49, remove or
revise the deferred-cancellation guarantee until the lifecycle implementation
enforces it.

In `@tests/gates/test_artifact_m6.py`:
- Around line 154-165: Split
test_artifact_export_normalizes_an_unavailable_writer into separate cases: one
with only workspace unavailable to verify workspace handling, and another with a
working workspace but unavailable artifacts collaborator to exercise the
artifacts.create path. Ensure each case asserts the expected normalized result
independently.

In `@tests/security/test_sandbox_runtime_m6.py`:
- Around line 227-230: Replace the hard-coded token argument in
ProgrammaticBridgeSession with a generated test token, or add a documented
targeted suppression for this intentional test credential so Ruff S106 passes
without changing the test’s behavior.
🪄 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: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 81aa63d7-7fe8-462a-83c7-033ce8fb5556

📥 Commits

Reviewing files that changed from the base of the PR and between db2eedf and 270e651.

📒 Files selected for processing (12)
  • docs/adr/0042-milestone-6-sandbox-and-artifact-seams.md
  • docs/status/project-state.yaml
  • src/agent_core/adapters/execution/docker.py
  • src/agent_core/adapters/execution/local_workspace.py
  • src/agent_core/execution/bridge_relay.py
  • src/agent_core/execution/manager.py
  • src/agent_core/tools/artifact_export.py
  • tests/contract/test_execution_environment_contract.py
  • tests/contract/test_workspace_handle_contract.py
  • tests/gates/test_artifact_m6.py
  • tests/gates/test_sandbox_m6.py
  • tests/security/test_sandbox_runtime_m6.py
🚧 Files skipped from review as they are similar to previous changes (6)
  • tests/contract/test_workspace_handle_contract.py
  • src/agent_core/tools/artifact_export.py
  • docs/status/project-state.yaml
  • src/agent_core/execution/bridge_relay.py
  • src/agent_core/adapters/execution/docker.py
  • src/agent_core/adapters/execution/local_workspace.py

Comment thread src/agent_core/execution/manager.py Outdated
Comment thread src/agent_core/execution/manager.py Outdated
Comment thread tests/contract/test_execution_environment_contract.py
Comment thread tests/gates/test_artifact_m6.py Outdated
Comment thread tests/security/test_sandbox_runtime_m6.py
@avitus

avitus commented Aug 4, 2026

Copy link
Copy Markdown
Owner Author

@coderabbitai review

@avitus

avitus commented Aug 4, 2026

Copy link
Copy Markdown
Owner Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Aug 4, 2026

Copy link
Copy Markdown
✅ Action performed

Full review finished.


Your included review limit is currently reached under our Fair Usage Limits Policy. Your recent PR review activity is in the 95th percentile or higher among CodeRabbit users, so adaptive limits apply. This review may still proceed through usage-based billing if eligible. Your next included review will be available in 8 minutes.

@coderabbitai

coderabbitai Bot commented Aug 4, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@avitus

avitus commented Aug 4, 2026

Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Aug 4, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 3

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

Inline comments:
In `@src/agent_core/execution/manager.py`:
- Around line 157-191: Update _handle_for and the teardown flows in release_run,
close, and _discard_unused_locks to retain lock entries while their user count
is nonzero, including provisioning that outlives a timed drain. Reconcile
handles created after teardown snapshots by destroying late-provisioned handles
before the released or closing operation completes, and prevent _handle_for from
raising KeyError during its final ownership handoff. Add regressions covering
blocked provisioning followed by both release_run and close.

In `@tests/contract/test_programmatic_bridge_m6.py`:
- Around line 6-8: Update the test fixture’s literal token argument near the
token setup to generate the token at runtime instead of embedding a hard-coded
secret-like string. Preserve the fixture’s expected token behavior while
removing the Ruff S106 violation, reusing the existing imports or standard
runtime facilities as appropriate.

In `@tests/gates/test_sandbox_m6.py`:
- Around line 129-159: Update DockerWorkspaceHandle.stream in
src/agent_core/adapters/execution/docker.py to enforce the same timeout for
direct docker exec subprocesses, and on timeout kill the process and await
wait() before propagating the timeout failure. In tests/gates/test_sandbox_m6.py
lines 129-159, extend the hanging-process helper test to record and assert
wait() after kill(), and add regression coverage for the streaming path. In
docs/adr/0042-milestone-6-sandbox-and-artifact-seams.md lines 123-129, limit the
timeout claim to _docker() until all direct Docker subprocess paths are bounded.
🪄 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: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 21c602ca-0f15-4a9e-aa90-18eb1d6bedb2

📥 Commits

Reviewing files that changed from the base of the PR and between 270e651 and 67f1611.

📒 Files selected for processing (19)
  • .circleci/config.yml
  • docs/adr/0042-milestone-6-sandbox-and-artifact-seams.md
  • docs/status/project-state.yaml
  • execution/sandbox.Dockerfile
  • src/agent_core/adapters/execution/docker.py
  • src/agent_core/execution/bridge_relay.py
  • src/agent_core/execution/environment.py
  • src/agent_core/execution/manager.py
  • src/agent_core/execution/proxy.py
  • src/agent_core/tools/artifact_export.py
  • src/agent_core/tools/bridge.py
  • src/agent_core/tools/executor.py
  • tests/contract/test_artifact_store_contract.py
  • tests/contract/test_artifact_writer_contract.py
  • tests/contract/test_execution_environment_contract.py
  • tests/contract/test_programmatic_bridge_m6.py
  • tests/gates/test_artifact_m6.py
  • tests/gates/test_sandbox_m6.py
  • tests/security/test_sandbox_runtime_m6.py
💤 Files with no reviewable changes (1)
  • src/agent_core/execution/environment.py
🚧 Files skipped from review as they are similar to previous changes (9)
  • .circleci/config.yml
  • execution/sandbox.Dockerfile
  • src/agent_core/execution/proxy.py
  • src/agent_core/execution/bridge_relay.py
  • docs/status/project-state.yaml
  • src/agent_core/tools/artifact_export.py
  • src/agent_core/adapters/execution/docker.py
  • tests/security/test_sandbox_runtime_m6.py
  • src/agent_core/tools/executor.py

Comment thread src/agent_core/execution/manager.py Outdated
Comment thread tests/contract/test_programmatic_bridge_m6.py
Comment thread tests/gates/test_sandbox_m6.py
@avitus

avitus commented Aug 4, 2026

Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Aug 4, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Caution

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

⚠️ Outside diff range comments (2)
src/agent_core/adapters/execution/docker.py (2)

371-394: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Handle subprocess exit races during cleanup.

If the subprocess exits after the returncode check, kill() or terminate() can raise ProcessLookupError. Suppress this exception and always await wait() with return_exceptions=True so cleanup does not mask the original result or skip relay-task cleanup.

  • src/agent_core/adapters/execution/docker.py#L371-L394
  • src/agent_core/adapters/execution/docker.py#L779-L785
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/agent_core/adapters/execution/docker.py` around lines 371 - 394, Update
the subprocess cleanup in src/agent_core/adapters/execution/docker.py lines
371-394 and 779-785 to tolerate exit races: suppress ProcessLookupError around
kill() or terminate() calls, and always await process.wait() with
return_exceptions=True so cleanup cannot mask the original result or prevent
relay-task cleanup. Apply the change to both affected cleanup sites.

366-390: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Use one deadline for the complete workspace stream.

Each asyncio.wait_for() starts a new 60-second timeout. Periodic output can keep the stream alive beyond 60 seconds. Compute one monotonic deadline before the loop and pass the remaining duration to each read and to process.wait(). Add a test with periodic data that exceeds the total deadline.

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

In `@src/agent_core/adapters/execution/docker.py` around lines 366 - 390, Update
the workspace stream method containing the read loop to compute a single
monotonic deadline before reading begins, then pass the remaining time to every
stdout read and the final process.wait() call instead of restarting
_DOCKER_COMMAND_TIMEOUT_SECONDS for each operation. Preserve the existing error
mapping and convert deadline exhaustion to the current ExecutionUnavailable
timeout behavior. Add coverage for periodic output that keeps arriving beyond
the total deadline.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@src/agent_core/adapters/execution/docker.py`:
- Around line 371-394: Update the subprocess cleanup in
src/agent_core/adapters/execution/docker.py lines 371-394 and 779-785 to
tolerate exit races: suppress ProcessLookupError around kill() or terminate()
calls, and always await process.wait() with return_exceptions=True so cleanup
cannot mask the original result or prevent relay-task cleanup. Apply the change
to both affected cleanup sites.
- Around line 366-390: Update the workspace stream method containing the read
loop to compute a single monotonic deadline before reading begins, then pass the
remaining time to every stdout read and the final process.wait() call instead of
restarting _DOCKER_COMMAND_TIMEOUT_SECONDS for each operation. Preserve the
existing error mapping and convert deadline exhaustion to the current
ExecutionUnavailable timeout behavior. Add coverage for periodic output that
keeps arriving beyond the total deadline.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: ba91e6a4-02f2-4811-b784-50b1667ea888

📥 Commits

Reviewing files that changed from the base of the PR and between 67f1611 and 4a2209d.

📒 Files selected for processing (7)
  • docs/adr/0042-milestone-6-sandbox-and-artifact-seams.md
  • docs/status/project-state.yaml
  • src/agent_core/adapters/execution/docker.py
  • src/agent_core/execution/manager.py
  • tests/contract/test_execution_environment_contract.py
  • tests/contract/test_programmatic_bridge_m6.py
  • tests/gates/test_sandbox_m6.py
🚧 Files skipped from review as they are similar to previous changes (5)
  • docs/status/project-state.yaml
  • tests/contract/test_programmatic_bridge_m6.py
  • tests/gates/test_sandbox_m6.py
  • docs/adr/0042-milestone-6-sandbox-and-artifact-seams.md
  • src/agent_core/execution/manager.py

@avitus

avitus commented Aug 4, 2026

Copy link
Copy Markdown
Owner Author

Addressed both out-of-diff findings from CodeRabbit review 4852935691 in 0c2df0a. Docker workspace streaming now uses one monotonic 60-second deadline across all reads and process exit, with periodic-output regression coverage. Docker control, workspace-stream, and bridge-relay cleanup now suppresses ProcessLookupError exit races and always reaps subprocesses. Local make check, 68 PostgreSQL integration tests, 10 real Docker sandbox tests, and docs checks all pass.

@avitus

avitus commented Aug 4, 2026

Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Aug 4, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@avitus

avitus commented Aug 4, 2026

Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Aug 4, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@avitus

avitus commented Aug 4, 2026

Copy link
Copy Markdown
Owner Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Aug 4, 2026

Copy link
Copy Markdown
✅ Action performed

Full review finished.


Your included review limit is currently reached under our Fair Usage Limits Policy. Your recent PR review activity is in the 95th percentile or higher among CodeRabbit users, so adaptive limits apply. This review may still proceed through usage-based billing if eligible. Your next included review will be available in 14 minutes.

@avitus

avitus commented Aug 4, 2026

Copy link
Copy Markdown
Owner Author

Addressed the focused local CodeRabbit finding in deb9ed6: Docker workspace streams now run an independent total-deadline task that kills and reaps docker exec even while the async iterator is suspended at yield; resumption reports the timeout. Added a regression that pauses after the first chunk and observes background kill/reap before resuming. All local gates pass.

@avitus

avitus commented Aug 4, 2026

Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Aug 4, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@avitus

avitus commented Aug 4, 2026

Copy link
Copy Markdown
Owner Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Aug 4, 2026

Copy link
Copy Markdown
✅ Action performed

Full review finished.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 13

Caution

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

⚠️ Outside diff range comments (1)
src/agent_core/runtime/worker.py (1)

165-190: 🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

Revalidate leases before sandbox deletion. SandboxManager.reap treats the point-in-time lease set as authoritative. A lease claimed after the snapshot can therefore lose its sandbox. Docker’s creation grace only delays this race and can be disabled. Revalidate each candidate before deletion, and add a concurrent claim-and-reap regression test.

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

In `@src/agent_core/runtime/worker.py` around lines 165 - 190, Update the sandbox
reaping flow invoked by MaintenanceWorker.run_once and implemented by
SandboxManager.reap to revalidate each candidate’s lease immediately before
deletion, rather than relying solely on live_run_leases captured at the initial
snapshot. Preserve deletion for unclaimed candidates and ensure a concurrent
claim prevents deletion. Add a regression test covering a lease claimed between
snapshot and reap.
🧹 Nitpick comments (14)
src/agent_core/adapters/execution/fake.py (1)

50-54: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Reject a negative maximum_bytes to match the Docker adapter.

DockerWorkspaceHandle.read_bounded and DockerWorkspaceHandle.stream raise ValueError when maximum_bytes < 0. The fake accepts a negative limit and then raises WorkspaceReadLimitExceededError for any non-empty file. Aligning the fake keeps the port contract testable against both adapters.

♻️ Proposed alignment
     async def read_bounded(self, path: str, maximum_bytes: int) -> bytes:
+        if maximum_bytes < 0:
+            raise ValueError("maximum_bytes must not be negative")
         data = await self.read(path)
         if len(data) > maximum_bytes:
             raise WorkspaceReadLimitExceededError("workspace file exceeds read limit")
         return data
 
     async def stream(self, path: str, maximum_bytes: int) -> AsyncIterator[bytes]:
+        if maximum_bytes < 0:
+            raise ValueError("maximum_bytes must not be negative")
         data = await self.read_bounded(path, maximum_bytes)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/agent_core/adapters/execution/fake.py` around lines 50 - 54, Update
FakeWorkspaceHandle.read_bounded to raise ValueError immediately when
maximum_bytes is negative, matching DockerWorkspaceHandle.read_bounded and
stream; retain the existing size-limit check and return behavior for
non-negative limits.
src/agent_core/execution/manager.py (1)

242-254: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Check for bridge support before the cast.

cast performs no runtime check. If a configured adapter does not implement execute_with_bridge, Line 251 raises AttributeError, which surfaces as an internal error instead of a domain rejection. A guarded check keeps the failure mode explicit.

♻️ Proposed guard
             if bridge is not None:
+                if not hasattr(self._environment, "execute_with_bridge"):
+                    raise ExecutionRejected("execution environment does not support a tool bridge")
                 bridge_environment = cast(_BridgeExecutionEnvironment, self._environment)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/agent_core/execution/manager.py` around lines 242 - 254, In the bridge
execution branch of the operation manager, validate that the configured
environment supports execute_with_bridge before casting it to
_BridgeExecutionEnvironment. Reject unsupported bridge execution through the
established domain-level failure path, and only construct the endpoint and
invoke execute_with_bridge after the capability check.
src/agent_core/execution/proxy.py (1)

122-125: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Import suppress at module level.

__import__("contextlib").suppress(OSError) performs a dynamic import inside an exception handler. A top-level from contextlib import suppress is clearer and avoids the per-call lookup.

♻️ Proposed change
@@
 import asyncio
 import json
 import os
 import socket
 import sys
+from contextlib import suppress
 from urllib.parse import urlsplit
@@
         if not writer.is_closing():
             writer.write(b"HTTP/1.1 502 Bad Gateway\r\nContent-Length: 0\r\n\r\n")
-            with __import__("contextlib").suppress(OSError):
+            with suppress(OSError):
                 await writer.drain()
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/agent_core/execution/proxy.py` around lines 122 - 125, Import suppress
from contextlib at module scope, then update the exception-handling block in the
proxy response path to use that imported symbol instead of dynamically importing
contextlib via __import__. Preserve the existing OSError suppression and
writer.drain behavior.
src/agent_core/execution/bridge_relay.py (1)

60-74: 🩺 Stability & Availability | 🔵 Trivial | 💤 Low value

Exit the relay when the host response channel closes.

Line 68 returns from handle when responses.readline() yields an empty result. An empty result means the host closed stdout. response_overrun stays clear, so Line 83 keeps waiting and the relay stays alive until the host sends SIGTERM through _BRIDGE_STOP_SCRIPT. Every later connection then completes with no response.

Set the shutdown event on that path so the relay exits by itself.

Separately, Lines 61-62 write to sys.stdout.buffer synchronously on the event loop. A single message can reach 65537 bytes, which exceeds the default pipe buffer, so the write can block the loop until the host drains the pipe. loop.connect_write_pipe with a StreamWriter would keep the write non-blocking.

♻️ Proposed exit on host EOF
                                 if not response:
+                                    response_overrun.set()
                                     return

Also applies to: 80-83

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

In `@src/agent_core/execution/bridge_relay.py` around lines 60 - 74, Update handle
so an empty result from responses.readline() sets response_overrun before
returning, allowing the relay shutdown path to terminate without waiting for
SIGTERM. Replace the synchronous sys.stdout.buffer.write/flush in the response
relay with an asyncio StreamWriter created through loop.connect_write_pipe, and
await its drain so large host responses do not block the event loop.
src/agent_core/tools/sandbox_run_command.py (1)

124-131: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

Wire output.hard_ceiling_multiplier into both output paths. The executor bounds the model and persisted payload before _finish. However, the loaded configuration value is ignored, and both src/agent_core/tools/sandbox_run_command.py and src/agent_core/tools/executor.py hard-code 4.

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

In `@src/agent_core/tools/sandbox_run_command.py` around lines 124 - 131, The
output-size multiplier is hard-coded as 4 in both the ExecutionCommand
construction in sandbox_run_command and the corresponding output handling in
executor.py. Load and reuse the configured output.hard_ceiling_multiplier in
both paths, replacing the literals while preserving the existing bounds
calculations and behavior.
tests/contract/test_artifact_store_contract.py (1)

67-81: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Use dataclasses.replace instead of positional construction.

Both blocks pass 13 positional arguments to ArtifactMetadata only to override one or two fields. Any added or reordered field in ArtifactMetadata silently changes what these tests assert. replace is already imported and used on line 48.

♻️ Proposed refactor
-    broken = ArtifactMetadata(
-        metadata.artifact_id,
-        metadata.tenant_id,
-        metadata.principal_id,
-        metadata.session_id,
-        metadata.run_id,
-        metadata.origin,
-        metadata.filename,
-        metadata.media_type,
-        metadata.size_bytes,
-        "0" * 64,
-        metadata.trust,
-        metadata.created_at,
-        metadata.expires_at,
-    )
+    broken = replace(metadata, sha256="0" * 64)
-    replacement_metadata = ArtifactMetadata(
-        metadata.artifact_id,
-        ...
-        metadata.expires_at,
-    )
+    replacement_metadata = replace(
+        metadata,
+        size_bytes=len(replacement),
+        sha256=hashlib.sha256(replacement).hexdigest(),
+    )

Also applies to: 116-130

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

In `@tests/contract/test_artifact_store_contract.py` around lines 67 - 81, Replace
the positional `ArtifactMetadata` constructions in both affected test blocks
with `dataclasses.replace`, reusing the existing metadata instance and
overriding only the fields needed for each case. Preserve the current altered
values and assertions, following the existing `replace` usage in the test.
src/agent_core/application/artifact_writer.py (1)

25-29: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Offload the spool file I/O off the event loop.

SpooledTemporaryFile rolls over to a real file after 8 MiB, and maximum_bytes allows up to 512 MiB. _file_stream then performs blocking source.read calls on the event loop, and create performs blocking spool.write calls on line 75. FilesystemArtifactStore.put already offloads its writes with asyncio.to_thread, so the read and spool sides are the remaining blocking hops for large artifacts.

Move both to a worker thread.

♻️ Proposed refactor
+import asyncio
+
 async def _file_stream(
     source: _ReadableBytes, chunk_bytes: int = 64 * 1024
 ) -> AsyncIterator[bytes]:
-    while chunk := source.read(chunk_bytes):
+    while chunk := await asyncio.to_thread(source.read, chunk_bytes):
         yield chunk

Apply the same treatment to the spool write:

-                spool.write(chunk)
+                await asyncio.to_thread(spool.write, chunk)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/agent_core/application/artifact_writer.py` around lines 25 - 29, Update
_file_stream to perform each blocking source.read call via asyncio.to_thread
before yielding the chunk, and update create’s spool.write path to use
asyncio.to_thread as well. Preserve the existing chunking and streaming behavior
while ensuring both file I/O operations run off the event loop.
tests/gates/test_artifact_m6.py (1)

299-308: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Exercise artifactization through the public dispatch path.

This gate calls the private pipeline._artifactize_large_output directly. The wiring in _execute_once at src/agent_core/tools/executor.py Lines 785-790, the validate_output ordering, and the _finish persistence of artifact_id, output_bytes, and truncated at Lines 1288-1306 stay uncovered. A regression that stops calling _artifactize_large_output from _execute_once would still pass this gate.

The exact byte assertions add a second problem. 5027, 4000, and 1027 derive from the JSON encoding of a TextPart. Any added field on TextPart changes rendered and breaks all three assertions for a reason unrelated to artifactization. Assert the relationships instead, for example captured_bytes == tool.spec.maximum_output_bytes * 4 and discarded_bytes == output_bytes - captured_bytes.

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

In `@tests/gates/test_artifact_m6.py` around lines 299 - 308, Update the test to
invoke artifactization through the public execution/dispatch path rather than
calling the private pipeline._artifactize_large_output method directly, covering
_execute_once, validate_output ordering, and _finish persistence of artifact_id,
output_bytes, and truncated. Replace hard-coded byte totals with
relationship-based assertions, including captured_bytes as
tool.spec.maximum_output_bytes * 4 and discarded_bytes as output_bytes -
captured_bytes, while preserving assertions that truncation and artifact
metadata are persisted.
src/agent_core/tools/executor.py (2)

754-764: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

Derive the artifact origin from the tool target instead of hard-coding SANDBOX_EXPORT.

Every tool receives a writer bound to ArtifactOrigin.SANDBOX_EXPORT, including tools whose target_kind is not "sandbox". A non-sandbox tool that writes through context.artifacts then persists an artifact row whose origin claims a sandbox export. _artifactize_large_output already selects ArtifactOrigin.TOOL_OUTPUT for its own writer, so the two paths disagree on provenance labeling.

Origin is stored provenance metadata that operators and the API surface read, so it should reflect the actual producer.

♻️ Proposed refactor
                 else self._artifact_writers.for_run(
                     tenant_id=run.tenant_id,
                     principal_id=principal.principal_id,
                     session_id=run.session_id,
                     run_id=run.id,
-                    origin=ArtifactOrigin.SANDBOX_EXPORT,
+                    origin=(
+                        ArtifactOrigin.SANDBOX_EXPORT
+                        if tool.spec.target_kind == "sandbox"
+                        else ArtifactOrigin.TOOL_OUTPUT
+                    ),
                 )
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/agent_core/tools/executor.py` around lines 754 - 764, Update the artifact
writer construction in the executor flow to derive `ArtifactOrigin` from the
tool’s `target_kind` instead of always using `ArtifactOrigin.SANDBOX_EXPORT`;
preserve sandbox exports as sandbox-origin artifacts and assign the appropriate
non-sandbox origin consistently with `_artifactize_large_output`’s
`ArtifactOrigin.TOOL_OUTPUT` path.

882-884: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Extract the duplicated trust-resolution rule.

These three lines repeat the rule at _finish Lines 1277-1279 exactly. Trust resolution decides whether tool output is treated as EXTERNAL_UNTRUSTED, so the two copies must not drift.

Move the rule into one helper and call it from both sites.

♻️ Proposed refactor
+def _effective_trust(result: ToolResult, tool: Tool) -> TrustLevel:
+    if tool.spec.output_trust is TrustLevel.EXTERNAL_UNTRUSTED:
+        return TrustLevel.EXTERNAL_UNTRUSTED
+    return result.output_trust or tool.spec.output_trust

Then at both call sites:

-        trust = result.output_trust or tool.spec.output_trust
-        if tool.spec.output_trust is TrustLevel.EXTERNAL_UNTRUSTED:
-            trust = TrustLevel.EXTERNAL_UNTRUSTED
+        trust = _effective_trust(result, tool)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/agent_core/tools/executor.py` around lines 882 - 884, Extract the
trust-resolution logic into a shared helper near the existing executor
utilities, preserving the rule that tool.spec.output_trust overrides
result.output_trust when it is TrustLevel.EXTERNAL_UNTRUSTED. Replace the
duplicated logic in the current site and _finish with calls to this helper so
both paths use one implementation.
tests/contract/test_artifact_repository_contract.py (1)

42-47: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win

Authorization Bypass (CWE-639): Authorization Bypass Through User-Controlled Key (IDOR)

Reachability: Internal

Add a principal-boundary case.

Assert NotFoundError for tenant-a with principal_id="user-b". This detects regressions that remove the principal_id predicate.

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

In `@tests/contract/test_artifact_repository_contract.py` around lines 42 - 47,
Extend the contract test around repository.get to assert NotFoundError for the
same artifact with Principal(tenant_id="tenant-a", principal_id="user-b"), while
preserving the existing tenant-b case. This verifies lookup remains bound to
both tenant_id and principal_id.
tests/security/test_sandbox_runtime_m6.py (2)

125-130: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Rename the local dictionary so it does not shadow the secrets module.

Line 8 imports secrets. Line 125 binds a local dictionary to the same name. Inside this function the module becomes unreachable. A later edit that calls secrets.token_urlsafe() here raises AttributeError.

♻️ Proposed change
-    secrets = {
+    parent_secrets = {
         "OPENAI_API_KEY": "synthetic-provider-value-7d951",
         "AGENT_DATABASE_URL": "synthetic-database-value-0be44",
         "AWS_SECRET_ACCESS_KEY": "synthetic-cloud-value-18c12",
         "PRIVATE_SERVICE_TOKEN": "synthetic-pattern-value-19d42",
     }

Update the four later references (parent=secrets, set(secrets), secrets.values()) to parent_secrets.

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

In `@tests/security/test_sandbox_runtime_m6.py` around lines 125 - 130, Rename the
local dictionary in the affected test function from secrets to parent_secrets,
and update all four subsequent references—including the parent argument, key-set
access, and values access—so the imported secrets module remains available.

274-277: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Poll for proxy readiness instead of sleeping for a fixed interval.

Line 275 waits 0.25 s for the egress proxy to accept connections. On a loaded CI machine the proxy can still be unready. The allowed assertion at Line 279 then fails for a timing reason, not a policy reason. A bounded readiness poll removes that flake.

♻️ Proposed change
-        await asyncio.sleep(0.25)
+        for _attempt in range(40):
+            probe = await _execute(
+                adapter,
+                handle,
+                "import urllib.request; urllib.request.urlopen('http://example.com:80',timeout=2)",
+            )
+            if probe.exit_code == 0:
+                break
+            await asyncio.sleep(0.25)
+        else:
+            raise AssertionError("egress proxy did not become ready")
         result = await _execute(adapter, handle, script, timeout=12)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/security/test_sandbox_runtime_m6.py` around lines 274 - 277, Replace
the fixed asyncio.sleep in the _environment test flow with a bounded poll that
checks egress proxy readiness before calling _execute. Reuse the existing
adapter/handle readiness mechanism if available, stop when the proxy accepts
connections, and retain a timeout so failures remain deterministic rather than
hanging.
tests/contract/test_execution_environment_contract.py (1)

231-233: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Bound the state busy-wait loops so a regression fails instead of hanging.

Several tests spin on private manager state with while ...: await asyncio.sleep(0) (Lines 231-232, 256-257, 281-282, 307-308, 330-331). If a future change stops setting that state, the loop never exits and the test hangs until the CI job timeout. A bounded helper turns the same regression into a fast failure.

♻️ Proposed helper
async def _await_condition(predicate: Callable[[], bool], *, timeout: float = 1.0) -> None:
    async def _spin() -> None:
        while not predicate():
            await asyncio.sleep(0)

    await asyncio.wait_for(_spin(), timeout)
-    while run_id not in manager._released_runs:
-        await asyncio.sleep(0)
+    await _await_condition(lambda: run_id in manager._released_runs)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/contract/test_execution_environment_contract.py` around lines 231 -
233, Replace the unbounded state-polling loops in the affected contract tests
with a shared bounded async helper such as _await_condition, using
asyncio.wait_for around the existing sleep(0) spin and a short default timeout.
Update each loop that waits on manager state, including _released_runs and the
other listed conditions, while preserving the existing assertions and
predicates.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/agent_core/adapters/artifacts/filesystem.py`:
- Around line 161-164: Update _restore_claim to suppress FileNotFoundError from
os.link in addition to FileExistsError, while preserving the existing
claim.unlink cleanup so concurrent reconcile_orphans passes do not abort when
the claim was already removed.

In `@src/agent_core/adapters/execution/docker.py`:
- Around line 680-707: Update _monitor_workspace_limits to use a substantially
longer polling interval with backoff, retaining prompt shutdown and limit
detection while reducing repeated docker exec calls. Configure workspace volume
quotas where the supported storage driver allows it, using monitoring only as a
safety net. Optimize _snapshot by skipping hashing for unchanged files based on
size and mtime, and avoid hashing files above the applicable workspace-size
bound.

In `@src/agent_core/adapters/execution/local_workspace.py`:
- Around line 216-217: Update LocalWorkspace.for_run to include lease_epoch in
the workspace handle cache key and generated host-directory path, ensuring
reclaimed runs receive distinct scoped workspaces. Add or update the
lease-handoff contract test to verify different lease epochs produce different
handles and directories.

In `@src/agent_core/application/public_services.py`:
- Around line 859-869: The artifact download flow should route using explicit
trajectory membership rather than artifact.origin, and eagerly open/validate the
backing object before creating the streaming response. Update the relevant
method around _artifacts.stream and _general_artifacts.open to use the
established trajectory-membership signal, translate FileNotFoundError to
NotFoundError, and preserve ArtifactIntegrityError while retaining streaming
after successful validation.

In `@src/agent_core/bootstrap.py`:
- Around line 666-668: Throttle store-wide orphan reconciliation by registering
a cadence-aware artifact maintenance service in src/agent_core/bootstrap.py
lines 666-668 instead of passing the raw reconcile_artifact_orphans callback. In
src/agent_core/runtime/worker.py lines 211-215, update
MaintenanceWorker.run_once to invoke orphan reconciliation only when its
separate coarse interval is due; preserve the existing frequent maintenance loop
for other tasks.
- Around line 518-525: Extend the maintenance flow associated with
ArtifactWriterFactory to add an idempotent expiry sweep for general artifacts:
remove expired metadata and their stored bytes based on retention_days, while
preserving reconcile_orphans for incomplete commits. Ensure repeated sweeps are
safe and locate the implementation through ArtifactWriterFactory and the
existing general-artifact maintenance methods.

In `@src/agent_core/execution/proxy.py`:
- Around line 91-109: Update _handle so plaintext HTTP traffic cannot relay
pipelined requests under the initial host authorization: parse each subsequent
request and run _resolved, evaluate_core, and _log for its Host, rejecting
unauthorized or changed hosts, or close both connections after completing only
one request. Preserve CONNECT tunneling behavior while ensuring every permitted
HTTP request is host-scoped and audited.

In `@src/agent_core/runtime/executor.py`:
- Line 64: Update RunCompleteCallback and the execute_claimed completion flow in
src/agent_core/runtime/executor.py (lines 64-64 and 97-118) to pass
claimed.lease alongside run_id; update the completion handler and
SandboxManager.release_run call in lines 225-247 to tear down only the matching
(run_id, lease_epoch), while keeping inline cleanup separate. Update the
callback wiring in src/agent_core/bootstrap.py (line 565) to accept and forward
the lease argument.

In `@src/agent_core/tools/artifact_export.py`:
- Around line 84-123: Update the exception handling in the artifact export flow
around workspace.stream and writer.create to catch ArtifactIntegrityError
alongside WorkspaceReadLimitExceededError, mapping it to the existing
OUTPUT_TOO_LARGE/tool.output_invalid failure response. Add the required
ArtifactIntegrityError import and preserve the current non-retryable detail
behavior.

In `@src/agent_core/tools/bridge.py`:
- Around line 96-113: Update the timeout handling in the bridge call method
around self._ordinal and bridge_call_id so the returned suspension cannot
advertise an invalid retry path: either mark bridge.approval_hold_expired as
non-retryable, or implement compatible ordinal exposure/rollback with explicit
duplicate-dispatch behavior. Preserve unique call-id and ordinal accounting for
subsequent invocations.

In `@src/agent_core/tools/executor.py`:
- Around line 922-930: Update the structured-output handling in _execute_once
and _artifactize_large_output so any stdout or stderr truncation remains
consistent with the declared output_schema and is visible to consumers; either
re-validate the mutated structured value before _finish persists it, or add an
explicit truncation indicator within structured and preserve that through the
returned structured_result.

In `@tests/contract/test_programmatic_bridge_m6.py`:
- Around line 26-31: Replace every literal token passed to
ProgrammaticBridgeSession in tests/contract/test_programmatic_bridge_m6.py at
lines 28, 77, 103, and 125 with a module-level secrets.token_urlsafe(16) value,
and reuse that value in the corresponding request payloads; also replace the
token literal in tests/security/test_sandbox_runtime_m6.py at lines 188-192 with
secrets.token_urlsafe(32).

In `@tests/gates/test_policy_m4.py`:
- Around line 335-338: Update the deadline-sensitive test around
active_run.model_copy to configure a fixed clock when building the application,
ensuring the 250 ms deadline remains stable while dispatch() computes
effective_timeout. Preserve the existing deadline and timeout assertions.

---

Outside diff comments:
In `@src/agent_core/runtime/worker.py`:
- Around line 165-190: Update the sandbox reaping flow invoked by
MaintenanceWorker.run_once and implemented by SandboxManager.reap to revalidate
each candidate’s lease immediately before deletion, rather than relying solely
on live_run_leases captured at the initial snapshot. Preserve deletion for
unclaimed candidates and ensure a concurrent claim prevents deletion. Add a
regression test covering a lease claimed between snapshot and reap.

---

Nitpick comments:
In `@src/agent_core/adapters/execution/fake.py`:
- Around line 50-54: Update FakeWorkspaceHandle.read_bounded to raise ValueError
immediately when maximum_bytes is negative, matching
DockerWorkspaceHandle.read_bounded and stream; retain the existing size-limit
check and return behavior for non-negative limits.

In `@src/agent_core/application/artifact_writer.py`:
- Around line 25-29: Update _file_stream to perform each blocking source.read
call via asyncio.to_thread before yielding the chunk, and update create’s
spool.write path to use asyncio.to_thread as well. Preserve the existing
chunking and streaming behavior while ensuring both file I/O operations run off
the event loop.

In `@src/agent_core/execution/bridge_relay.py`:
- Around line 60-74: Update handle so an empty result from responses.readline()
sets response_overrun before returning, allowing the relay shutdown path to
terminate without waiting for SIGTERM. Replace the synchronous
sys.stdout.buffer.write/flush in the response relay with an asyncio StreamWriter
created through loop.connect_write_pipe, and await its drain so large host
responses do not block the event loop.

In `@src/agent_core/execution/manager.py`:
- Around line 242-254: In the bridge execution branch of the operation manager,
validate that the configured environment supports execute_with_bridge before
casting it to _BridgeExecutionEnvironment. Reject unsupported bridge execution
through the established domain-level failure path, and only construct the
endpoint and invoke execute_with_bridge after the capability check.

In `@src/agent_core/execution/proxy.py`:
- Around line 122-125: Import suppress from contextlib at module scope, then
update the exception-handling block in the proxy response path to use that
imported symbol instead of dynamically importing contextlib via __import__.
Preserve the existing OSError suppression and writer.drain behavior.

In `@src/agent_core/tools/executor.py`:
- Around line 754-764: Update the artifact writer construction in the executor
flow to derive `ArtifactOrigin` from the tool’s `target_kind` instead of always
using `ArtifactOrigin.SANDBOX_EXPORT`; preserve sandbox exports as
sandbox-origin artifacts and assign the appropriate non-sandbox origin
consistently with `_artifactize_large_output`’s `ArtifactOrigin.TOOL_OUTPUT`
path.
- Around line 882-884: Extract the trust-resolution logic into a shared helper
near the existing executor utilities, preserving the rule that
tool.spec.output_trust overrides result.output_trust when it is
TrustLevel.EXTERNAL_UNTRUSTED. Replace the duplicated logic in the current site
and _finish with calls to this helper so both paths use one implementation.

In `@src/agent_core/tools/sandbox_run_command.py`:
- Around line 124-131: The output-size multiplier is hard-coded as 4 in both the
ExecutionCommand construction in sandbox_run_command and the corresponding
output handling in executor.py. Load and reuse the configured
output.hard_ceiling_multiplier in both paths, replacing the literals while
preserving the existing bounds calculations and behavior.

In `@tests/contract/test_artifact_repository_contract.py`:
- Around line 42-47: Extend the contract test around repository.get to assert
NotFoundError for the same artifact with Principal(tenant_id="tenant-a",
principal_id="user-b"), while preserving the existing tenant-b case. This
verifies lookup remains bound to both tenant_id and principal_id.

In `@tests/contract/test_artifact_store_contract.py`:
- Around line 67-81: Replace the positional `ArtifactMetadata` constructions in
both affected test blocks with `dataclasses.replace`, reusing the existing
metadata instance and overriding only the fields needed for each case. Preserve
the current altered values and assertions, following the existing `replace`
usage in the test.

In `@tests/contract/test_execution_environment_contract.py`:
- Around line 231-233: Replace the unbounded state-polling loops in the affected
contract tests with a shared bounded async helper such as _await_condition,
using asyncio.wait_for around the existing sleep(0) spin and a short default
timeout. Update each loop that waits on manager state, including _released_runs
and the other listed conditions, while preserving the existing assertions and
predicates.

In `@tests/gates/test_artifact_m6.py`:
- Around line 299-308: Update the test to invoke artifactization through the
public execution/dispatch path rather than calling the private
pipeline._artifactize_large_output method directly, covering _execute_once,
validate_output ordering, and _finish persistence of artifact_id, output_bytes,
and truncated. Replace hard-coded byte totals with relationship-based
assertions, including captured_bytes as tool.spec.maximum_output_bytes * 4 and
discarded_bytes as output_bytes - captured_bytes, while preserving assertions
that truncation and artifact metadata are persisted.

In `@tests/security/test_sandbox_runtime_m6.py`:
- Around line 125-130: Rename the local dictionary in the affected test function
from secrets to parent_secrets, and update all four subsequent
references—including the parent argument, key-set access, and values access—so
the imported secrets module remains available.
- Around line 274-277: Replace the fixed asyncio.sleep in the _environment test
flow with a bounded poll that checks egress proxy readiness before calling
_execute. Reuse the existing adapter/handle readiness mechanism if available,
stop when the proxy accepts connections, and retain a timeout so failures remain
deterministic rather than hanging.
🪄 Autofix

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: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: ccc7fc16-8e40-4d63-bde3-c4fe0d1933f7

📥 Commits

Reviewing files that changed from the base of the PR and between cdda972 and deb9ed6.

📒 Files selected for processing (66)
  • .circleci/config.yml
  • Makefile
  • docs/adr/0042-milestone-6-sandbox-and-artifact-seams.md
  • docs/adr/index.md
  • docs/plan/current-milestone.md
  • docs/status/project-state.yaml
  • evals/gates/sandbox.yaml
  • execution/sandbox.Dockerfile
  • mkdocs.yml
  • pyproject.toml
  • src/agent_core/adapters/artifacts/filesystem.py
  • src/agent_core/adapters/execution/docker.py
  • src/agent_core/adapters/execution/fake.py
  • src/agent_core/adapters/execution/local_workspace.py
  • src/agent_core/adapters/persistence/mappers.py
  • src/agent_core/adapters/persistence/memory.py
  • src/agent_core/adapters/persistence/repositories.py
  • src/agent_core/adapters/persistence/unit_of_work.py
  • src/agent_core/application/artifact_writer.py
  • src/agent_core/application/public_services.py
  • src/agent_core/bootstrap.py
  • src/agent_core/config.py
  • src/agent_core/domain/artifacts.py
  • src/agent_core/domain/credentials.py
  • src/agent_core/domain/errors.py
  • src/agent_core/domain/execution.py
  • src/agent_core/domain/tools.py
  • src/agent_core/domain/trajectory.py
  • src/agent_core/execution/__init__.py
  • src/agent_core/execution/bridge_relay.py
  • src/agent_core/execution/egress.py
  • src/agent_core/execution/egress_core.py
  • src/agent_core/execution/environment.py
  • src/agent_core/execution/manager.py
  • src/agent_core/execution/proxy.py
  • src/agent_core/ports/artifacts.py
  • src/agent_core/ports/credentials.py
  • src/agent_core/ports/execution.py
  • src/agent_core/ports/persistence.py
  • src/agent_core/ports/repositories.py
  • src/agent_core/runtime/executor.py
  • src/agent_core/runtime/worker.py
  • src/agent_core/sandbox/limits.yaml
  • src/agent_core/tools/artifact_export.py
  • src/agent_core/tools/bridge.py
  • src/agent_core/tools/executor.py
  • src/agent_core/tools/sandbox_run_command.py
  • tests/contract/support.py
  • tests/contract/test_artifact_repository_contract.py
  • tests/contract/test_artifact_store_contract.py
  • tests/contract/test_artifact_writer_contract.py
  • tests/contract/test_artifact_writer_provider_contract.py
  • tests/contract/test_credential_resolver_contract.py
  • tests/contract/test_execution_environment_contract.py
  • tests/contract/test_maintenance_repository_contract.py
  • tests/contract/test_programmatic_bridge_m6.py
  • tests/contract/test_workspace_handle_contract.py
  • tests/gates/test_api_m5.py
  • tests/gates/test_artifact_m6.py
  • tests/gates/test_gate_registry.py
  • tests/gates/test_policy_m4.py
  • tests/gates/test_sandbox_m6.py
  • tests/integration/test_artifact_persistence_m6.py
  • tests/security/test_sandbox_runtime_m6.py
  • tests/unit/test_config.py
  • tests/unit/test_toolchain.py

Comment thread src/agent_core/adapters/artifacts/filesystem.py
Comment thread src/agent_core/adapters/execution/docker.py
Comment thread src/agent_core/adapters/execution/local_workspace.py Outdated
Comment thread src/agent_core/application/public_services.py Outdated
Comment thread src/agent_core/bootstrap.py
Comment thread src/agent_core/tools/artifact_export.py
Comment thread src/agent_core/tools/bridge.py
Comment thread src/agent_core/tools/executor.py Outdated
Comment thread tests/contract/test_programmatic_bridge_m6.py
Comment thread tests/gates/test_policy_m4.py
@avitus

avitus commented Aug 4, 2026

Copy link
Copy Markdown
Owner Author

Full-review remediation is pushed in 8b81ec9.

The outside-diff lease-reaper finding is fixed: maintenance supplies a repository-backed live-lease recheck for each candidate immediately before deletion, and a regression covers a claim acquired after the initial snapshot.

All 14 nitpicks are also addressed: negative fake read bounds; off-loop artifact spool I/O; relay EOF/backpressure; bridge capability checks; module-level suppress; configured hard-ceiling wiring; shared trust resolution; principal scoping; dataclasses.replace fixtures; bounded async test polling; public-dispatch artifactization coverage; non-shadowing secret names; and removal of the redundant fixed proxy sleep because provisioning already performs a bounded readiness poll.

For artifact origin, the implementation preserves the normative plan rule that artifact.export is SANDBOX_EXPORT even though the tool is intentionally in_process. Origin selection is now centralized: artifact.export uses SANDBOX_EXPORT and other tool-created output uses TOOL_OUTPUT.

Final local verification on this commit:

  • make check: 224 static, 100 contract, strict mypy over 239 files, docs and 116 active gates passed
  • PostgreSQL integration/resilience: 69 passed
  • real Docker sandbox: 10 passed
  • 403 non-live tests total

@avitus

avitus commented Aug 4, 2026

Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Aug 4, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

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

⚠️ Outside diff range comments (3)
src/agent_core/adapters/execution/docker.py (1)

1125-1130: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Do not let a live lease override environment expiry.

If expired is true and is_live() returns true, this branch skips cleanup. The container can then remain allocated after its expires_at deadline and bypass the configured sandbox hard cap.

Call is_live() only for non-expired resources.

Proposed fix
-            if not expired and (run_id, lease_epoch) in live_leases:
-                continue
-            if not expired and not old_enough:
-                continue
-            if is_live is not None and await is_live(run_id, lease_epoch):
-                continue
+            if not expired:
+                if (run_id, lease_epoch) in live_leases:
+                    continue
+                if not old_enough:
+                    continue
+                if is_live is not None and await is_live(run_id, lease_epoch):
+                    continue
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/agent_core/adapters/execution/docker.py` around lines 1125 - 1130, Update
the cleanup conditions around live_leases and is_live so is_live(run_id,
lease_epoch) is evaluated only when expired is false; expired resources must
proceed with cleanup regardless of the callback result, while preserving the
existing handling for non-expired leases.
src/agent_core/api/app.py (1)

440-451: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Sensitive Data Exposure (CWE-525): Use of Web Browser Cache Containing Sensitive Information

Reachability: External · Exploitability: Moderate

Reachability path
● Entry
  src/agent_core/bootstrap.py:634
  PublicArtifactService
│
▼
● Hop
  src/agent_core/application/public_services.py:839
  _get_ref: ArtifactRef requires an expiry, and persistence mapping rejects legacy null rows.
│
▼
● Sink
  src/agent_core/api/app.py

Add Cache-Control: private, no-store to both artifact responses.

Authorization does not replace an explicit cache policy. The endpoint returns principal-scoped bytes, but user-agent caches can retain the response. Apply the header to both 200 and 304 responses. No global middleware sets it.

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

In `@src/agent_core/api/app.py` around lines 440 - 451, Update the artifact
response handling around the ETag match so both the 304 Response and the 200
StreamingResponse include Cache-Control: private, no-store in their headers,
while preserving the existing ETag and response behavior.
src/agent_core/execution/proxy.py (1)

94-100: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Sensitive Data Exposure (CWE-319): Cleartext Transmission of Sensitive Information

Reachability: External · Exploitability: Moderate

Reachability path
● Entry
  src/agent_core/execution/bridge_relay.py:40
  handle
│
▼
● Sink
  src/agent_core/execution/proxy.py

Reject non-http absolute URIs in the non-CONNECT path.

For https targets, this path opens a plain TCP connection and forwards headers without TLS. Reject non-http schemes before dialing, or establish TLS with SNI and hostname validation. Add a regression test.

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

In `@src/agent_core/execution/proxy.py` around lines 94 - 100, Update the
non-CONNECT proxy request path around urlsplit(raw_target) to reject absolute
targets whose scheme is not http before opening the TCP connection; do not allow
https or other schemes to proceed without TLS. Add a regression test covering
rejection of a non-http absolute URI.
🧹 Nitpick comments (7)
tests/integration/test_artifact_persistence_m6.py (1)

88-91: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Assert that the unexpired artifact survives the sweep.

The test proves the expired TOOL_OUTPUT artifact is removed. It does not prove the unexpired SANDBOX_EXPORT artifact is retained. PostgresArtifactRepository.list_expired and delete_expired are new SQL in this PR, and their expires_at <= now predicate is the guard that protects live artifacts. A predicate inversion would still pass this test.

Add one assertion for the retained artifact.

💚 Proposed addition
         assert await expiry.sweep_expired() == 1
         assert await expiry.sweep_expired() == 0
         async with composition.uow_factory() as uow:
             assert await uow.artifacts.exists(expired_ref.artifact_id) is False
+            assert await uow.artifacts.exists(ref.artifact_id) is True
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/integration/test_artifact_persistence_m6.py` around lines 88 - 91, Add
an assertion in the existing uow block after the expired artifact check to
verify the unexpired SANDBOX_EXPORT artifact still exists, using its artifact ID
and the same artifacts.exists method.
src/agent_core/adapters/persistence/repositories.py (1)

1465-1489: 🚀 Performance & Scalability | 🔵 Trivial

Consider a partial index for the general-artifact expiry sweep.

Both sweep queries filter on origin != 'trajectory_export' together with expires_at <= now. Only ix_artifacts_expires_at exists today, so PostgreSQL must filter the origin predicate after the index scan. As the trajectory-export share of the table grows, each sweep reads more rows than it returns.

A partial index on expires_at restricted to non-trajectory origins keeps the ordered LIMIT scan tight.

The predicate semantics look correct: expires_at is nullable, and a NULL value makes expires_at <= now unknown, so legacy rows without an expiry are never swept.

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

In `@src/agent_core/adapters/persistence/repositories.py` around lines 1465 -
1489, Add a PostgreSQL partial index on ArtifactRow.expires_at restricted to
rows whose origin is not "trajectory_export", alongside the existing artifact
indexes. Keep the predicate aligned with list_expired and delete_expired so
nullable expires_at values remain excluded and the ordered expiry sweep can use
the index efficiently.
src/agent_core/tools/executor.py (1)

953-980: 🗄️ Data Integrity & Integration | 🔵 Trivial | 💤 Low value

Artifactization replaces any artifact the tool already produced.

The model_copy update sets artifacts to a single-element list holding the output artifact. It discards every entry the tool returned. _finish then reads result.artifacts[0] at Line 1309 and persists that id as invocation.artifact_id, so a tool that produced its own artifact loses that reference whenever its rendered output exceeds the budget.

artifact.export is the only current artifact-producing tool, and its structured output is small, so the oversized branch is not reached today. Append instead of replace, so the contract stays correct for the next artifact-producing tool.

♻️ Proposed change
-                "artifacts": [
-                    {
-                        "artifact_id": str(ref.artifact_id),
-                        "sha256": ref.sha256,
-                        "size_bytes": ref.size_bytes,
-                        "media_type": ref.media_type,
-                    }
-                ],
+                "artifacts": [
+                    *result.artifacts,
+                    {
+                        "artifact_id": str(ref.artifact_id),
+                        "sha256": ref.sha256,
+                        "size_bytes": ref.size_bytes,
+                        "media_type": ref.media_type,
+                    },
+                ],

If you apply this change, note that _finish still records only the first entry. Decide whether invocation.artifact_id should track the tool's own artifact or the truncated-output artifact, and select that entry explicitly rather than by position.

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

In `@src/agent_core/tools/executor.py` around lines 953 - 980, Update the
oversized-output branch in the executor’s model_copy artifact handling to append
the truncated-output artifact to the existing result.artifacts list instead of
replacing it. Then update _finish to explicitly select the intended artifact
entry for invocation.artifact_id, preserving the tool-produced artifact
reference while consistently choosing whether the invocation tracks that
artifact or the truncated-output artifact.
tests/contract/test_trajectory_artifact_store_contract.py (1)

44-58: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Assert the eager-failure contract of open_verified directly.

src/agent_core/api/app.py awaits content.open() before it constructs StreamingResponse, so the API depends on open_verified raising before any chunk is produced. This test only reaches that behavior through stream, where iteration and verification are indistinguishable. A regression that moved verification back into the iterator would still pass.

Add one assertion that awaiting open_verified raises without iterating.

💚 Proposed addition
     with pytest.raises(ArtifactIntegrityError, match="digest or size"):
         _ = [chunk async for chunk in store.stream(stored)]
+    with pytest.raises(ArtifactIntegrityError, match="digest or size"):
+        await store.open_verified(stored)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/contract/test_trajectory_artifact_store_contract.py` around lines 44 -
58, Extend test_trajectory_artifact_store_rejects_integrity_drift to call and
await open_verified on the tampered stored artifact, asserting
ArtifactIntegrityError with the existing “digest or size” match before any
iteration occurs. Keep the stream assertion as-is to continue covering chunked
reads.
src/agent_core/application/artifact_writer.py (2)

113-118: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

The rollback delete can mask the original persistence failure.

If uow.artifacts.create fails and self._store.delete then raises, the storage error replaces the persistence error that the caller needs to see. The except BaseException clause also catches asyncio.CancelledError, and an await during cancellation is not guaranteed to complete, so the orphaned bytes may remain anyway.

Suppress and log the rollback error, then re-raise the original exception.

♻️ Proposed change
         try:
             async with self._uow_factory() as uow:
                 await uow.artifacts.create(artifact)
         except BaseException:
-            await self._store.delete(stored, tenant_id=self._tenant_id)
+            try:
+                await self._store.delete(stored, tenant_id=self._tenant_id)
+            except Exception:
+                logger.exception(
+                    "artifact_rollback_delete_failed",
+                    extra={"artifact_id": str(artifact_id)},
+                )
             raise

The orphan reconciliation sweep referenced in runtime/worker.py remains the backstop for bytes that survive a failed rollback.

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

In `@src/agent_core/application/artifact_writer.py` around lines 113 - 118, Update
the exception handling around the UoW artifact creation in the artifact-writing
method to preserve and re-raise the original persistence exception. Attempt
self._store.delete as rollback, but catch, suppress, and log any rollback
failure; avoid allowing cancellation handling to replace the original error,
while leaving orphan cleanup to the existing reconciliation sweep.

178-186: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Mark the broad except as intentional for Ruff.

Ruff reports BLE001 on Line 182. The broad catch is correct here, because the loop must continue and aggregate every per-artifact failure into the ExceptionGroup. Add a scoped suppression with a reason so the lint gate stays green and the intent stays documented.

♻️ Proposed change
-            except Exception as exc:
+            # Aggregate every per-artifact failure; one bad artifact must not stop the sweep.
+            except Exception as exc:  # noqa: BLE001
                 failures.append(exc)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/agent_core/application/artifact_writer.py` around lines 178 - 186, In the
expired-artifact deletion loop around the broad exception handler in the
artifact writer method, add a narrowly scoped Ruff BLE001 suppression with a
clear reason documenting that failures are collected in failures so processing
continues and the final ExceptionGroup reports them together.

Source: Linters/SAST tools

tests/gates/test_artifact_m6.py (1)

395-401: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Pin the hard-ceiling multiplier in the test instead of relying on the default.

Line 441 computes captured_bytes = tool.spec.maximum_output_bytes * 4. The 4 mirrors the default hard_ceiling_multiplier in ToolPipeline.__init__, but this ToolPipeline never passes that argument. If the default changes, the test fails with an opaque byte-count mismatch rather than pointing at the multiplier.

Pass the multiplier explicitly and derive the expected size from it.

💚 Proposed change
+        hard_ceiling_multiplier = 4
         pipeline = ToolPipeline(
             registry,
             composition.uow_factory,
             composition.clock,
             SequenceIdFactory([UUID(int=31_000)]),
             artifact_writers=writers,
+            hard_ceiling_multiplier=hard_ceiling_multiplier,
         )
-        captured_bytes = tool.spec.maximum_output_bytes * 4
+        captured_bytes = tool.spec.maximum_output_bytes * hard_ceiling_multiplier

Also applies to: 441-443

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

In `@tests/gates/test_artifact_m6.py` around lines 395 - 401, Update the
ToolPipeline construction in the test to pass an explicit hard-ceiling
multiplier, then use that same multiplier when computing captured_bytes instead
of the literal 4. Keep the expected byte-count assertion aligned with the
configured value so changes to ToolPipeline.__init__ defaults do not affect this
test implicitly.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/agent_core/adapters/execution/docker.py`:
- Line 867: Update the provisioning flow around provision() and the before =
dict(state.snapshot) assignment to capture the initial filesystem snapshot after
/workspace/.agent-initialized is created and before the first command executes.
Ensure state.snapshot is populated from that baseline so the marker is not
reported as CREATED or assigned SANDBOX_WRITTEN provenance.

In `@src/agent_core/execution/proxy.py`:
- Around line 111-129: Update _handle() to reject repeated Content-Length
headers and accept only a single ASCII-decimal value before assigning
content_length, rather than forwarding multiple values or relying on int()
alone. Ensure invalid or duplicate headers raise ValueError before
asyncio.open_connection() is called, and add a regression test covering
rejection at that point.

---

Outside diff comments:
In `@src/agent_core/adapters/execution/docker.py`:
- Around line 1125-1130: Update the cleanup conditions around live_leases and
is_live so is_live(run_id, lease_epoch) is evaluated only when expired is false;
expired resources must proceed with cleanup regardless of the callback result,
while preserving the existing handling for non-expired leases.

In `@src/agent_core/api/app.py`:
- Around line 440-451: Update the artifact response handling around the ETag
match so both the 304 Response and the 200 StreamingResponse include
Cache-Control: private, no-store in their headers, while preserving the existing
ETag and response behavior.

In `@src/agent_core/execution/proxy.py`:
- Around line 94-100: Update the non-CONNECT proxy request path around
urlsplit(raw_target) to reject absolute targets whose scheme is not http before
opening the TCP connection; do not allow https or other schemes to proceed
without TLS. Add a regression test covering rejection of a non-http absolute
URI.

---

Nitpick comments:
In `@src/agent_core/adapters/persistence/repositories.py`:
- Around line 1465-1489: Add a PostgreSQL partial index on
ArtifactRow.expires_at restricted to rows whose origin is not
"trajectory_export", alongside the existing artifact indexes. Keep the predicate
aligned with list_expired and delete_expired so nullable expires_at values
remain excluded and the ordered expiry sweep can use the index efficiently.

In `@src/agent_core/application/artifact_writer.py`:
- Around line 113-118: Update the exception handling around the UoW artifact
creation in the artifact-writing method to preserve and re-raise the original
persistence exception. Attempt self._store.delete as rollback, but catch,
suppress, and log any rollback failure; avoid allowing cancellation handling to
replace the original error, while leaving orphan cleanup to the existing
reconciliation sweep.
- Around line 178-186: In the expired-artifact deletion loop around the broad
exception handler in the artifact writer method, add a narrowly scoped Ruff
BLE001 suppression with a clear reason documenting that failures are collected
in failures so processing continues and the final ExceptionGroup reports them
together.

In `@src/agent_core/tools/executor.py`:
- Around line 953-980: Update the oversized-output branch in the executor’s
model_copy artifact handling to append the truncated-output artifact to the
existing result.artifacts list instead of replacing it. Then update _finish to
explicitly select the intended artifact entry for invocation.artifact_id,
preserving the tool-produced artifact reference while consistently choosing
whether the invocation tracks that artifact or the truncated-output artifact.

In `@tests/contract/test_trajectory_artifact_store_contract.py`:
- Around line 44-58: Extend
test_trajectory_artifact_store_rejects_integrity_drift to call and await
open_verified on the tampered stored artifact, asserting ArtifactIntegrityError
with the existing “digest or size” match before any iteration occurs. Keep the
stream assertion as-is to continue covering chunked reads.

In `@tests/gates/test_artifact_m6.py`:
- Around line 395-401: Update the ToolPipeline construction in the test to pass
an explicit hard-ceiling multiplier, then use that same multiplier when
computing captured_bytes instead of the literal 4. Keep the expected byte-count
assertion aligned with the configured value so changes to ToolPipeline.__init__
defaults do not affect this test implicitly.

In `@tests/integration/test_artifact_persistence_m6.py`:
- Around line 88-91: Add an assertion in the existing uow block after the
expired artifact check to verify the unexpired SANDBOX_EXPORT artifact still
exists, using its artifact ID and the same artifacts.exists method.
🪄 Autofix

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: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 8b7f03d4-e57d-4fc3-82b0-cd9179451487

📥 Commits

Reviewing files that changed from the base of the PR and between deb9ed6 and 8b81ec9.

📒 Files selected for processing (40)
  • docs/adr/0042-milestone-6-sandbox-and-artifact-seams.md
  • docs/plan/current-milestone.md
  • docs/status/project-state.yaml
  • src/agent_core/adapters/artifacts/filesystem.py
  • src/agent_core/adapters/artifacts/local.py
  • src/agent_core/adapters/execution/docker.py
  • src/agent_core/adapters/execution/fake.py
  • src/agent_core/adapters/execution/local_workspace.py
  • src/agent_core/adapters/persistence/memory.py
  • src/agent_core/adapters/persistence/repositories.py
  • src/agent_core/api/app.py
  • src/agent_core/application/artifact_writer.py
  • src/agent_core/application/public_services.py
  • src/agent_core/bootstrap.py
  • src/agent_core/domain/views.py
  • src/agent_core/execution/bridge_relay.py
  • src/agent_core/execution/manager.py
  • src/agent_core/execution/proxy.py
  • src/agent_core/ports/artifacts.py
  • src/agent_core/ports/repositories.py
  • src/agent_core/runtime/executor.py
  • src/agent_core/runtime/worker.py
  • src/agent_core/tools/artifact_export.py
  • src/agent_core/tools/bridge.py
  • src/agent_core/tools/executor.py
  • src/agent_core/tools/sandbox_run_command.py
  • tests/contract/test_artifact_repository_contract.py
  • tests/contract/test_artifact_store_contract.py
  • tests/contract/test_execution_environment_contract.py
  • tests/contract/test_maintenance_repository_contract.py
  • tests/contract/test_programmatic_bridge_m6.py
  • tests/contract/test_trajectory_artifact_store_contract.py
  • tests/contract/test_workspace_factory_contract.py
  • tests/gates/test_artifact_m6.py
  • tests/gates/test_policy_m4.py
  • tests/gates/test_proxy_m6.py
  • tests/gates/test_sandbox_m6.py
  • tests/integration/test_artifact_persistence_m6.py
  • tests/integration/test_event_runtime_m2.py
  • tests/security/test_sandbox_runtime_m6.py
🚧 Files skipped from review as they are similar to previous changes (16)
  • docs/status/project-state.yaml
  • src/agent_core/tools/sandbox_run_command.py
  • src/agent_core/tools/artifact_export.py
  • src/agent_core/ports/artifacts.py
  • src/agent_core/execution/bridge_relay.py
  • src/agent_core/tools/bridge.py
  • tests/contract/test_artifact_repository_contract.py
  • src/agent_core/adapters/persistence/memory.py
  • src/agent_core/adapters/artifacts/filesystem.py
  • src/agent_core/adapters/execution/fake.py
  • docs/adr/0042-milestone-6-sandbox-and-artifact-seams.md
  • src/agent_core/execution/manager.py
  • src/agent_core/bootstrap.py
  • tests/gates/test_policy_m4.py
  • tests/contract/test_artifact_store_contract.py
  • tests/security/test_sandbox_runtime_m6.py

Comment thread src/agent_core/adapters/execution/docker.py
Comment thread src/agent_core/execution/proxy.py
@avitus

avitus commented Aug 4, 2026

Copy link
Copy Markdown
Owner Author

Incremental-review remediation is pushed in 65daecd.

All three outside-diff findings are fixed: expired environments now bypass lease preservation and honor the sandbox hard cap; both 200 and 304 artifact responses carry Cache-Control: private, no-store; and plaintext proxy requests reject every non-http absolute URI before resolution or dialing.

All seven nitpicks are addressed as well: the PostgreSQL expiry test proves the unexpired artifact survives; revision f2a6d74b9c10 adds a partial general-artifact expiry index and round-trips cleanly; oversized output preserves tool-produced artifact references and explicitly selects the appended truncated-output artifact for invocation metadata; eager trajectory integrity failure is tested directly; rollback deletion can no longer mask the original metadata failure; the aggregate sweep catch is documented and scoped; and the hard-ceiling test pins and reuses its configured multiplier.

Local verification on 65daecd:

  • make check: 226 static, 100 contract, strict mypy, docs and 116 gates passed
  • PostgreSQL integration/resilience: 69 passed, including the new migration round trip and metadata drift check
  • real Docker sandbox: 10 passed, including baseline provenance and hard-expiry-over-live-lease assertions
  • 405 non-live tests total

@avitus

avitus commented Aug 4, 2026

Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Aug 4, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@avitus
avitus merged commit 259ad43 into dev Aug 4, 2026
6 checks passed
@avitus
avitus deleted the codex-milestone-6 branch August 4, 2026 11:57
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