Skip to content

feat(agentctl): validate packet completion from runtime evidence - #13

Closed
Sinity wants to merge 6 commits into
masterfrom
feature/agents/packet-completion
Closed

feat(agentctl): validate packet completion from runtime evidence#13
Sinity wants to merge 6 commits into
masterfrom
feature/agents/packet-completion

Conversation

@Sinity

@Sinity Sinity commented Aug 25, 2026

Copy link
Copy Markdown
Owner

Summary

  • Add a typed packet-completion seal based on runtime, workspace, Git, verifier, and delivery evidence.
  • Validate the exact packet start-to-final change range, declared write scope, and deletion obligations.
  • Fail closed on malformed or incomplete result artifacts without creating a second task authority.

Problem

A successful worker exit and prose summary did not prove that a packet was committed, clean, within its declared scope, verified at the exact final head, or ready for delivery. That left campaign coordination dependent on manual reconstruction and allowed malformed result artifacts to escape the typed runner boundary.

Solution

AgentCTL now derives packet completion from its existing job and workspace state. The seal binds the packet start and final heads, computes the actual changed range, checks exact path and deletion obligations, requires the declared verifier and delivery evidence, and revalidates the state after publication. Gateway plumbing carries the packet contract without taking ownership of Beads or campaign scheduling.

Malformed JSON, invalid UTF-8, null or non-list write scopes, dirty workspaces, omitted or extra paths, missing deletions, stale verification, and delivery drift produce bounded typed failures. The documentation describes this as a packet execution primitive rather than a campaign engine.

Verification

  • nix build --accept-flake-config .#sinnixd --no-link — completed successfully at b53e2c663f40bc06bf883697c5ecc6594f3d4da1.
  • Focused packet-completion package — 280 passed, 1 skipped during independent review of the implementation and hardening series.
  • Invalid UTF-8 regression test — 1 passed at the final correction head.
  • Independent exact-head review — PASS after direct invalid UTF-8 and malformed-JSON probes, plus reproduction of the pre-fix uncaught exception.

Sinity and others added 6 commits August 25, 2026 04:42
1. docs/sinnixd.md: Clarify that packet scope covers the immutable
   start→sealed-final range; start/final heads come from the two job
   records, not from fields in the Beads binding; GitHub branch
   protection owns review/check authority, not AgentCTL.

2. contracts.py: Reject write_scope: null at the gateway/launch
   boundary. The prior guard used `if scope is not None`, which silently
   accepted null and created a scope-less packet. The fix checks
   `if "write_scope" in binding` so any present value must be a valid
   non-empty path list.

3. delivery.py: Deletion evidence must exactly equal the deleted path
   set for the packet range. The prior check allowed overclaims
   (set(deletions) >= deleted); the fix requires set(deletions) ==
   deleted so unrelated paths are also rejected.

4. runner.py: Preserve the underlying cause when _seal_packet_result
   cannot read or parse the worker result. The prior code set
   delivery = None on OSError or JSONDecodeError, losing the reason.
   The fix re-raises as RunnerError with a typed, non-sensitive message
   using the existing bounded error mechanism.

5. test_service.py: Add a composed test that runs the real runner seal
   through exact-head verification into delivery validation, proving one
   accepted add/delete range and one tampered sealed envelope rejection.
   Reuses existing fixtures and helpers; no new harness.

Inherited failures: test_agent_runner_revalidates_checkout_and_writes_
a_bounded_result_fixture (SINNIX env identity), test_workspace_restack_
detaches_child_after_squash_equivalent_parent_disappears (git merge
error), test_real_user_systemd_service_cgroup_cancels_descendants
(systemd/cgroup access) — all pre-exist on 2a2f30e.

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

coderabbitai Bot commented Aug 25, 2026

Copy link
Copy Markdown

Important

  • 🔍 Trigger review

This repository does not receive automatic reviews because it has fewer than 10 stars.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 13c45db8-b83a-46b5-bf36-116ffb822b56


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: b53e2c663f

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

principal=principal,
timeout_seconds=operation.timeout_seconds,
checkout=checkout.to_dict() if checkout is not None else None,
contract=dict(contract),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Include packet bindings in the verification cache identity

When a bound verification runs on a tree whose tree+environment result is already cached—or whose identical verification is active—_start_declared_locked returns that existing record before this new contract is stored. The returned verifier therefore carries an absent or different Beads binding, and _packet rejects it against the packet job, making valid packet delivery fail whenever the verification cache is warm. Include the binding in cache/coalescing identity or bypass reuse for bound verification jobs.

Useful? React with 👍 / 👎.

Comment on lines 53 to +54
review = self._review_after_push(workspace_id)
return {**review, "published": True, "created": created, "publication_output": publication_output}
return {**review, "published": True, "created": created, "publication_output": publication_output, "completion": receipt}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Revalidate completion after inspecting the published review

If the workspace becomes dirty after the post-push _verified_workspace call but before or during PR creation/review reconciliation, review_status still succeeds because it checks only the Git HEAD, and this returns the earlier completion receipt without rerunning the clean/scope/deletion checks. This contradicts the new delivery guarantee that the complete precondition is repeated after review inspection and can hand a closure consumer a completion artifact for a workspace that is no longer clean; rerun _verified_workspace after _review_after_push before returning.

Useful? React with 👍 / 👎.

Comment on lines +261 to +265
or len(scope) > 128
or any(
not isinstance(path, str)
or not path
or len(path.encode()) > 1024

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Bound the aggregate write scope to the result artifact limit

A valid binding may contain 128 paths of up to 1024 bytes each, but packet sealing caps the entire JSON envelope at MAX_RESULT_BYTES (64,000 bytes), and deletion evidence must repeat every deleted path. Consequently, a packet deleting enough individually valid scoped paths—such as 128 paths around 500 bytes—can never seal successfully even with a correct report. Validate the aggregate encoded size against the envelope budget or reduce these per-entry/count limits.

Useful? React with 👍 / 👎.

Comment on lines +885 to +886
if isinstance(write_scope, list):
binding["write_scope"] = write_scope

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Tell scoped workers to emit the required delivery JSON

For a Bead with a valid write_scope, this addition makes the runner parse the agent's last message as JSON and later requires an exact five-field delivery schema, but the gateway prompt still merely asks the worker to “report evidence plus residuals” and neither supplies the field names nor requests JSON. Since no managed skill or other prompt surface defines this schema, normal scoped gateway launches will return prose and fail in _seal_packet_result even after successfully completing the task; include the exact output contract in the packet prompt.

Useful? React with 👍 / 👎.

Comment on lines +880 to +885
if isinstance(encoded_scope, str):
try:
write_scope = json.loads(encoded_scope)
except json.JSONDecodeError:
write_scope = None
if isinstance(write_scope, list):

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Reject malformed Beads write scopes instead of dropping them

When the Bead contains a write_scope value that is invalid JSON or decodes to anything other than a list, this code silently omits the scope and launches an ordinary unsealed agent job. That is a fail-open downgrade of an explicitly declared packet policy: the worker runs without the packet result contract or eventual scope evidence, rather than receiving the bounded typed failure promised for malformed scopes. Reject the launch when a present scope cannot be decoded and validated.

AGENTS.md reference: AGENTS.md:L40-L42

Useful? React with 👍 / 👎.

Comment on lines +312 to +315
packet_job_id = arguments.get("packet_job_id")
return self.delivery.publish(
*publish_arguments,
**({"packet_job_id": packet_job_id} if isinstance(packet_job_id, str) else {}),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Reject non-string packet job identifiers

If a direct API caller includes packet_job_id with a non-string value, both publication routes accept the field but omit it from the call, silently executing ordinary delivery without any packet binding, scope, or worker-report validation. A malformed packet-aware request should fail rather than publish or land through the less restrictive fallback; validate a present identifier with _job_argument before dispatching.

Useful? React with 👍 / 👎.

@Sinity

Sinity commented Aug 25, 2026

Copy link
Copy Markdown
Owner Author

Superseded by direct master integration in 68da68f. The packet hardening behavior and coverage are present in the evolved runtime implementation, and master passed the full default check.

@Sinity Sinity closed this Aug 25, 2026
@Sinity
Sinity deleted the feature/agents/packet-completion branch August 25, 2026 19:03
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