-
Notifications
You must be signed in to change notification settings - Fork 0
feat(agentctl): validate packet completion from runtime evidence #13
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
63df92f
369eb96
b169974
2a2f30e
bbeb98b
b53e2c6
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -875,6 +875,15 @@ def v2_run_for_bead( | |
| "request_id": request_id, | ||
| "assignment_ref": parent_assignment_ref, | ||
| } | ||
| metadata = bead.get("metadata") | ||
| encoded_scope = metadata.get("write_scope") if isinstance(metadata, Mapping) else None | ||
| if isinstance(encoded_scope, str): | ||
| try: | ||
| write_scope = json.loads(encoded_scope) | ||
| except json.JSONDecodeError: | ||
| write_scope = None | ||
| if isinstance(write_scope, list): | ||
| binding["write_scope"] = write_scope | ||
|
Comment on lines
+885
to
+886
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
For a Bead with a valid Useful? React with 👍 / 👎. |
||
| assigned_context = { | ||
| "bead": bead, | ||
| "project_ref": project_ref, | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -134,7 +134,7 @@ def start_agent( | |
| if not self.native_runner.is_file() or not os.access(self.native_runner, os.X_OK): | ||
| raise ContractError("native agent runner is unavailable") | ||
| checkout = self.projects.checkout(project_id, checkout_id) | ||
| binding = self._bead_binding(bead_binding, checkout) | ||
| binding = self.bead_binding(bead_binding, checkout) | ||
| job_id = str(uuid4()) | ||
| prompt_path = self.inputs_root / f"{job_id}.prompt" | ||
| public_contract = { | ||
|
|
@@ -240,19 +240,35 @@ def _start( | |
| return response | ||
|
|
||
| @staticmethod | ||
| def _bead_binding( | ||
| def bead_binding( | ||
| value: Mapping[str, Any] | None, checkout: RegisteredCheckout | ||
| ) -> dict[str, Any] | None: | ||
| """Validate public Beads provenance carried by an attested agent job.""" | ||
| """Validate public Beads provenance frozen into a packet job contract.""" | ||
| if value is None: | ||
| return None | ||
| expected = { | ||
| "bead_ref", "project_ref", "checkout_ref", "task_revision", | ||
| "task_etag", "claim_ref", "claim_receipt", "request_id", "assignment_ref", | ||
| } | ||
| if not isinstance(value, Mapping) or set(value) != expected: | ||
| allowed = expected | {"write_scope"} | ||
| if not isinstance(value, Mapping) or (set(value) != expected and set(value) != allowed): | ||
| raise ContractError("agent bead binding is malformed") | ||
| binding = dict(value) | ||
| scope = binding.get("write_scope") | ||
| if "write_scope" in binding and ( | ||
| not isinstance(scope, list) | ||
| or not scope | ||
| or len(scope) > 128 | ||
| or any( | ||
| not isinstance(path, str) | ||
| or not path | ||
| or len(path.encode()) > 1024 | ||
|
Comment on lines
+261
to
+265
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
A valid binding may contain 128 paths of up to 1024 bytes each, but packet sealing caps the entire JSON envelope at Useful? React with 👍 / 👎. |
||
| or path.startswith("/") | ||
| or ".." in Path(path).parts | ||
| for path in scope | ||
| ) | ||
| ): | ||
| raise ContractError("agent Beads write scope is malformed") | ||
| project_ref = f"sinnix://projects/{checkout.project_id}" | ||
| checkout_ref = f"{project_ref}/checkouts/{checkout.checkout_id}" | ||
| bead_prefix = f"{project_ref}/beads/" | ||
|
|
@@ -293,7 +309,9 @@ def _bead_binding( | |
| UUID(str(binding["request_id"])) | ||
| except (TypeError, ValueError, AttributeError) as error: | ||
| raise ContractError("agent bead binding request_id is malformed") from error | ||
| return binding | ||
| # The caller retains its request object. Persist an independent JSON value so | ||
| # neither it nor a nested claim receipt can mutate a launched job's binding. | ||
| return json.loads(json.dumps(binding, sort_keys=True, separators=(",", ":"))) | ||
|
|
||
| def _environment( | ||
| self, checkout: RegisteredCheckout, job_id: str, principal: str, timeout_seconds: int | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When the Bead contains a
write_scopevalue 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 👍 / 👎.