From 75ebe7f5c9136788a9f83cf6d632a42cc6470692 Mon Sep 17 00:00:00 2001 From: cyurekli Date: Sun, 13 Sep 2026 14:27:34 +0200 Subject: [PATCH 1/2] Release v0.19.0: qualify AGY 1.2.2 and native repair --- .codex-plugin/plugin.json | 2 +- PRIVACY.md | 20 +- benchmarks/v1/portable-source.json | 2 +- compat/agy-distribution-manifest.json | 6 +- compat/agy-last-reviewed.txt | 2 +- compat/agy-model-effort-matrix.json | 4 +- compat/agy-model-effort-matrix.sha256 | 2 +- compat/agy-models-inventory-binding.json | 10 +- compat/agy-models-inventory-binding.sha256 | 2 +- compat/agy-upstream-head.txt | 2 +- compat/agy-verified-version.txt | 2 +- compat/agy-version-manifest.json | 55 +- compat/agy-version-manifest.sha256 | 2 +- compat/reviews/agy-1.2.2-activation.md | 95 ++ compat/reviews/agy-1.2.2-candidate.md | 118 +++ compat/sources.md | 12 +- docs/INSTALLATION.md | 10 +- docs/REPO_MAP.md | 20 +- docs/ROADMAP.md | 48 +- scripts/codex_usage_report.py | 2 +- skills/agy-worker/SKILL.md | 191 ++-- .../PROJECT_LIFECYCLE_AND_VERIFICATION.md | 40 + .../references/SECURITY_AND_COMPATIBILITY.md | 37 +- .../benchmarks/v1/portable-source.json | 2 +- .../runtime/compat/agy-last-reviewed.txt | 2 +- .../compat/agy-model-effort-matrix.json | 4 +- .../compat/agy-model-effort-matrix.sha256 | 2 +- .../compat/agy-models-inventory-binding.json | 10 +- .../agy-models-inventory-binding.sha256 | 2 +- .../runtime/compat/agy-upstream-head.txt | 2 +- .../runtime/compat/agy-verified-version.txt | 2 +- .../runtime/compat/agy-version-manifest.json | 55 +- .../compat/agy-version-manifest.sha256 | 2 +- skills/agy-worker/runtime/doctor.sh | 2 +- skills/agy-worker/runtime/ground-truth.sh | 14 +- .../schemas/model-selection.schema.json | 12 +- .../runtime/scripts/agy_dispatch.py | 103 ++- .../scripts/agy_dispatch_containment.py | 469 +++++++++- .../runtime/scripts/codex_usage_report.py | 2 +- .../runtime/scripts/compatibility.py | 14 +- .../agy_worker_remediation_recovery_cases.py | 5 + ...rker_remediation_runtime_boundary_cases.py | 14 +- tests/test-agy-1-1-22-activation.py | 51 +- tests/test-agy-worker-remediation.py | 383 +++++++- tests/test-agy-worker.sh | 14 +- tests/test-doctor.sh | 24 +- tests/test-evidence-receipt.sh | 2 +- tests/test-model-evidence-campaign.py | 2 +- tests/test-official-distribution.py | 18 +- tests/test-packaging.sh | 72 +- tests/test-provider-containment.py | 826 +++++++++++++++++- tests/test-update.sh | 12 +- tests/test-version-manifest-engine.py | 36 +- tests/test-workflow-integration.py | 2 +- 54 files changed, 2462 insertions(+), 382 deletions(-) create mode 100644 compat/reviews/agy-1.2.2-activation.md create mode 100644 compat/reviews/agy-1.2.2-candidate.md diff --git a/.codex-plugin/plugin.json b/.codex-plugin/plugin.json index 6fab018..5d2d531 100644 --- a/.codex-plugin/plugin.json +++ b/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "codex-agy-worker", - "version": "0.18.0", + "version": "0.19.0", "description": "Use Antigravity CLI for exploration, feature work, and project-scale implementation, with Codex independently verifying results.", "author": { "name": "cagdasyurekli", diff --git a/PRIVACY.md b/PRIVACY.md index b5768d7..08935e0 100644 --- a/PRIVACY.md +++ b/PRIVACY.md @@ -40,10 +40,24 @@ with private HOME/TMP and never falls back to session mode. In native mode, the reviewed Keychain service access; this is not a recipient allowlist. On macOS, the listener rule also permits wildcard binds, so the agy image can expose a listener to the local network. Outbound connections to local TCP services remain denied. -The exact `/usr/bin/security` helper shares the reviewed Keychain service access, -without additional network or filesystem access. This permission cannot be limited +The bound agy image can inspect metadata and existence of its executable's exact +parent directory for Core Foundation SSL initialization. This adds no directory +listing or sibling-file access and does not transfer to a different executed image. +The same image can read metadata for ancestors of its private HOME so SQLite can +resolve conversation database paths. This grants no ancestor listing or file data; +other executable images and self-verification receive no such exception. +Native preparation expresses the approved staged read/write scope in a generated +private AGY settings file. It copies no user settings and grants no commands, +URLs, MCP tools, or ambient paths; the native filesystem boundary remains in force. + +The exact `/usr/bin/security` helper shares the reviewed Keychain service access +and can read the bound default Keychain file. This file rule grants no provider, +other-executable, directory, write, or additional network access. Before native +launch, the driver reads only the default Keychain locator and file identity, then +creates a minimal locator preference in the private provider HOME; it does not copy +owner preferences or credentials. The existing service permission cannot be limited to one token or operation; disclose broader same-user Keychain read/change/delete -authority in the initial approval package. +authority and the helper's exact file access in the initial approval package. The stage is writable, while reconciliation enforces its approved write subset. Process-group cleanup and trusted-local-owner limits are described in [Security and compatibility](skills/agy-worker/references/SECURITY_AND_COMPATIBILITY.md). diff --git a/benchmarks/v1/portable-source.json b/benchmarks/v1/portable-source.json index 97051e0..84942d1 100644 --- a/benchmarks/v1/portable-source.json +++ b/benchmarks/v1/portable-source.json @@ -1 +1 @@ -{"files":[{"mode":"100755","path":"benchmark.sh","sha256":"7e0033f6bf3eec1e6007752ef67e8e33db8f39c7136790a5403d651b39eeab8d"},{"mode":"100755","path":"qa-gate.sh","sha256":"878cf09fbc982b70895f8bf1335892d7291a56616434f64b58334c91c8f87d3f"},{"mode":"100755","path":"verify-job.sh","sha256":"b719ebac651b83d27d6fe2af47a0d454a43a4ab649afe98093fcc13d9164073c"},{"mode":"100755","path":"scripts/benchmark.py","sha256":"a6e8527194b6394767d173d66610a6f57e427438765e955741a96239308d6c2e"},{"mode":"100755","path":"scripts/candidate_state.py","sha256":"de6c5c8006ac641a29b37bb2aaf17b4fe0d00d6844806ba382aa19ce6852efa7"},{"mode":"100755","path":"scripts/compatibility.py","sha256":"c7366f2b11864c6f6000bb0c274269e86e47b59a4d76ea06835863a5e222cd80"},{"mode":"100755","path":"scripts/evidence_receipt.py","sha256":"eaa1878355c541bdd7e5f3411002b6e4bb4860f7989f0397ba9237aaec76c3c6"},{"mode":"100755","path":"scripts/model_selection.py","sha256":"9e0be61a5a5900a2f11c7c9ca8799ba204e9e3d006638d3114407937cd7aedea"},{"mode":"100755","path":"scripts/recommendation_record.py","sha256":"2d8eb267535d7fd73c83185c1c8dc20f2639676371b9fc245f9b976bbd5437ba"},{"mode":"100755","path":"scripts/validate-envelope.py","sha256":"87799cbcc981ae38a5f96ab3191043047fd8de91d60ed98ee31e6666bf1091eb"},{"mode":"100644","path":"schemas/benchmark-plan.schema.json","sha256":"34c31fd50bee7e459f9be65bdd64c6af4673010c389763d7c59010065de662dc"},{"mode":"100644","path":"schemas/benchmark-result.schema.json","sha256":"f90ea7a9ff62f943e66349a6a48fef33b619e7191e2fbcbac97620cce1a376ad"},{"mode":"100644","path":"schemas/evidence-receipt.schema.json","sha256":"377e9161580cbf9b802074dcc09897b89b1e31e99f1f60a06bec30c12561f500"},{"mode":"100644","path":"schemas/worker-result.schema.json","sha256":"f2589ae5249b395dc90279af07600298b6b4354d1fbe0e2efe3fa72832e9a3b0"},{"mode":"100644","path":"schemas/worker-result.provider.schema.json","sha256":"d17bf6d47ddf89f57644ce94f154c370cb92f65f174255066c79e7cc233f6fdd"}],"kind":"agy-worker-benchmark-portable-source","schema_version":1,"source_revision":"offline-benchmark-v1"} +{"files":[{"mode":"100755","path":"benchmark.sh","sha256":"7e0033f6bf3eec1e6007752ef67e8e33db8f39c7136790a5403d651b39eeab8d"},{"mode":"100755","path":"qa-gate.sh","sha256":"878cf09fbc982b70895f8bf1335892d7291a56616434f64b58334c91c8f87d3f"},{"mode":"100755","path":"verify-job.sh","sha256":"b719ebac651b83d27d6fe2af47a0d454a43a4ab649afe98093fcc13d9164073c"},{"mode":"100755","path":"scripts/benchmark.py","sha256":"a6e8527194b6394767d173d66610a6f57e427438765e955741a96239308d6c2e"},{"mode":"100755","path":"scripts/candidate_state.py","sha256":"de6c5c8006ac641a29b37bb2aaf17b4fe0d00d6844806ba382aa19ce6852efa7"},{"mode":"100755","path":"scripts/compatibility.py","sha256":"b42cbb740aedfc3e2c183a5ea1bf5e43bf60e992987b38e36b7cc49f285bce5d"},{"mode":"100755","path":"scripts/evidence_receipt.py","sha256":"eaa1878355c541bdd7e5f3411002b6e4bb4860f7989f0397ba9237aaec76c3c6"},{"mode":"100755","path":"scripts/model_selection.py","sha256":"9e0be61a5a5900a2f11c7c9ca8799ba204e9e3d006638d3114407937cd7aedea"},{"mode":"100755","path":"scripts/recommendation_record.py","sha256":"2d8eb267535d7fd73c83185c1c8dc20f2639676371b9fc245f9b976bbd5437ba"},{"mode":"100755","path":"scripts/validate-envelope.py","sha256":"87799cbcc981ae38a5f96ab3191043047fd8de91d60ed98ee31e6666bf1091eb"},{"mode":"100644","path":"schemas/benchmark-plan.schema.json","sha256":"34c31fd50bee7e459f9be65bdd64c6af4673010c389763d7c59010065de662dc"},{"mode":"100644","path":"schemas/benchmark-result.schema.json","sha256":"f90ea7a9ff62f943e66349a6a48fef33b619e7191e2fbcbac97620cce1a376ad"},{"mode":"100644","path":"schemas/evidence-receipt.schema.json","sha256":"377e9161580cbf9b802074dcc09897b89b1e31e99f1f60a06bec30c12561f500"},{"mode":"100644","path":"schemas/worker-result.schema.json","sha256":"f2589ae5249b395dc90279af07600298b6b4354d1fbe0e2efe3fa72832e9a3b0"},{"mode":"100644","path":"schemas/worker-result.provider.schema.json","sha256":"d17bf6d47ddf89f57644ce94f154c370cb92f65f174255066c79e7cc233f6fdd"}],"kind":"agy-worker-benchmark-portable-source","schema_version":1,"source_revision":"offline-benchmark-v1"} diff --git a/compat/agy-distribution-manifest.json b/compat/agy-distribution-manifest.json index b78e703..ea8725d 100644 --- a/compat/agy-distribution-manifest.json +++ b/compat/agy-distribution-manifest.json @@ -1,5 +1,5 @@ { - "version": "1.1.27", - "url": "https://storage.googleapis.com/antigravity-public/antigravity-cli/1.1.27-5211191891591168/darwin-arm/cli_mac_arm64.tar.gz", - "sha512": "cd627f798e059f84a88bfe21dbde54cc5262e2d93d1943f120eb4b386823f4a1f7c4a57d4d0f876557709b6061a4c02f1fc13157f0047c5df6f300afa471e411" + "version": "1.2.2", + "url": "https://storage.googleapis.com/antigravity-public/antigravity-cli/1.2.2-6061403484848128/darwin-arm/cli_mac_arm64.tar.gz", + "sha512": "8a3b5edea51e107a74413cea5eed1c5b02dede945ba21c7625b7c86f477c2c8ac423581de0ed84ff2f97c3bf6818c1fc24ca84cf9a76c2fb02a50e89d8a0d29a" } diff --git a/compat/agy-last-reviewed.txt b/compat/agy-last-reviewed.txt index 98624ac..fce5b20 100644 --- a/compat/agy-last-reviewed.txt +++ b/compat/agy-last-reviewed.txt @@ -1 +1 @@ -2026-09-06 +2026-09-13 diff --git a/compat/agy-model-effort-matrix.json b/compat/agy-model-effort-matrix.json index b4e0760..3ee4a40 100644 --- a/compat/agy-model-effort-matrix.json +++ b/compat/agy-model-effort-matrix.json @@ -2,8 +2,8 @@ "schema_version": 1, "resolution_status": "active", "inventory": { - "agy_version": "1.1.27", - "reviewed_source_revision": "1ae9cb7b51667192c051b73a91099c71e816ca5f", + "agy_version": "1.2.2", + "reviewed_source_revision": "ba985e6b5de2ac8aa09860a154a102831eb7722b", "evidence": [ "agy-models", "official-release", diff --git a/compat/agy-model-effort-matrix.sha256 b/compat/agy-model-effort-matrix.sha256 index 4f0eaee..cd08770 100644 --- a/compat/agy-model-effort-matrix.sha256 +++ b/compat/agy-model-effort-matrix.sha256 @@ -1 +1 @@ -56ee4cefdf918184e8bae57c49f01c51c18e49b30ff0bd4b322d8801703dfaac +2c12abf09910489b681a01c88b43e8fbaf7df3a68fd715c7280c7f6fff6c89e4 diff --git a/compat/agy-models-inventory-binding.json b/compat/agy-models-inventory-binding.json index f485195..e52f9d6 100644 --- a/compat/agy-models-inventory-binding.json +++ b/compat/agy-models-inventory-binding.json @@ -1,11 +1,11 @@ { "schema_version": 1, "status": "accepted-current-inventory", - "agy_version": "1.1.27", - "reviewed_source_revision": "1ae9cb7b51667192c051b73a91099c71e816ca5f", - "source_sha256": "d583be1344ea9cfa0c45cff2c1342af7837f4833c4edb65e69bee84776a45caa", - "version_binding_sha256": "a1f8b651123f1e95ef7e744ae45e9066967b6ceb0a710ddfd685e54b14b0b0a6", - "capture_record_sha256": "ac8ddc28fcca90a20e05f6bd0678d32550db451a8e3402e4c287154eab289b33", + "agy_version": "1.2.2", + "reviewed_source_revision": "ba985e6b5de2ac8aa09860a154a102831eb7722b", + "source_sha256": "cabadc15a61944372bede1fdff186701c17467dd9d718e97dc79283055d3c101", + "version_binding_sha256": "cc3bd8bb44b31891e2bed4c4367ed90ae2334e0df29e9fba2e94bce7c3105bef", + "capture_record_sha256": "73d65488b618b721d70d85076cc1ca67c8ead62e0606588502aeb69e5230d032", "capture_stdout_sha256": "d02970e6b6b4e0910461999afca8fb99d757e9094ab2874b557dad18fc75464a", "capture_response_sha256": "b1cc011310435afa07b1e132a5b7f3e22297aa21427177461c858bcbd6a58794", "inventory_normalized_sha256": "d5e58ab55e91ebd4a2cd23841c76cbe12b47d607c62cd8c834fc8f6b9f078ad7", diff --git a/compat/agy-models-inventory-binding.sha256 b/compat/agy-models-inventory-binding.sha256 index f2bd8c0..1bf9ecc 100644 --- a/compat/agy-models-inventory-binding.sha256 +++ b/compat/agy-models-inventory-binding.sha256 @@ -1 +1 @@ -3a554f90922321700120e2c398f865a4894b2128087a4bac90968ddf2409762d +efcc59a983b0c9f60309a55047188d800f5e34d2dc42a38d9339cdc361e13802 diff --git a/compat/agy-upstream-head.txt b/compat/agy-upstream-head.txt index 73e8d81..d0f31fd 100644 --- a/compat/agy-upstream-head.txt +++ b/compat/agy-upstream-head.txt @@ -1 +1 @@ -1ae9cb7b51667192c051b73a91099c71e816ca5f +ba985e6b5de2ac8aa09860a154a102831eb7722b diff --git a/compat/agy-verified-version.txt b/compat/agy-verified-version.txt index 93325dd..23aa839 100644 --- a/compat/agy-verified-version.txt +++ b/compat/agy-verified-version.txt @@ -1 +1 @@ -1.1.27 +1.2.2 diff --git a/compat/agy-version-manifest.json b/compat/agy-version-manifest.json index bc69f7c..3636f12 100644 --- a/compat/agy-version-manifest.json +++ b/compat/agy-version-manifest.json @@ -2,9 +2,7 @@ "schema_version": 1, "kind": "agy-version-manifest", "versions": { - "1.1.27": { - "version": "1.1.27", - "support_tier": "current", + "1.2.2": { "allowed_operations": [ "activation", "capture", @@ -13,6 +11,57 @@ "reprofile", "version-evidence" ], + "capture_runner_source_sha256": "34fd925d3d66a8fa46d7308ec1f67e5a1578fb4f961197993f5bfb17b6ec8677", + "capture_snapshot_policy": "macos-readonly-mount", + "distribution_sha512": "8a3b5edea51e107a74413cea5eed1c5b02dede945ba21c7625b7c86f477c2c8ac423581de0ed84ff2f97c3bf6818c1fc24ca84cf9a76c2fb02a50e89d8a0d29a", + "distribution_url": "https://storage.googleapis.com/antigravity-public/antigravity-cli/1.2.2-6061403484848128/darwin-arm/cli_mac_arm64.tar.gz", + "expected_stdout": "1.2.2\n", + "output_profile_name": "models.capture.1.2.2.profile.json", + "prior_name": "agy-models-capture-1.2.2.version", + "recovery_binding_sha256": "cc3bd8bb44b31891e2bed4c4367ed90ae2334e0df29e9fba2e94bce7c3105bef", + "recovery_runner_bytes": 48402, + "recovery_runner_sha256": "16fe57ed938bd482aa8df99e5e42914984fdb0b9f22de59919a5ae64aa020f98", + "recovery_stdout": "1.2.2\n", + "recovery_summary_bytes": 260, + "release_commit": "ba985e6b5de2ac8aa09860a154a102831eb7722b", + "source_sha256": "cabadc15a61944372bede1fdff186701c17467dd9d718e97dc79283055d3c101", + "source_size": 180089984, + "support_tier": "current", + "version": "1.2.2", + "historical_recovery_binding_sha256": "a1f8b651123f1e95ef7e744ae45e9066967b6ceb0a710ddfd685e54b14b0b0a6", + "historical_recovery_source_sha256": "d583be1344ea9cfa0c45cff2c1342af7837f4833c4edb65e69bee84776a45caa", + "reprofile_output_name": "models.capture.1.2.2.reprofile.json", + "failure_ruleset_version": "agy-1.2.2-failure-rules-v1", + "capture_record_sha256": "73d65488b618b721d70d85076cc1ca67c8ead62e0606588502aeb69e5230d032", + "capture_stdout_sha256": "d02970e6b6b4e0910461999afca8fb99d757e9094ab2874b557dad18fc75464a", + "capture_response_sha256": "b1cc011310435afa07b1e132a5b7f3e22297aa21427177461c858bcbd6a58794", + "inventory_normalized_sha256": "d5e58ab55e91ebd4a2cd23841c76cbe12b47d607c62cd8c834fc8f6b9f078ad7", + "slug_count": 14, + "slugs": [ + "claude-opus-4-6-thinking", + "claude-sonnet-4-6", + "gemini-3.1-pro-high", + "gemini-3.1-pro-low", + "gemini-3.6-flash-high", + "gemini-3.6-flash-low", + "gemini-3.6-flash-medium", + "gemini-3.7-flash-high", + "gemini-3.7-flash-low", + "gemini-3.7-flash-medium", + "gemini-3.8-flash-high", + "gemini-3.8-flash-low", + "gemini-3.8-flash-medium", + "gpt-oss-120b-medium" + ] + }, + "1.1.27": { + "version": "1.1.27", + "support_tier": "previous", + "allowed_operations": [ + "capture", + "profile", + "version-evidence" + ], "expected_stdout": "1.1.27\n", "source_sha256": "d583be1344ea9cfa0c45cff2c1342af7837f4833c4edb65e69bee84776a45caa", "source_size": 177426912, diff --git a/compat/agy-version-manifest.sha256 b/compat/agy-version-manifest.sha256 index 83fd61c..e57cd58 100644 --- a/compat/agy-version-manifest.sha256 +++ b/compat/agy-version-manifest.sha256 @@ -1 +1 @@ -898bc921c15aa342b9306600aa1e380bb2930e80de465c76a99b87ef34b0d6e0 +5b28e6f05c6e63c2022b461cf2aac596f9f3a4f4a1aa72957b5a62a5c334428e diff --git a/compat/reviews/agy-1.2.2-activation.md b/compat/reviews/agy-1.2.2-activation.md new file mode 100644 index 0000000..12731db --- /dev/null +++ b/compat/reviews/agy-1.2.2-activation.md @@ -0,0 +1,95 @@ +# AGY 1.2.2 activation evidence + +Reviewed: 2026-09-13 + +This record supports the accepted v0.19.0 implementation. Its stable candidate +passed all 44 offline CI stages and independent review. Publication and installation +remain separately verified delivery states. + +The accepted version and single account inventory capture bind AGY **1.2.2**, +reviewed upstream revision `ba985e6b5de2ac8aa09860a154a102831eb7722b`, executable +SHA-256 `cabadc15a61944372bede1fdff186701c17467dd9d718e97dc79283055d3c101`, and +version binding `cc3bd8bb44b31891e2bed4c4367ed90ae2334e0df29e9fba2e94bce7c3105bef`. +The capture record is `73d65488b618b721d70d85076cc1ca67c8ead62e0606588502aeb69e5230d032`. +Its fourteen model slugs have normalized SHA-256 +`d5e58ab55e91ebd4a2cd23841c76cbe12b47d607c62cd8c834fc8f6b9f078ad7`. +Mappings are unchanged from 1.1.27. Root and portable compatibility records bind +this version, source, inventory, matrix, and capture together. The separate +observational distribution snapshot was refreshed to the same accepted 1.2.2 +URL/SHA-512 tuple after a fresh fixed-manifest read. This prevents a stale snapshot +from generating permanent drift warnings; it grants no activation or download +authority. + +The official [1.2.2 changelog](https://github.com/google-antigravity/antigravity-cli/blob/1.2.2/CHANGELOG.md) +and bounded version/help inspection were reconciled with observable behavior. The +upstream repository does not expose the CLI implementation. Caller-selected model +and effort continue to resolve to one advertised compound slug, passed through +`--model`; the presence of `--effort` in help does not establish safe composition. + +## Failure handling + +A real refused-command canary exposed `denied_actions` but lacked structured output. +It remained `invalid_envelope`; no candidate was invented. For a valid envelope on +exact 1.1.27 or 1.2.2, offline fixtures verify that key presence produces +`permission_required`, preserves a valid candidate, and prevents automatic repair. +This policy does not assume a stable field payload schema. + +A direct eight-second canary returned exit zero and terminal success with a partial +output warning. Its relative file request also caused an ambient account-home +configuration search, so this route was not repeated. Subsequent worker cases used +wrapper-controlled absolute stage paths. Offline controller scenarios verify that +the exact observed warning, bound to the job duration, yields `provider_timeout` +and preserves a valid candidate for independent review. Permission denial, invalid +reports, hard deadlines, cancellation, and binding failures keep their precedence. +The canary did not itself produce a valid worker candidate. + +## Session and native qualification + +One bounded campaign used only `gemini-3.8-flash-high`. Its initial ten-start cap was +explicitly raised to eleven; renewed user authority after the failed continuation +was bounded to two further repair starts. Thirteen actual starts were recorded. +There were no model substitutions, additional inventory reads, or Boost starts. + +Session normal work and same-conversation repair passed driver-owned checks. The +repair deliberately began with a trim-only candidate; the second turn added +lowercasing and passed five checks. These cases qualify only the exercised workflow +and candidates; session mode does not confine host reads. + +Native qualification required narrow, separately reviewed preparation fixes: +exact provider executable-parent metadata for local initialization, a private +default-Keychain locator with read access restricted to the security helper, and +minimal staged-path permissions in private job settings. Owner configuration was +not copied or changed. The normal native candidate then passed five driver checks +and independent review. + +An earlier native continuation failed with `status_unavailable` at `binding_failure`. +The prior candidate remained on disk, but cleanup and result rebinding were not +established. Targeted diagnostics identified a failed conversation database open +before the continuation panic. An offline upstream SQLite differential reproduced +an ancestor `lstat` denial. Provider-image-only metadata access to private HOME's +ancestor chain restored database creation, WAL/SHM use, and close/reopen recovery; +it grants neither ancestor listing/data nor the same access to other images or +self-verification. No credential access or AGY invocation was used in that proof. + +The two newly authorized native turns then completed in the same recorded +conversation in approximately 32 seconds total, within the original job budget. +The expected trim-only candidate was preserved, and the repaired candidate passed +five driver checks. Independent review accepted both native normal and repair +results. Historical failed attempts and their uncertain cleanup remain documented +in the [investigation record](agy-1.2.2-candidate.md); their outcomes are not rewritten. + +## Activation and limits + +Activation promotes 1.2.2 to current, retains 1.1.27 and 1.1.26 as previous, 1.1.22 +as legacy, and 1.1.24, 1.1.16, and 1.1.12 as historical. Stable offline CI, +package parity, instruction audit, and independent repository acceptance passed +for the implementation candidate. The skill simplification preserves automatic discovery, +same-scope repair, candidate preservation, and driver-owned assurance labels; it +makes no measured speed or quality claim. + +These bounded cases do not establish exhaustive compatibility, provider backend +identity, model quality, authentication or quota for other runs, billing, fallback, +effective routing, or live optional self-verification. Native tests establish the +exercised filesystem boundaries, not complete same-user tamper resistance. No new +Boost qualification is claimed. The closed-binary provider backend cannot be +independently attested by this evidence. diff --git a/compat/reviews/agy-1.2.2-candidate.md b/compat/reviews/agy-1.2.2-candidate.md new file mode 100644 index 0000000..ca00b2d --- /dev/null +++ b/compat/reviews/agy-1.2.2-candidate.md @@ -0,0 +1,118 @@ +# AGY 1.2.2 candidate compatibility review + +Reviewed: 2026-09-13 + +This record preserves the reviewed AGY `1.2.2` source/distribution observation and +the chronological live-workflow investigation before activation. At this stage the +active baseline remained 1.1.27; the subsequent +[activation record](agy-1.2.2-activation.md) owns the current decision. This record contains no account identifier, credential, +private path, raw capture, prompt, task content, conversation identifier, or provider +response. + +## Bound candidate evidence + +- Official release revision: `ba985e6b5de2ac8aa09860a154a102831eb7722b`. +- Executable SHA-256: `cabadc15a61944372bede1fdff186701c17467dd9d718e97dc79283055d3c101`. +- Distribution SHA-512: + `8a3b5edea51e107a74413cea5eed1c5b02dede945ba21c7625b7c86f477c2c8ac423581de0ed84ff2f97c3bf6818c1fc24ca84cf9a76c2fb02a50e89d8a0d29a`. + +The initial candidate manifest permitted version evidence only. One bounded account +inventory read selected fourteen reviewed slugs; that observation alone did not +activate an inventory, matrix, capture/classifier profile, or routing decision. + +## Bounded live observations + +- A one-time harmless-command canary observed a real `denied_actions` key. Its empty + `SUCCESS` response lacked structured output, so the controller classified it as + `invalid_envelope` with `failure_stage=missing_structured_output` and preserved no + candidate. This confirms the controller's existing fail-closed response handling; + it does not establish a general denied-action payload contract. +- A direct print canary returned exit `0` and terminal `SUCCESS` with a partial-output + warning after eight seconds. It is a timeout/stream-boundary observation, not a + successful worker result. Its relative filename caused an ambient account-home + configuration search; that raw route will not be repeated. Remaining cases use + wrapper-controlled absolute stage guidance. +- The approved session normal case completed with driver verification. It establishes + only that exercised case and checked candidate. +- The same-conversation session repair completed after its bounded second start. The + final candidate passed five driver checks. This establishes only that exercised + session repair and its exact final candidate. +- The native normal case stopped at an authentication-required condition. Its public + result is `status_unavailable` at `binding_failure` after the authentication wait; + no provider stream or candidate was produced. Shutdown identity remains uncertain, + so this record makes no native cleanup-success claim. + +A subsequent offline macOS C probe isolated a local initialization failure: the +external executable's main bundle and SSL policy were null under the original +profile, while metadata/existence access to only its exact parent directory restored +both. The runtime candidate adds that permission only for the bound provider image. +Offline tests retain denied directory listing, sibling reads/stat, and exec-helper +access; self-verification receives no exception. A subsequent authorized native +normal attempt no longer emitted the SSL-policy error, but silent authentication +still failed before any stream or candidate. This is not native live qualification. + +A separate offline experiment tested a minimal default-Keychain locator in private +HOME with a synthetic item explicitly accessible to the native security helper. +Metadata-only file access did not retrieve the item. Read-only access to that exact +file, limited to `/usr/bin/security`, enabled both default and explicit-path lookup; +the fake provider and a different executable remained unable to read it. This +supports the narrow helper-file mechanism for that fixture, not actual AGY token +presence, lock state, or item access controls. No real credentials or account +inventory were queried by this experiment. + +After independent review and explicit authorization for actual Keychain access, +the next native attempt reached the selected model and received a response with +reported token usage. Authentication therefore progressed in that attempt. The +provider then denied a directory-listing read within the approved synthetic stage, +returned no structured result, and produced no candidate. The controller retained +`invalid_envelope` / `missing_structured_output`; this does not qualify native normal +work or authorize an automatic permission retry. + +The reviewed runtime now prepares minimal AGY permission rules inside each native +job's private HOME. Rules cover only approved staged reads and exact write selectors +for the already-bounded attempts. Settings bytes remain identical across repair; +the host profile still permits only the current stage. Owner account and permission +settings are neither copied nor changed. Offline boundary tests and an independent +host preparation check accepted this mechanism before the next provider launch. + +The ninth start completed the native normal case. Its sole file edit passed five +driver-owned checks and independent review. The tenth start completed the first +native repair turn, preserving the intentionally incomplete candidate: trimming +passed and lowercasing failed as expected. The eleventh start used the recorded +conversation identifier for the approved correction, but AGY reported `CLI crashed` +before producing any stream output. The controller stopped after approximately +sixty seconds of that attempt with `status_unavailable` / `binding_failure`. +The prior candidate remains on disk; result rebinding and process-group cleanup +were not established, so neither repaired success nor successful cleanup is claimed. +The crash message alone does not establish an authentication failure or root cause. + +At that point, the approved eleven-start budget and single account inventory read +were exhausted. Native normal was accepted; native repair remained unqualified. +After renewed user authority, follow-up work was bounded to two additional native +repair starts, with the same model and selected synthetic file scope. + +Targeted CLI diagnostics then identified a preceding persistence error: the initial +turn could not open its conversation database, and continuation panicked in the +trajectory database component. An offline upstream SQLite reproduction localized +`EPERM` to `lstat` on an ancestor of private HOME. A one-rule differential granted +only the exact provider image metadata access to that ancestor chain; database +creation, WAL/SHM use, commit, close/reopen, and sentinel recovery then passed. +No AGY start or credential access was used for this reproduction. This explains the +observed storage prerequisite. The twelfth and thirteenth starts then completed +the native repair in the same conversation: the first preserved the expected +trim-only candidate, and the second passed all five driver checks. Independent +review accepted the native normal and repaired candidates. +No unchanged retry, model substitution, or Boost run was performed. + +## Decision and limits + +The initial non-activating decision was superseded only after the accepted session +and native normal/repair results. The [activation record](agy-1.2.2-activation.md) +binds the resulting metadata promotion and its verification limits. Failed attempts +remain part of this investigation; their uncertain cleanup is not retroactively +reported as successful. + +These bounded cases do not establish provider/backend identity, model quality, +exhaustive compatibility, authentication state for other runs, pricing, quota, +fallback, billing, effective routing, or live optional self-verification. No Boost +success or publication is established by this record. diff --git a/compat/sources.md b/compat/sources.md index 2ad414c..7647739 100644 --- a/compat/sources.md +++ b/compat/sources.md @@ -39,9 +39,9 @@ separately authorized mutation path. - Fixed `darwin_arm64` distribution manifest: https://antigravity-cli-auto-updater-974169037036.us-central1.run.app/manifests/darwin_arm64.json - Installed interface evidence: `./ground-truth.sh` -The verified active baseline is agy `1.1.27` at official release commit -`1ae9cb7b51667192c051b73a91099c71e816ca5f`; its accepted inventory is reconciled in -[`reviews/agy-1.1.27-activation.md`](reviews/agy-1.1.27-activation.md). Earlier +The local development candidate's active baseline is agy `1.2.2` at official release +commit `ba985e6b5de2ac8aa09860a154a102831eb7722b`; its accepted inventory is reconciled in +[`reviews/agy-1.2.2-activation.md`](reviews/agy-1.2.2-activation.md). Earlier activation records remain historical evidence. The active matrix binds every adjustable pair to one exact advertised compound slug and records fixed choices as non-adjustable. It neither forwards `--effort` nor attests the effective provider @@ -63,7 +63,7 @@ in [`reviews/agy-1.1.13-quota-terminal.md`](reviews/agy-1.1.13-quota-terminal.md It authorizes only the exact version/shape classifier and sanitized countdown; it is not a general quota/rate-limit signature, baseline update, or retry authority. -`agy-distribution-manifest.json` records the observed `1.1.27` version, exact Google +`agy-distribution-manifest.json` records the observed `1.2.2` version, exact Google Storage archive URL, and lowercase SHA-512 tuple. It is an observational snapshot, not an authoritative baseline, signature, or permission to download the archive. The checker fetches only the fixed small manifest, rejects redirects and malformed @@ -101,9 +101,9 @@ resolution until another human reconciliation is accepted. Codex drift remains observation-only and never disables agy dispatch. The original agy `1.1.22` failed capture is retained as non-activating historical -metadata. The separately authorized 1.1.27 capture and human reconciliation advance +metadata. The separately authorized 1.2.2 capture and human reconciliation advance the active version, release binding, inventory, and matrix only through -[`reviews/agy-1.1.27-activation.md`](reviews/agy-1.1.27-activation.md). Neither record +[`reviews/agy-1.2.2-activation.md`](reviews/agy-1.2.2-activation.md). Neither record authorizes another account call. Ordinary version-independent literal model pass-through and agy-owned default selection remain independent of that matrix; reviewed model/effort resolution still diff --git a/docs/INSTALLATION.md b/docs/INSTALLATION.md index 064df6e..cdd29fc 100644 --- a/docs/INSTALLATION.md +++ b/docs/INSTALLATION.md @@ -125,11 +125,17 @@ also needs network access. Do not use dangerous permission or approval bypass fl ## Refused actions and report paths -For AGY 1.1.27, a valid result containing `denied_actions` stops with +For exact AGY 1.1.27 and 1.2.2, a valid result containing `denied_actions` stops with `permission_required`. A valid candidate remains available for review and finalization; the worker does not automatically continue past a permission denial. The field's payload shape is not interpreted. +For observed AGY 1.2.2, the reviewed partial-output timeout warning stops provider +success even when the process exits zero. A valid candidate remains available for +independent review; an invalid report does not become a candidate. The warning must +match the job's bound duration. AGY 1.2.2 is the v0.19.0 compatibility +baseline, supported by the [bounded activation evidence](../compat/reviews/agy-1.2.2-activation.md). + File tools use absolute workspace paths. Final `files_changed` reports should use workspace-relative paths. Scoped reconciliation also accepts canonical absolute paths beneath that attempt's exact staged root, subject to the same observed @@ -138,7 +144,7 @@ mutation and scope checks; paths elsewhere remain invalid. ## Version drift and direct model selection The accepted model/effort mapping, exact agy version, and evidence digests live in the -current [activation record](../compat/reviews/agy-1.1.27-activation.md). Historical +current [activation record](../compat/reviews/agy-1.2.2-activation.md). Historical observations remain history; they do not override the current source and checked-in matrix. Codex compatibility evidence is observational and grants neither dispatch nor model-selection authority. diff --git a/docs/REPO_MAP.md b/docs/REPO_MAP.md index 4263662..28e433f 100644 --- a/docs/REPO_MAP.md +++ b/docs/REPO_MAP.md @@ -260,7 +260,7 @@ does not establish same-user tamper resistance. | Path | Responsibility | Owning offline suite | |---|---|---| -| `agy-worker.sh`, `skills/agy-worker/runtime/agy-worker.sh`, `skills/agy-worker/runtime/scripts/agy_dispatch.py`, `skills/agy-worker/runtime/scripts/agy_dispatch_worktree.py`, `scripts/transmission_preview.py`, `skills/agy-worker/runtime/scripts/transmission_preview.py` | Root compatibility entry point plus canonical runtime entry point; explicitly mirrored helpers remain byte-synchronized. Caller-owned selection, optional personas, workflow resolution (`explore`/`task`: 1..2, default 2; `project`: 1..5, default 5), private prompt/log staging, a closed provider/probe environment baseline with exact-name `--provider-env` opt-ins bound into command state, deterministic external state root derivation under `XDG_STATE_HOME`/`HOME` when unset, prospective and post-resolution fail-closed rejection of project roots inside the target worktree before prompt staging or recovery dispatch, process-owning progress-aware dispatch, and a path-bound sibling engine for bounded no-follow Git/worktree observations. Provider-free `transmission-preview` runs before prompt, selection, provider, state, log, stdin, or network work and exposes a reusable schema-v1 path/kind manifest over two complete content-free scans of a canonical branch-backed linked worktree. Fixed bounded `/usr/bin/git worktree list` plumbing verifies real registration without hooks, prompts, provider, or network; streamed directory enumeration applies count/time bounds before sorting. It excludes the root `.git` marker, lists contained symlink aliases without targets, rejects drift, escapes, special nodes, and limits, and is review evidence rather than approval or provider-launch binding. Current V13/command V10 supports an explicit transmission choice and a closed read/write file-or-tree provider scope whose policy, readable manifest, selected-content manifest, and approval are bound by one transmission SHA. Its advanced one-cycle Boost task profile additionally binds a job-specific authority warning, requires `accept-edits`, no persona, slash protection, and provider init `agent=Boost` plus `permission_mode=request-review`, and disables resume/restart/continue without widening transmission or permissions. Each attempt uses a fresh external owner-private mode-0700 Git-less stage as provider cwd. Descriptor-relative no-follow copies reject aliases, hardlinks, special nodes, Git administration, and casefold/NFC/NFD collisions under count, byte, depth, and deadline bounds. For exact AGY 1.1.27, a valid terminal report containing `denied_actions` yields `permission_required` without automatic continuation; candidate and upstream provenance remain reviewable. Scoped report paths accept canonical absolute descendants of the exact staged root as relative equivalents before the existing exact mutation comparison. Recognized success, error, and cancelled reports preserve and transactionally reconcile only authorized stage mutations after source rebind; durable backups, fsync, an atomic recovery ledger, prior/post identities, post-reconcile equality, and exact rollback fail closed on drift or uncertainty. Initial launch has no implicit transmission mode: provider scope is recommended, while whole-worktree dispatch remains a manifest-bound exception rechecked immediately before the initial provider process. V6/V7 jobs and already-queued states load compatibly, but unapproved legacy broad records cannot launch through dispatcher run/start; existing jobs may upgrade structurally but never acquire narrow scope or Boost authority. Current V13 candidate/lifecycle writes preserve all V9 explicit semantic-v1 candidate snapshots plus stable root/Git boundary identity and sanitized `provider_terminal_status` (`unknown`, `success`, `error`, `cancelled`) without exposing provider status in public driver disposition or altering action thresholds. A terminal scoped candidate is read and finalized against its stored post-provider snapshot, including authorized deletions; changed scoped bytes can continue only under an explicit initial scoped-repair grant, which binds scope, model, conversation, candidate lineage, and budgets; otherwise they remain result/finalize-only. Verification v2 rebinds result/schema/root/candidate before and after a no-follow isolated verification copy. It preserves regular bytes/executable bits, rebases every contained symlink inside the copy, and rejects broken/outward/Git-admin links (no `.git`) so writable driver checks do not reconcile ignored drift into the candidate. Partial/promisor clones fail synchronously with a fixed sanitized full-clone diagnostic before queued state or provider launch. A valid, non-empty REUC observation immediately before provider launch yields the bounded public reason `resolve_undo_present`, existing exit code 20, and `failure_stage=binding_failure` without provider launch or clearing index metadata; malformed, duplicate, or racing observations remain generic `status_unavailable`. Wrapper parse errors remain `64`; post-parse copy runtime/binding/destination failures map to `20`. Explicit native scoped provider attempts additionally use the native macOS containment helper described below; ordinary verification copies and environment filtering alone do not provide OS isolation. V1 remains read-only; V3/V4 require first-transition SHA+rebound-migration approval and do not advertise the non-migrating copy helper. A preserved current V13 job already inside its worktree keeps exact command/schema/root/result readback and driver-only non-verified finalization, while verified finalization, verification-copy, and provider continuation/restart remain unavailable. Other current finalization retains artifact/schema/root/worktree rebinding; preserved provider-error/cancelled candidates, queued SHA+entry rebinding, and exact quota-terminal mapping remain unchanged. Provider-scope approval grants neither provider execution, Git action, driver acceptance, nor publication. | `tests/test-agy-worker.sh` (302 cases), including its sourced `tests/agy_worker_project_lifecycle_cases.sh`; `tests/test-agy-worker-remediation.py` (111 focused cases); its non-discoverable case modules are loaded by that canonical suite | +| `agy-worker.sh`, `skills/agy-worker/runtime/agy-worker.sh`, `skills/agy-worker/runtime/scripts/agy_dispatch.py`, `skills/agy-worker/runtime/scripts/agy_dispatch_worktree.py`, `scripts/transmission_preview.py`, `skills/agy-worker/runtime/scripts/transmission_preview.py` | Root compatibility entry point plus canonical runtime entry point; explicitly mirrored helpers remain byte-synchronized. Caller-owned selection, optional personas, workflow resolution (`explore`/`task`: 1..2, default 2; `project`: 1..5, default 5), private prompt/log staging, a closed provider/probe environment baseline with exact-name `--provider-env` opt-ins bound into command state, deterministic external state root derivation under `XDG_STATE_HOME`/`HOME` when unset, prospective and post-resolution fail-closed rejection of project roots inside the target worktree before prompt staging or recovery dispatch, process-owning progress-aware dispatch, and a path-bound sibling engine for bounded no-follow Git/worktree observations. Provider-free `transmission-preview` runs before prompt, selection, provider, state, log, stdin, or network work and exposes a reusable schema-v1 path/kind manifest over two complete content-free scans of a canonical branch-backed linked worktree. Fixed bounded `/usr/bin/git worktree list` plumbing verifies real registration without hooks, prompts, provider, or network; streamed directory enumeration applies count/time bounds before sorting. It excludes the root `.git` marker, lists contained symlink aliases without targets, rejects drift, escapes, special nodes, and limits, and is review evidence rather than approval or provider-launch binding. Current V13/command V10 supports an explicit transmission choice and a closed read/write file-or-tree provider scope whose policy, readable manifest, selected-content manifest, and approval are bound by one transmission SHA. Its advanced one-cycle Boost task profile additionally binds a job-specific authority warning, requires `accept-edits`, no persona, slash protection, and provider init `agent=Boost` plus `permission_mode=request-review`, and disables resume/restart/continue without widening transmission or permissions. Each attempt uses a fresh external owner-private mode-0700 Git-less stage as provider cwd. Descriptor-relative no-follow copies reject aliases, hardlinks, special nodes, Git administration, and casefold/NFC/NFD collisions under count, byte, depth, and deadline bounds. For exact AGY 1.1.27 and 1.2.2, a valid terminal report containing `denied_actions` yields `permission_required` without automatic continuation; candidate and upstream provenance remain reviewable. For observed AGY 1.2.2, an exact partial-output timeout warning bound to the job duration yields `provider_timeout` even with exit zero; a valid candidate remains reviewable, while invalid envelopes and hard deadlines retain their existing precedence. An exited provider whose descendant holds the output pipe open may preserve that exact partial candidate at idle expiry; unrelated idle failures retain their prior behavior. Scoped report paths accept canonical absolute descendants of the exact staged root as relative equivalents before the existing exact mutation comparison. Recognized success, error, and cancelled reports preserve and transactionally reconcile only authorized stage mutations after source rebind; durable backups, fsync, an atomic recovery ledger, prior/post identities, post-reconcile equality, and exact rollback fail closed on drift or uncertainty. Initial launch has no implicit transmission mode: provider scope is recommended, while whole-worktree dispatch remains a manifest-bound exception rechecked immediately before the initial provider process. V6/V7 jobs and already-queued states load compatibly, but unapproved legacy broad records cannot launch through dispatcher run/start; existing jobs may upgrade structurally but never acquire narrow scope or Boost authority. Current V13 candidate/lifecycle writes preserve all V9 explicit semantic-v1 candidate snapshots plus stable root/Git boundary identity and sanitized `provider_terminal_status` (`unknown`, `success`, `error`, `cancelled`) without exposing provider status in public driver disposition or altering action thresholds. A terminal scoped candidate is read and finalized against its stored post-provider snapshot, including authorized deletions; changed scoped bytes can continue only under an explicit initial scoped-repair grant, which binds scope, model, conversation, candidate lineage, and budgets; otherwise they remain result/finalize-only. Verification v2 rebinds result/schema/root/candidate before and after a no-follow isolated verification copy. It preserves regular bytes/executable bits, rebases every contained symlink inside the copy, and rejects broken/outward/Git-admin links (no `.git`) so writable driver checks do not reconcile ignored drift into the candidate. Partial/promisor clones fail synchronously with a fixed sanitized full-clone diagnostic before queued state or provider launch. A valid, non-empty REUC observation immediately before provider launch yields the bounded public reason `resolve_undo_present`, existing exit code 20, and `failure_stage=binding_failure` without provider launch or clearing index metadata; malformed, duplicate, or racing observations remain generic `status_unavailable`. Wrapper parse errors remain `64`; post-parse copy runtime/binding/destination failures map to `20`. Explicit native scoped provider attempts additionally use the native macOS containment helper described below; ordinary verification copies and environment filtering alone do not provide OS isolation. V1 remains read-only; V3/V4 require first-transition SHA+rebound-migration approval and do not advertise the non-migrating copy helper. A preserved current V13 job already inside its worktree keeps exact command/schema/root/result readback and driver-only non-verified finalization, while verified finalization, verification-copy, and provider continuation/restart remain unavailable. Other current finalization retains artifact/schema/root/worktree rebinding; preserved provider-error/cancelled candidates, queued SHA+entry rebinding, and exact quota-terminal mapping remain unchanged. Provider-scope approval grants neither provider execution, Git action, driver acceptance, nor publication. | `tests/test-agy-worker.sh` (302 cases), including its sourced `tests/agy_worker_project_lifecycle_cases.sh`; `tests/test-agy-worker-remediation.py` (112 focused cases); its non-discoverable case modules are loaded by that canonical suite | | `model-selection.sh`, `skills/agy-worker/runtime/model-selection.sh`, `skills/agy-worker/runtime/scripts/model_selection.py`, portable matrix/schema/SHA | Root compatibility entry plus exact matrix-bound model/effort resolution, CLI-only version-independent literal records, bounded safe-target semantic version plus structural critical-help preflight, automatic V2 mechanical launch authority for an exact version match, and explicit Codex disposition plus exact raw-help SHA for drift. V3 records bind that drift decision, capabilities/matrix/selection facts, and a bounded no-follow descriptor digest plus complete safe executable path authority (only the macOS `/var` alias is normalized); controller help prose is data, never availability inference. Codex inspects current bounded raw help before every reviewed direct dispatch and owns the semantic stop decision, including for an exact-version match. Literal version observation remains non-gating and selection provenance is driver-owned. | dispatcher, doctor, and packaging suites | | `model-recommendation.sh`, `skills/agy-worker/runtime/model-recommendation.sh`, `skills/agy-worker/runtime/scripts/model-recommendation.py` | Root compatibility entry plus side-effect-free pre/post recommendations; direct selections are labelled but unranked and never applied | `tests/test-agy-worker.sh` (shared dispatcher suite) | | `model-intelligence.sh`, `skills/agy-worker/runtime/model-intelligence.sh`, `skills/agy-worker/runtime/scripts/model_intelligence.py`, `skills/agy-worker/runtime/compat/model-intelligence/dataset.v1.json`, schemas | Root compatibility entry plus offline Model Intelligence v1 validation, benchmark review tracking on supported model inventory/binding changes or dataset expiry with explicit maintainer disposition, and deterministic Pareto advisory calculation across quality, latency, token, and cost dimensions; distinct provenance types (vendor, independent, local), freshness/expiry, requested vs observed models, comparability boundaries (accounting, tokenizer, cost basis), and zero dispatch/git/model-change authority. | `tests/test-model-intelligence.py` plus doctor, resolver, packaging, and CI sharding/timing suites | @@ -268,7 +268,7 @@ does not establish same-user tamper resistance. | `delegation-policy.sh`, `skills/agy-worker/runtime/delegation-policy.sh`, `skills/agy-worker/runtime/scripts/delegation_policy.py`, `skills/agy-worker/runtime/schemas/delegation-policy.schema.json` | Root compatibility entry plus closed evaluator for explicit opt-in delegation-first coordinator policy; assigns AGY as first substantive repository actor after discovery/worktree/verification setup; fails closed on missing approvals, hard stops, preflight failures, or budget exhaustion without silent fallback to Codex. | `tests/test-delegation-policy.py` plus doctor, resolver, packaging, and CI sharding/timing suites | | `workflow.sh`, `skills/agy-worker/runtime/workflow.sh`, `skills/agy-worker/runtime/scripts/workflow.py`, `skills/agy-worker/runtime/schemas/workflow-state.schema.json` | Root compatibility entry plus canonical thin workflow facade (`run`, `status`, `verify-finalize`) over existing job lifecycle, dispatch, and verification authorities. Ordinary run accepts an absolute repo/job ID, binds omitted base to `HEAD` once, derives deterministic owner-private state plus an isolated lifecycle-owned branch/worktree under safe XDG/HOME state, and retains preview resources for the approved second call. Launch requires either the exact whole-worktree manifest approval or an exact provider-scope policy plus selected-content transmission digest; the scoped path delegates to the canonical staging boundary without `--add-dir`. The all-explicit state/worktree/branch/base tuple remains advanced compatibility. Status projects facade, existing job-lifecycle, or dispatcher sources read-only without migration; verification remains driver-owned. Same-invocation pre-dispatch rollback delegates exact clean facade-created deletion to the lifecycle and refuses drift or dispatch evidence. | `tests/test-workflow.py` (20 cases), `tests/test-workflow-integration.py` (installed CLI with a synthetic worker; detects an injected outside-write regression without claiming native provider containment), plus doctor, resolver, packaging, and CI sharding/timing suites | | `doctor.sh`, `skills/agy-worker/runtime/doctor.sh`, `skills/agy-worker/runtime/scripts/doctor-metadata.py`, `skills/agy-worker/runtime/compat/` | Root compatibility entry plus deterministic offline prerequisite checks and byte-synchronized portable agy metadata | `tests/test-doctor.sh` (219 cases) plus packaging synchronization checks | -| `install.sh`, `skills/agy-worker/README.md`, `skills/agy-worker/SKILL.md`, `skills/agy-worker/agents/openai.yaml`, `skills/agy-worker/references/`, `skills/agy-worker/scripts/resolve-pipeline.sh` | Install and resolve complete-plugin, explicit-checkout, or folder-only skill layouts without fetching code. `SKILL.md` stays the concise progressive-disclosure router and preserves mandatory user-facing provider dispatch notices across initial, resume, continue, and restart launches, authority/privacy stops, workflow choice, and independent plan governance; the standalone README and references own package orientation, detailed lifecycle/Verification v2, security/compatibility, and actionable troubleshooting. Package metadata states its truthful use case. All internal package links resolve without repository-root files, and decorative assets are optional rather than a completeness requirement. | `tests/test-packaging.sh` (494 cases), including package-document presence, link, metadata, standalone-completeness, and no-required-asset guards | +| `install.sh`, `skills/agy-worker/README.md`, `skills/agy-worker/SKILL.md`, `skills/agy-worker/agents/openai.yaml`, `skills/agy-worker/references/`, `skills/agy-worker/scripts/resolve-pipeline.sh` | Install and resolve complete-plugin, explicit-checkout, or folder-only skill layouts without fetching code. `SKILL.md` stays the concise progressive-disclosure router and preserves mandatory user-facing provider dispatch notices across initial, resume, continue, and restart launches, authority/privacy stops, workflow choice, and independent plan governance; the standalone README and references own package orientation, detailed lifecycle/Verification v2, security/compatibility, and actionable troubleshooting. Package metadata states its truthful use case. All internal package links resolve without repository-root files, and decorative assets are optional rather than a completeness requirement. | `tests/test-packaging.sh` (510 cases), including package-document presence, link, metadata, standalone-completeness, and no-required-asset guards | | `skills/agy-worker/runtime/schemas/`, `skills/agy-worker/runtime/scripts/validate-envelope.py` | Dependency-free envelope contract validation | dispatcher and gate suites | | `qa-gate.sh`, `skills/agy-worker/runtime/qa-gate.sh` | Root compatibility entry plus canonical immutable-base Git audit, bounded envelope intake, path policy, escalation, ordered no-shell canonical argv verification, explicitly acknowledged shell compatibility, a default verifier baseline without `HOME`, separately acknowledged credential-name opt-ins delivered only through a private descriptor, sanitized label/mode diagnostics, and internal pre-opened structured evidence handoff | `tests/test-qa-gate.sh` (62 cases) plus receipt suite no-FD compatibility checks | | `verify-job.sh`, `skills/agy-worker/runtime/verify-job.sh`, `skills/agy-worker/runtime/scripts/evidence_receipt.py`, `skills/agy-worker/runtime/schemas/evidence-receipt.schema.json` | Root compatibility entry plus exact input hashing, strict selection/advisory binding, startup-isolated parent-exclusive gate evidence, domain-separated canonical verifier-spec hashes, mode/acknowledgement/ordinary-and-credential environment-name policy hashing, private value handoff without value persistence, interruption cleanup, structurally compatible unsigned Receipt v1 validation, and private durable no-overwrite publication | `tests/test-evidence-receipt.sh` (99 cases) | @@ -279,7 +279,7 @@ does not establish same-user tamper resistance. | `skills/agy-worker/runtime/agents/*.md` | Prompt-injected optional personas. Direct `--persona` selection and explicit mode restrictions remain enforced by `agy-worker.sh`; prompt text grants no routing, verification, or acceptance authority. | dispatcher suite and packaging checks | | `proof-demo.sh`, `conformance/v1/envelopes/honest.json`, conformance content sources | Repository-only offline starter proof using the two-state teaching subset of the public versioned contract | `tests/test-proof-demo.sh` (21 cases) | | `conformance/run.sh`, `conformance/v1/`, `docs/CONFORMANCE.md` | Repository-only public qa-gate v1 fixture contract, strict manifest/source binding, private normally disposable repositories, bounded supplied-gate execution, FD-relative no-follow cleanup under an explicit same-UID TCB, fail-closed residual policy, and non-certification claim | `tests/test-conformance.py` (83 cases) plus packaging policy checks | -| `update.sh`, `ground-truth.sh`, `skills/agy-worker/runtime/ground-truth.sh`, `scripts/compatibility.py`, `scripts/compatibility_probe.py`, `scripts/agy_inventory.py`, `scripts/official_github.py`, `scripts/official_distribution.py`, `compat/` | Explicit project releases; exact fixed-REST agy/Codex observation; compact exact stable-tag-ref commit binding including bounded annotated project tags; separately bounded release/ref documents; bounded process-group/version probes; canonical packaged agy version/help ground-truth helper, resolved through `$PIPELINE` in installed layouts and delegated by the root wrapper, plus an explicit account-state phase; exact-line allowlisted agy inventory interpretation; sanitized reconciliation records; bounded distribution-manifest canary; active agy `1.1.27` exact version/release/inventory/digest-bound 14-slug model matrix. The authorized 1.1.27 capture and separate reconciliation preserve the Gemini 3.8 Flash low/medium/high compound slugs; the current activation record distinguishes passing normal/repair checks from the failed pre-fix Boost reconciliation and subsequent offline fix evidence; 3.5 Flash is not a current mapping. The activation path requires the digest-bound version manifest and rejects a missing or drifting manifest. Gemini 3.1 Pro medium remains outside this wrapper's reviewed compound-slug route because the accepted account inventory has no `gemini-3.1-pro-medium` slug. Codex `0.150.1` is an observational compatibility baseline with no agy dispatch, model, routing, or worker-backend authority. Explicit apply-time Git fetch remains ambient-configuration-aware. | `tests/test-version-manifest-engine.py` generic policy coverage, `tests/test-agy-1-1-22-activation.py` (25 active cases), the fixed 1.1.22 capture suites, `tests/test-update.sh` (325 cases, including fixed transport, supervisor, inventory, and daily-watch policy harnesses), `tests/test-official-github.py` (65 cases), plus packaging ground-truth phase coverage | +| `update.sh`, `ground-truth.sh`, `skills/agy-worker/runtime/ground-truth.sh`, `scripts/compatibility.py`, `scripts/compatibility_probe.py`, `scripts/agy_inventory.py`, `scripts/official_github.py`, `scripts/official_distribution.py`, `compat/` | Explicit project releases; exact fixed-REST agy/Codex observation; compact exact stable-tag-ref commit binding including bounded annotated project tags; separately bounded release/ref documents; bounded process-group/version probes; canonical packaged agy version/help ground-truth helper, resolved through `$PIPELINE` in installed layouts and delegated by the root wrapper, plus an explicit account-state phase; exact-line allowlisted agy inventory interpretation; sanitized reconciliation records; bounded distribution-manifest canary; active agy `1.2.2` exact version/release/inventory/digest-bound 14-slug model matrix. The authorized 1.2.2 capture and separate reconciliation preserve the Gemini 3.8 Flash low/medium/high compound slugs; the current activation record binds accepted session/native normal and same-conversation repair checks; earlier failures remain historical and no new live Boost success is claimed; 3.5 Flash is not a current mapping. The activation path requires the digest-bound version manifest and rejects a missing or drifting manifest. Gemini 3.1 Pro medium remains outside this wrapper's reviewed compound-slug route because the accepted account inventory has no `gemini-3.1-pro-medium` slug. Codex `0.150.1` is an observational compatibility baseline with no agy dispatch, model, routing, or worker-backend authority. Explicit apply-time Git fetch remains ambient-configuration-aware. | `tests/test-version-manifest-engine.py` generic policy coverage, `tests/test-agy-1-1-22-activation.py` (25 active cases), the fixed 1.1.22 capture suites, `tests/test-update.sh` (325 cases, including fixed transport, supervisor, inventory, and daily-watch policy harnesses), `tests/test-official-github.py` (65 cases), plus packaging ground-truth phase coverage | | `update-notifier.sh`, `scripts/update_notifier.py`, `scripts/update_notifier_child.py` | Optional macOS daily LaunchAgent over a hash-bound snapshot of the read-only watcher. Canonical account HOME, closed transitive source manifest, serialized lifecycle, launchctl reconciliation, parent-death acknowledgement, process-owned signals, drift-fingerprint deduplication, and resumable uninstall; no apply/provider/baseline authority. A valid installed record whose live source bytes changed is `maintenance-required`: one sanitized maintenance notification pauses ordinary watch results until the owner explicitly runs `refresh`, which performs the existing serialized uninstall/install rebind rather than silently adopting code. Refresh alone also recognizes the exact immediately-prior v0.8.0 18-file ledger, uses its historical bindings and authenticated uninstall authority, and creates a fresh current 21-file install; arbitrary legacy shapes and all other commands remain strict. | `tests/test-update-notifier.py` (89 offline fake-control cases) | | `scripts/adoption_measurement.py`, `docs/MEASUREMENT.md` | Explicit owner-private canonical ledger and fixed 30/60/90 aggregate reports. Closed metrics, public evidence URL allowlist, opaque observations, bounded locked append, rolling age-out, no discovery/network/HOME/telemetry, and no activation authority. | `tests/test-adoption-measurement.py` (41 offline cases) | | `scripts/version_attestation_runner.py` | Canonical fixed-profile snapshot-backed `--version` attestation; fixed `/usr/bin/python3 -I -S -B` launch under an explicit trusted Apple interpreter/host/local-owner/OS-admin boundary; exact family, component, family-specific alias kind, alias/target identity, executable/no-setid, and no-world-writable-directory/resolved-executable checks; bounded UID/GID/mode diagnostics; one exact Popen; bounded streams/pre-reap group cleanup; and private durable binding. Production owns nonthrowing signal observation through flushed output and a fixed-priority completion snapshot before `os._exit`; ignored/caller-blocked signals are excluded, and embedded restoration is an explicit caller handoff. Synthetic-only self-test. It does not prove binary provenance, code signing, host attestation, or same-user/hostile-PR tamper resistance. Production execution remains a separate explicit action. | `tests/test-version-attestation-runner.py` (165 cases) | @@ -290,22 +290,22 @@ does not establish same-user tamper resistance. | `scripts/models_capture_runner.py` | Separate capture-only fixed-profile mechanism for one future explicitly authorized real-account `models` observation, pinned to the reviewed models-runner bytes. Canonical stdin binds the exact owner-private account HOME identity, reviewed version binding and retained snapshot; the child receives that HOME plus capture-owned private TMP/XDG/cwd and a fixed environment. Nofollow component/identity revalidation, exact empty post-child scratch, 25-second/64-KiB bounds, pre-reap process-group closure, and process-owned mode-0600 no-overwrite publication make the final `models.capture.sha256` marker provisional until flushed bounded success output and the fixed-priority signal snapshot, followed by `os._exit`. Any bounded exit-zero stdout/stderr is captured without inventory or error interpretation. It independently rejects equality or either containment direction between account HOME and the source or snapshot, so a hand-authored canonical profile cannot bypass the process-inert builder. It does not inspect HOME contents, accept inventory, advance metadata, route, retry, log in, or prompt; tests make no real-account call. The external CLI may mutate/cache in HOME and the runner cannot detect or revert it; account residuals can remain after rejection. The account HOME/local owner/same-UID processes/reviewed source and interpreter/OS admins are trusted; no same-user tamper resistance is claimed. | `tests/test-models-capture-runner.py` (84 fake-account cases) | | `scripts/models_capture_profile.py` | Process-inert preparation/validation of the runner's exact ten-field canonical profile. It accepts only `--prepare` or `--validate` bounded stdin, traverses explicit account/source/snapshot/version/output authorities with no-follow descriptors, binds existing version evidence and external snapshot identity without requiring snapshot co-location, and creates one mode-0600 no-overwrite profile through fsynced hard-link publication with bounded rollback. Its production CLI owns the signal/output boundary and keeps the profile provisional through the flushed result and fixed-priority completion snapshot before `os._exit`; the builder remains process-inert and has no Popen authority. It never imports process-capable runners, enumerates HOME, inherits environment, launches a child, uses Git/network/provider authority, or authorizes capture. The local owner/same-UID processes/OS/interpreter remain its TCB; source-contract tests are selected reviewed-source drift controls, not coordinated hostile-source resistance or provenance proof. | `tests/test-models-capture-profile.py` (121 synthetic cases) | | `scripts/version_manifest_version_evidence.py`, `scripts/version_manifest_capture_profile.py`, `scripts/version_manifest_capture_runner.py`, `scripts/version_manifest_capture_classifier.py`, `scripts/version_manifest_reprofile.py`; thin `scripts/models_capture_1_1_22_*.py` adapters | Shared production implementations for version evidence, process-inert profile preparation, bounded capture, sidecar failure classification, and nlink-only reprofile. Runtime version and manifest path select closed digest-bound constants without changing the algorithms; the stable 1.1.22 filenames are thin CLI adapters. Their existing authority limits remain: version evidence owns one private `--version` child; profile and reprofile launch no child or enumerate HOME; capture owns one no-retry process group and private bounded artifacts; classifier emits only a sanitized mode-0600 record and grants no activation. A version row may require a macOS kernel-reported read-only mount for a disposable capture snapshot (for example, an owner-prepared UDRO image); digest and descriptor/path evidence plus the mount flag are rebound before and after the child. This blocks the observed in-place update route, but not a re-exec from a writable location; same-UID or administrator tampering remains outside the guarantee. | Fixed 1.1.22 suites: version evidence 45, profile 30, runner 63, classifier 24, reprofile 88 offline cases | -| `scripts/version_manifest_engine.py`, `scripts/version_copy_guard.py`, `compat/agy-version-manifest.json`, `compat/version-manifest.schema.json`; historical records under `compat/reviews/` | Digest-bound closed-schema candidate/current/legacy/previous/historical support tiers shared by the five production operations and active compatibility validation. A candidate binds established source/distribution inputs for version-evidence only, without inventing recovery or inventory evidence; capture and activation stay unavailable until their evidence-bearing tier is explicitly established. Current 1.1.27 permits all five operations plus activation; legacy 1.1.22 preserves its fixed adapters without activation authority; previous 1.1.26 permits only generic version-evidence/profile/capture; historical 1.1.24, 1.1.16, and 1.1.12 permit no executable operation. Capture policy is explicit per version (`stable` by default or macOS read-only mount) and does not authorize a provider call. The retired 1.1.12/1.1.16 version-stamped algorithms are absent while their evidence and review records remain unchanged. Activation requires the manifest and fails closed when it is absent; the copy guard permits only the five stable 1.1.22 adapters and rejects new version-stamped algorithm copies even if a manifest row exists. A manifest row is configuration, not live activation authority: separate reviewed evidence and canary are still required. | `tests/test-version-manifest-engine.py` (28 offline cases), `tests/test-agy-1-1-22-activation.py` (25 active cases), fixed 1.1.22 suites, and packaging synchronization checks | +| `scripts/version_manifest_engine.py`, `scripts/version_copy_guard.py`, `compat/agy-version-manifest.json`, `compat/version-manifest.schema.json`; historical records under `compat/reviews/` | Digest-bound closed-schema candidate/current/legacy/previous/historical support tiers shared by the five production operations and active compatibility validation. A candidate binds established source/distribution inputs for version-evidence only, without inventing recovery or inventory evidence; capture and activation stay unavailable until their evidence-bearing tier is explicitly established. Current 1.2.2 permits all five operations plus activation; legacy 1.1.22 preserves its fixed adapters without activation authority; previous 1.1.27 and 1.1.26 permit only generic version-evidence/profile/capture; historical 1.1.24, 1.1.16, and 1.1.12 permit no executable operation. Capture policy is explicit per version (`stable` by default or macOS read-only mount) and does not authorize a provider call. The retired 1.1.12/1.1.16 version-stamped algorithms are absent while their evidence and review records remain unchanged. Activation requires the manifest and fails closed when it is absent; the copy guard permits only the five stable 1.1.22 adapters and rejects new version-stamped algorithm copies even if a manifest row exists. A manifest row is configuration, not live activation authority: separate reviewed evidence and canary are still required. | `tests/test-version-manifest-engine.py` (28 offline cases), `tests/test-agy-1-1-22-activation.py` (25 active cases), fixed 1.1.22 suites, and packaging synchronization checks | | `codex-usage-report.sh`, `skills/agy-worker/runtime/codex-usage-report.sh`, `scripts/codex_usage_report.py`, `skills/agy-worker/runtime/scripts/codex_usage_report.py` | Root compatibility entry plus privacy-safe, version-pinned Codex CLI 0.150.1 usage observation; exact generated-schema digest preflight; live bounded JSONL stdio app-server protocol with independently drained streams and process-group cleanup; thread-bound response validation; separate cumulative and latest-phase reporting of input, cached input, net-new input, cache-write, output, and reasoning (subset); explicit owner-private session-file parsing; strict redaction of cwd, prompts, messages, raw logs, thread IDs, account IDs, and paths; estimated credits as provider estimates without inferring money/quota. | `tests/test-codex-usage-report.py` | | `bug-report.sh`, `scripts/bug-report.py`, `.github/ISSUE_TEMPLATE/` | Local privacy filtering, exact double confirmation for public bug/improvement submission, explicit private-only security drafts, fixed-destination issue submission, and conservative non-proof keyword barrier | `tests/test-reporting.sh` (47 cases) | | `feedback-triage.sh`, `scripts/feedback-triage.py`, `.github/workflows/feedback-watch.yml` | Explicit or weekly bounded read-only aggregate over fixed public issue metadata; raw issue content is not requested, surfaced, or agent input; no GitHub writes | `tests/test-feedback-triage.py` (26 cases) plus packaging workflow policy | -| `.codex-plugin/plugin.json`, `.agents/plugins/marketplace.json`, `docs/MARKETPLACE.md` | Codex skills-only package identity plus a root-source (`.`) repo marketplace contract. The entry names the one canonical `skills/agy-worker/` bundle/runtime and is not installation or publication evidence. | `tests/test-packaging.sh` (494 cases) plus platform validators | -| `PRIVACY.md`, `TERMS.md`, `SUPPORT.md` | Public data disclosure, project policy, and support route | `tests/test-packaging.sh` (494 cases) plus review | -| `docs/index.md`, `docs/VERIFYING_AGENT_OUTPUT.md`, `docs/_layouts/`, `docs/_config.yml`, `docs/sitemap.xml` | Static GitHub Pages landing, source-grounded verification tutorial, canonical metadata, mobile table/inline-code overflow containment, and sitemap; enabling Pages and submitting the sitemap through Search Console remain external | `tests/test-packaging.sh` (494 cases) plus rendered desktop/mobile review | -| `docs/assets/brand/`, `scripts/validate-brand-assets.py` | Approved light/dark master marks, pixel-hinted micro variants, favicon PNGs, social preview, and dependency-free asset validation | `tests/test-packaging.sh` (494 cases) plus rendered review | +| `.codex-plugin/plugin.json`, `.agents/plugins/marketplace.json`, `docs/MARKETPLACE.md` | Codex skills-only package identity plus a root-source (`.`) repo marketplace contract. The entry names the one canonical `skills/agy-worker/` bundle/runtime and is not installation or publication evidence. | `tests/test-packaging.sh` (510 cases) plus platform validators | +| `PRIVACY.md`, `TERMS.md`, `SUPPORT.md` | Public data disclosure, project policy, and support route | `tests/test-packaging.sh` (510 cases) plus review | +| `docs/index.md`, `docs/VERIFYING_AGENT_OUTPUT.md`, `docs/_layouts/`, `docs/_config.yml`, `docs/sitemap.xml` | Static GitHub Pages landing, source-grounded verification tutorial, canonical metadata, mobile table/inline-code overflow containment, and sitemap; enabling Pages and submitting the sitemap through Search Console remain external | `tests/test-packaging.sh` (510 cases) plus rendered desktop/mobile review | +| `docs/assets/brand/`, `scripts/validate-brand-assets.py` | Approved light/dark master marks, pixel-hinted micro variants, favicon PNGs, social preview, and dependency-free asset validation | `tests/test-packaging.sh` (510 cases) plus rendered review | | `CONTRIBUTING.md`, `SECURITY.md`, `CODE_OF_CONDUCT.md`, `.github/pull_request_template.md` | Contribution workflow, private vulnerability route, conduct enforcement, and review checklist | human review plus relevant offline suites | -| `skills/agy-worker/runtime/scripts/agy_dispatch_containment.py` | Native macOS scoped provider launch and identity rebinding for a fresh Gitless stage, private HOME/TMP, and inherited filesystem restrictions. The bound provider image receives non-local TCP 443, local resolver and TCP listener permissions, and reviewed Keychain access; no recipient allowlist is claimed. Exact `/usr/bin/security` shares only the reviewed Keychain services; arguments, items, and read/change/delete operations cannot be restricted by this rule. The macOS listener rule permits wildcard binds; local TCP outbound remains denied. Self-verification denies network and credentials. Process cleanup covers only the bound process group; detached descendants remain confined but are not proven reaped. Unsupported hosts reject native launch. Real-provider compatibility still requires version-specific live qualification. | `tests/test-provider-containment.py` (native cases require an unsandboxed macOS test runner; Linux checks unsupported-host rejection) | +| `skills/agy-worker/runtime/scripts/agy_dispatch_containment.py` | Native macOS scoped provider launch and identity rebinding for a fresh Gitless stage, private HOME/TMP, and inherited filesystem restrictions. The bound provider image receives metadata/existence access to only its executable's exact parent for Core Foundation SSL initialization; directory listing, sibling access, and exec helpers remain denied, and self-verification receives no exception. The exact provider image also receives metadata-only access to private HOME ancestors so SQLite can resolve its conversation database path; ancestor data/listing and other images stay denied. The bound provider image receives non-local TCP 443, local resolver and TCP listener permissions, and reviewed Keychain access; no recipient allowlist is claimed. A bounded read-only default-Keychain lookup supplies an owner-validated file locator and minimal private-HOME preference, without copying owner preferences or reading/hashing credentials. Exact `/usr/bin/security` shares the reviewed Keychain services and receives read-only access to that literal file; provider/other-executable file access, parent access, writes, and network authority do not expand. Prelaunch rebinding and exact repair reuse reject locator/preference drift. Native preparation also binds generated private AGY settings that express staged reads and exact scope write selectors for bounded attempts; current-stage OS containment remains unchanged, owner settings are neither copied nor changed, and drift rejects repair. Session and self-verification receive no permission settings. Arguments, items, and service read/change/delete operations cannot be restricted by this rule. The macOS listener rule permits wildcard binds; local TCP outbound remains denied. Self-verification denies network and credentials. Process cleanup covers only the bound process group; detached descendants remain confined but are not proven reaped. Unsupported hosts reject native launch. Real-provider compatibility still requires version-specific live qualification. | `tests/test-provider-containment.py` (native cases require an unsandboxed macOS test runner; Linux checks unsupported-host rejection) | | `skills/agy-worker/runtime/scripts/agy_dispatch_verification.py` | Optional verification manifests, required/optional check selection, selected-content copies, bounded private logs, and advisory-only V2 feedback. The existing dispatcher binds candidate/manifest authority, permits one check action per attempt, shares the total budget, and recovers interrupted actions without replay. Stored advisory feedback can feed the same conversation; worker text never becomes a command or feedback log. Final acceptance remains driver-owned. | `tests/test-self-verification.py` (portable contracts plus native macOS containment), `tests/test-self-verification-lifecycle.py` (real controller state and scoped copies; mocked check execution) | | `.github/workflows/test.yml`, `scripts/ci-offline.sh`, `scripts/ci_stages.py`, `scripts/ci-worktree-check.sh`, `scripts/ci-diff-check.sh`, `scripts/ci_diff_check.py`, `scripts/ci_timing.py`, `scripts/ci_sharding.py` | Required `test` verifies exact full macOS offline coverage via four parallel fail-closed shards (`dispatcher`, `dispatcher-remediation`, `other-a`, `other-b`) on PRs (and explicit exact-SHA manual dispatch), cancels stale same-PR runs, and does not repeat the suite after a normal merge. A single committed-range diff-hygiene preflight gates the shards. Each job checks out the exact immutable head SHA; each shard runs its registered stage subset from the 44-stage canonical manifest (`scripts/ci_stages.py`), and emits a mode-0600 no-overwrite privacy-safe v2 receipt with per-stage monotonic durations; standalone validation retains read-only v1 shape/digest compatibility, but same-run publication and aggregate acceptance require v2. GitHub retains the uploaded workflow artifact for one day under repository Actions access. The aggregate `test` job runs with `if: always()` and succeeds only when all four unique shard receipts exist, preflight and all producer jobs succeeded, all match the expected head and inventory, and every canonical stage appears exactly once. Lower CI wall time from sharding does not mean lower compute, token usage, cost, or weaker verification. Canonical local runner `./scripts/ci-offline.sh` checks tracked changes and non-ignored untracked candidate files for whitespace, then runs all 44 offline stages, including all 39 registered suite commands, by default without network or provider calls; explicit syntax bytecode is externalized without leaking the cache prefix into ordinary suites, and `--timing-report` observes monotonic wall time. | `tests/test-ci-sharding.py` (103 cases), `tests/test-ci-timing.py` (46 cases), `tests/test-ci-worktree-check.py`, plus packaging policy tests and GitHub Actions | | `.github/workflows/compatibility-watch.yml` | Daily/manual macOS observation of fixed official evidence; bounded Step Summary only, never a required PR or metadata/action path | static policy tests in `tests/test-update.sh` plus GitHub Actions observation | | `.github/workflows/feedback-watch.yml` | Weekly/manual Linux metadata-only feedback aggregate; read-only GitHub permissions, no raw issue content in logs or prompts, and no issue mutations | packaging policy tests plus GitHub Actions observation | | `README.md`, `docs/INSTALLATION.md`, `docs/USAGE.md`, `docs/PROJECT_WORKFLOW.md`, `docs/OPERATIONS.md`, `docs/DOCUMENTATION_POLICY.md`, `docs/public-files.allowlist`, `scripts/validate-docs.py` | Compact first-visit onboarding plus task-owned installation, usage, project-lifecycle, and operations guides under a progressive-disclosure, single-owner, public-claim, inline-link/anchor, ordered-onboarding, Pages-mapping, complete public-docs inventory, and permanent 450-line README contract | `python3 scripts/validate-docs.py . --readme-max-lines 450`, `tests/test-packaging.sh`, and `agents-md-auditor` | -| `docs/ROADMAP.md` | Dependency-ordered product slices with explicit implemented, candidate, blocked, feature-request, or deferred status; published v0.5.0 through v0.15.0 history; published v0.17.0 normal-session behavior and the local v0.18.0 agy 1.1.27 compatibility candidate; the separately governed pending SkillStore reassessment for v0.14.0; and release-external P2-D Codex-usage observation requirements. Historical release tree identity and separately verified rewritten tag identity remain distinct evidence. Source, tests, and the owning public task guide remain current-behavior authority, while tag, release, account-backed evidence, and live-provider state remain separately verifiable. | human review; publication claims remain prohibited until their gates complete | +| `docs/ROADMAP.md` | Dependency-ordered product slices with explicit implemented, candidate, blocked, feature-request, or deferred status; published v0.5.0 through v0.15.0 history; published v0.17.0 normal-session behavior, v0.18.0 agy 1.1.27 history, and the v0.19.0 AGY 1.2.2 compatibility release candidate; SkillStore publication remains a separate externally verified state; and release-external P2-D Codex-usage observation requirements. Historical release tree identity and separately verified rewritten tag identity remain distinct evidence. Source, tests, and the owning public task guide remain current-behavior authority, while tag, release, account-backed evidence, and live-provider state remain separately verifiable. | human review; publication claims remain prohibited until their gates complete | | `AGENTS.md`, `docs/lessons_learned.md`, this file | Durable contributor rules, context routing, and architecture rationale | `agents-md-auditor` after material changes plus packaging policy checks | ## Trust boundaries diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index 1da0fa5..5f6a1b9 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -12,14 +12,24 @@ scope; reuse existing authority when it covers the work. This roadmap does not i authorize code, commit, push, pull-request, merge, release, live model use, or another external action. -## v0.18.0 candidate +## v0.19.0 — release candidate -This candidate reconciles AGY 1.1.27 with the existing normal-session task, -same-conversation repair, and Boost workflows. Model mappings and caller-owned -permissions remain unchanged. The current compatibility evidence and its limits -belong in the [activation record](../compat/reviews/agy-1.1.27-activation.md). -Final candidate verification, independent release acceptance, and publication -remain separate delivery steps. +v0.19.0 activates AGY 1.2.2 after bounded session and native normal and +same-conversation repair qualification. It recognizes refused actions and observed +partial-output timeouts, preserves useful candidates, repairs native conversation +persistence, and shortens the skill's main instructions. The unchanged fourteen-slug +model mappings and caller-owned selection remain intact. The +[activation record](../compat/reviews/agy-1.2.2-activation.md) owns the evidence and +limits. The stable implementation passed all 44 offline CI stages and independent +acceptance. Release publication and installation are verified separately against +the exact public commit; marketplace visibility is a separate external state. + +## v0.18.0 — released + +v0.18.0 established the prior AGY 1.1.27 normal-session compatibility baseline. +Its [historical activation record](../compat/reviews/agy-1.1.27-activation.md) retains +the accepted cases and the limits of its Boost observations. Marketplace visibility +is verified separately from the GitHub release. ## v0.17.0 — released @@ -118,11 +128,11 @@ Every roadmap slice must preserve all of these rules: ## Current agy inventory correction -The accepted agy `1.1.27` reconciliation combines documented `--effort` and +The accepted agy `1.2.2` reconciliation combines documented `--effort` and machine-readable `models` surfaces with one separately authorized, version-bound JSON model capture. The exact unchanged 14-slug list, its SHA-256, reviewed release revision, inventory binding, and bounded behavior limits live in -[`../compat/reviews/agy-1.1.27-activation.md`](../compat/reviews/agy-1.1.27-activation.md). +[`../compat/reviews/agy-1.2.2-activation.md`](../compat/reviews/agy-1.2.2-activation.md). The earlier [`1.1.22` observation](../compat/reviews/agy-1.1.22.md) remains a non-activating historical failed-capture record, and the [`1.1.12` reconciliation](../compat/reviews/agy-1.1.12.md) remains historical evidence. @@ -130,7 +140,7 @@ Agent and plugin catalogs were not part of that bounded review and remain outside this contract. This advances the binding without turning advertised flags or historical failure -behavior into broader agy `1.1.27` promises: +behavior into broader agy `1.2.2` promises: - Do not expose `--effort` before G0 reconciles official releases/source/docs with a sandbox-correct inventory and bounded behavior tests. G1 may then expose the same @@ -141,7 +151,7 @@ behavior into broader agy `1.1.27` promises: documented commands and validate their expected semantic output; neither an unknown subcommand's exit code nor generic usage text is compatibility evidence. - Do not assume agy's separate `--model` and `--effort` flags compose safely. - The current bounded `1.1.27` JSON inventory advertises the compound slugs. + The current bounded `1.2.2` JSON inventory advertises the compound slugs. The official release and documentation were human-reconciled, but this repository has not yet completed evidence that establishes dual-selector composition or precedence. G1 therefore resolves a verified base/effort pair to @@ -323,7 +333,7 @@ exact fixed GitHub REST paths with no ambient proxy or redirect path, and a boun process-group supervisor also contains installed version probes. Check/watch makes no Git network request. The explicit `apply` fetch remains a separately authorized ambient-Git transport path and is not claimed hardened by this slice. The later agy -`1.1.27` reconciliation is the exact active version/release/inventory/matrix binding. +`1.2.2` reconciliation is the exact active version/release/inventory/matrix binding. The first authorized 1.1.22 JSON capture remains historical failed evidence; its later accepted capture established the prior baseline, and the separately accepted 1.1.24 capture advanced the fourteen-slug inventory from Gemini 3.5 Flash to Gemini 3.8 Flash. @@ -636,7 +646,7 @@ provenance, code-signing verification, or OS attestation. tuple is an observational same-version change detector, not a verified release, source revision, signature, or baseline. Official release, source, documentation, and distribution evidence are non-activating review inputs; the separately accepted - 1.1.27 capture and human reconciliation, not the canary, advance the active baseline + 1.2.2 capture and human reconciliation, not the canary, advance the active baseline and G1 matrix. - **Baseline advancement:** A maintainer may advance either verified baseline only after reconciling official docs, release notes, and available release evidence; @@ -645,9 +655,11 @@ provenance, code-signing verification, or OS attestation. running every offline suite and syntax/compile/diff check; and recording the exact reviewed revisions. If behavior affecting dispatch changed, a bounded job against an explicit public fixture is a separate live-data approval, not part of the watch. - The watch never performs this reconciliation. agy `1.1.27` is active because its - capture, strict inventory, and bounded normal/repair paths were separately reconciled; - the activation record states the failed pre-fix Boost canary and offline fix coverage. Any later version + The watch never performs this reconciliation. agy `1.2.2` is active in the local + development candidate because its capture, strict inventory, and bounded + session/native normal and repair paths were separately reconciled; the + [activation record](../compat/reviews/agy-1.2.2-activation.md) states the limits and + makes no new live Boost claim. Any later version or release movement returns the result to drift-review until another reconciliation is accepted. - **Resolution-matrix rule:** G0 derives model-specific effort support and its single @@ -655,7 +667,7 @@ provenance, code-signing verification, or OS attestation. bounded CLI behavior—not from a provider API table or a model-name guess. The matrix records its agy version and release revision. Any agy version/release drift makes it stale and keeps effort resolution disabled until human reconciliation. - The verified `1.1.27` inventory exposes compound slugs: Gemini 3.8 Flash, Gemini 3.7 + The verified `1.2.2` inventory exposes compound slugs: Gemini 3.8 Flash, Gemini 3.7 Flash, and Gemini 3.6 Flash have low/medium/high. Gemini 3.7 `minimal` is outside the reviewed inventory. Official Gemini 3.1 Pro supports medium effort, but the accepted account inventory has no reviewed `gemini-3.1-pro-medium` compound slug, so the @@ -696,7 +708,7 @@ provenance, code-signing verification, or OS attestation. - **Minimum accept tests:** Fixed fake official sources unchanged return `0`; installed versus verified differences and stale review dates are reported separately and return `3`; unavailable network returns `2` with an inconclusive label; absent - future-version evidence retains `1.1.27` and AMBER; version-bound resolution + future-version evidence retains `1.2.2` and AMBER; version-bound resolution fixtures reproduce every documented pair-to-compound-slug mapping, preserve fixed no-level/thinking/medium-labelled entries, and mark drift stale; a raw `gemini-3.6-flash-high` selection remains pass-through, unranked, recommendation-only, diff --git a/scripts/codex_usage_report.py b/scripts/codex_usage_report.py index f266b9a..12e828d 100755 --- a/scripts/codex_usage_report.py +++ b/scripts/codex_usage_report.py @@ -738,7 +738,7 @@ def read_responses() -> None: "method": "initialize", "params": { "capabilities": {"experimentalApi": True}, - "clientInfo": {"name": "codex-agy-worker", "version": "0.18.0"}, + "clientInfo": {"name": "codex-agy-worker", "version": "0.19.0"}, }, }) read_responses() diff --git a/skills/agy-worker/SKILL.md b/skills/agy-worker/SKILL.md index 50ca30a..a11302b 100644 --- a/skills/agy-worker/SKILL.md +++ b/skills/agy-worker/SKILL.md @@ -1,19 +1,18 @@ --- name: agy-worker -description: Let Codex use Google Antigravity CLI (agy) for repository exploration, bounded feature work, and project-scale implementation. Use when a Codex task benefits from delegated repository work while Codex retains diff review, verification, repair, and delivery assurance. +description: Use when Codex should delegate repository exploration or implementation to Google Antigravity CLI (agy), then review, verify, repair, and deliver the result. license: MIT compatibility: Requires OpenAI Codex CLI, Bash, Python 3, git, and agy with provider network access. Claude and Claude Code hosts are not supported. metadata: author: cagdasyurekli - version: "0.18.0" + version: "0.19.0" --- # Delegate repository work and verify the result -Use this skill for useful repository exploration, bounded implementation, or broad -project work through `agy`. The worker discovers ordinary structure and proposes or -makes changes; Codex remains responsible for scope, diff review, driver-owned checks, -repair decisions, and the final assurance label. +Use this skill when `agy` can usefully explore a repository, implement bounded work, +or help with broader project changes. Codex retains responsibility for scope, diff +review, driver-owned checks, repair decisions, and the final assurance label. Resolve the installed package instead of guessing a checkout path: @@ -21,29 +20,21 @@ Resolve the installed package instead of guessing a checkout path: PIPELINE="$(bash "$SKILL_ROOT/scripts/resolve-pipeline.sh")" || exit $? ``` -Optionally check offline prerequisites before spending provider quota: +`"$PIPELINE/doctor.sh" --repo /absolute/path/to/target` can check offline +prerequisites before provider use. `ready` does not prove authentication, provider +availability, task quality, or future job success. For package orientation, read the +[Package README](README.md). -```bash -"$PIPELINE/doctor.sh" --repo /absolute/path/to/target -``` - -`ready` covers offline prerequisites only. It does not prove authentication, provider -availability, task quality, or future job success. For installation and package -orientation, read [Package README](README.md). - -## Before every provider launch +## Authorize provider work -Obtain explicit approval for the exact transmission mode, repository content, and task -being sent unless that exact provider transmission was already approved. - -For an ordinary job, prepare one concrete approval package covering foreseeable -provider work, readable/writable scope, derived repair feedback, model, and budgets. -Reuse that authority while it remains applicable. Fresh state hashes and internal -candidate checks are mechanical bindings, not requests for another human approval. -Ask again only for a material change in authority, content exposure, destination, or -budget. Minimize approvals without treating exactly one approval as a hard requirement. -Prepare manifests and private paths for the user; ordinary jobs require neither -hand-authored JSON nor a Goal. +Before a provider launch, obtain explicit human approval for the exact provider-readable +content, transmission and isolation mode, task, caller-selected model, and budget. +One concrete approval package may cover foreseeable provider work and bounded repair; +reuse it while those facts remain unchanged. Approval is a human decision. Preview, +transmission, state, candidate, and dispatch SHA values are mechanical bindings to +that decision; refreshing a still-applicable binding is not another approval request. +Ask again only when authority, content exposure, destination, or budget materially +changes. Ordinary jobs require neither hand-authored JSON nor a Goal. Prefer `--provider-scope FILE --approve-transmission-sha SHA256` for bounded jobs. It binds exact reviewed read entries, their selected-content digest, and a write subset, then stages only selected entries in a fresh owner-private mode-`0700` Gitless provider cwd. Whole-worktree dispatch remains an explicit exception. Treat the entire disposable worktree passed as `--workdir` as worker-readable and potentially transmissible to Google/Gemini, regardless of requested edit paths; `--add-dir`, prompt denylist instructions, `qa-gate --only`, and `--allow` do not narrow that read boundary. @@ -56,47 +47,27 @@ Keep raw worker logs and local controller state outside the worktree and out of Installation does not authorize provider transmission, Git actions, publication, or acceptance. -Before every provider-launch attempt (initial start/run, resume, continue, and restart), tell the user in one or two concise user-facing sentences what task is being sent to AGY. -Include a short public-safe task label, caller-selected model information, caller-selected effort when separately selectable, and the exact resolved model slug. -For default selection where no model is selected or the default tier is used, state truthfully that the provider default model is used and that model or effort is unresolved, without inventing a resolved slug or thinking level. -For fixed/compound/literal models where effort is not separately selectable, state that accurately without inferring backend reasoning or inventing a thinking level. -The notice must precede every dispatch attempt and remain accurate afterward. -If preflight fails before provider launch, explicitly state that the task was not sent to AGY. -If provider reach is genuinely uncertain, state that it is unverified rather than claiming success. +For the required user-facing provider-launch notice, including defaults, preflight +failures, resumes, and continuation, read [Project lifecycle and verification](references/PROJECT_LIFECYCLE_AND_VERIFICATION.md#approval-bindings-and-launch-notices). Direct model and effort selection remain caller-owned; recommendations are advisory. -For the complete transmission, environment, verifier, and compatibility boundaries, -read [Security and compatibility](references/SECURITY_AND_COMPATIBILITY.md). - -## Route the request +## Choose and run the workflow -| User intent | Workflow | Default cycle budget | Driver responsibility | +| User intent | Workflow | Default cycle budget | Codex responsibility | |---|---|---:|---| | Explore, understand, review, or plan | `explore` | 2 | Spot-check material claims and state coverage limits. | -| Implement a feature, refactor, tests, or a bounded repair | `task` | 2 | Inspect the diff and run relevant project checks. | -| Build a project or perform broad audit-and-fix work | `project` | 5 | Review repo-wide changes and run build/test/lint as applicable. | - -`explore` and `task` accept `1..2` cycles; `project` accepts `1..5`. Personas -are optional prompt specializations, not capability, approval, routing, verification, -or quality gates. - -The advanced raw dispatcher also offers an opt-in `--boost` profile for one bounded -`task` cycle. Boost may invoke provider-side subagents and protected tools, so it -requires the exact job-bound `--approve-boost-risk-sha` printed by the rejected -preflight. The acknowledgement does not grant runtime permissions or widen provider -scope. Boost is restricted to `accept-edits`, one cycle, no persona, and default slash -protection; the controller accepts a result only when the provider init frame reports -both `agent=Boost` and `permission_mode=request-review`. A Boost job cannot resume, -restart, or continue. Treat any failed attempt as terminal and request a new job and -fresh approval rather than reusing its conversation. - -For material UX, lifecycle, trust-boundary, security, data-semantics, or other domain plans: -A coordinator and suitable domain expert must co-plan. -Freeze user journeys, acceptance tests, and authority/privacy constraints before implementation. -The final acceptor must be a different agent or fresh context; no planner or implementer may self-accept. +| Implement a feature, refactor, tests, or bounded repair | `task` | 2 | Inspect the diff and run relevant project checks. | +| Build a project or perform broad audit-and-fix work | `project` | 5 | Review repo-wide changes and run applicable build, test, and lint checks. | + +`explore` and `task` accept `1..2` cycles; `project` accepts `1..5`. Personas are +optional prompt specializations, not capability, approval, routing, verification, or +quality gates. The raw `--boost` profile is an advanced, separately acknowledged +one-cycle task path; read [Security and compatibility](references/SECURITY_AND_COMPATIBILITY.md#boost-authority-boundary) +before using it. + +For material UX, lifecycle, trust-boundary, security, data-semantics, or other domain +plans, use the co-planning policy in [Project lifecycle and verification](references/PROJECT_LIFECYCLE_AND_VERIFICATION.md#material-planning-governance). Purely mechanical changes are exempt. -Verification v2 and the controller bind candidate evidence, not agent identity or governance. -The final human-readable handoff must report the planner/reviewer separation. Explicit delegation-first requires running the `delegation-policy.sh` evaluator before substantive repository work. The controller records are local: the runtime cannot infer prior work or approval and @@ -104,69 +75,39 @@ must never silently authorize direct-Codex fallback after a missing approval, ha stop, preflight failure, provider failure, or exhausted budget. Direct-Codex and second-eye work remain explicit policy choices. -## Use the primary lifecycle - -Prefer the portable `workflow.sh` facade for `run --preview`, approved `run`, -read-only `status`, and `verify-finalize`. For ordinary use, supply only an absolute -repository and job ID; optional `--base` overrides the first-call `HEAD` binding. The -facade derives owner-private state and delegates branch-backed disposable-worktree -creation to the job lifecycle. Review the content-free preview, then repeat the same -repository/job ID with `--approve-whole-worktree` and its exact manifest digest, or -preview with `--provider-scope` and repeat it with the exact -`--approve-transmission-sha`. Preview and stale approval retain those -bindings. Explicit state/worktree/branch/base inputs remain an all-or-nothing advanced -compatibility surface. The lifecycle may roll back only clean façade-created resources -from the same failing pre-dispatch invocation and refuses any drift or dispatch evidence. - -Facade `status` can project an explicitly supplied existing low-level job state, -dispatcher state, or dispatcher job ID. Treat its available actions as read-only facts; -run any mutation through the named low-level lifecycle authority. - -After a candidate arrives: - -1. Inspect the actual Git diff; do not trust `files_changed` or worker prose. -2. Select build, test, lint, or type-check commands yourself. Never execute - `commands_run` or `tests_run` from an envelope. -3. Run writable checks in a verification copy so generated artifacts cannot change - the bound candidate. -4. Bind only sanitized driver findings to the current candidate in Verification v2. -5. Continue the same conversation for a bounded repair when useful, or finalize with - an accurate `verified`, `partially_verified`, `rejected`, or `blocked` disposition. - -Do not delete a useful candidate merely because a check fails or the cycle budget -ends. A fresh `restart` is an explicit user decision, not an automatic retry. - -The copyable facade, lifecycle-state, Verification v2, isolated-copy, gate/receipt, -and finalization procedures live in -[Project lifecycle and verification](references/PROJECT_LIFECYCLE_AND_VERIFICATION.md). -For actionable failure diagnosis, read -[Troubleshooting](references/TROUBLESHOOTING.md). +Prefer `workflow.sh` for `run --preview`, approved `run`, read-only `status`, and +`verify-finalize`. Review the content-free preview, then run with its exact approved +whole-worktree or scoped binding. The facade does not choose a model, assurance label, +repair, retry, Git action, or external write. Read the lifecycle guide for facade +examples, low-level recovery, Verification v2, and required notices. + +After a candidate arrives, inspect the actual Git diff; select and run checks yourself +in an isolated verification copy, never an envelope's `commands_run` or `tests_run`; +bind only sanitized driver findings to the current candidate; then finalize honestly or +request a bounded same-conversation repair. A scoped candidate can receive another +provider turn only when initial dispatch included +`--allow-scoped-repair`, which binds the same approved scope, selected model, +conversation, and budgets. Preserve useful work when a check fails or the cycle budget +ends; `restart` is an explicit user decision. See [Project lifecycle and verification](references/PROJECT_LIFECYCLE_AND_VERIFICATION.md). ## Hard stops and delivery -Do not dispatch or continue when: - -- the exact provider transmission is unapproved; -- secrets, denied paths, or unrelated private content remain anywhere in the default - worktree transmission or inside scoped entries approved for staging; -- the requested write can escape the disposable worktree, enter `.git`, or traverse - a symlink boundary; -- the task requires dangerous permission or approval bypass flags; -- a commit, push, PR, feedback submission, release, installation, update, account - action, or other external write lacks its own authorization. - -Unknown files, incomplete architecture knowledge, an unknown first test command, lack -of a persona, or a failed first check are not hard stops by themselves. Discover what -is needed, preserve the candidate, and report evidence limits honestly. - -Before changing agy-facing flags or claims, resolve the installed package and run -`"$PIPELINE/ground-truth.sh"` without arguments, then inspect its current `agy --help` -output; do not describe that interface from memory. Its default interface phase invokes -only `agy --version` and `agy --help`. `--account` is a separate explicit action because -it inspects account-owned model, agent, plugin, and local-settings state. An agy exit -zero with empty ordinary output is possible: consume `result.structured_output`, not -the echoed schema. - -Before delivery, review the exact candidate bytes and run the relevant driver-owned -checks. Report only what those checks establish. Do not claim provider success, -completeness, release state, security, or general correctness from offline evidence. +Do not dispatch or continue without exact provider approval; when approved +provider-readable content contains secrets, denied paths, or unrelated private files; +when writes can escape the disposable worktree, enter `.git`, or traverse a symlink +boundary; with dangerous permission or approval-bypass flags; or when a Git action, +publication, installation, account action, or other external write lacks its own +authorization. + +Unknown architecture, an unknown first test command, lack of a persona, or a failed +first check are not hard stops. Discover what is needed, preserve the candidate, and +report evidence limits. Before changing agy-facing flags or claims, run +`"$PIPELINE/ground-truth.sh"` and inspect its current `agy --help`; consume +`result.structured_output` when ordinary agy output is empty. Its default phase is +version/help only; `--account` is a separate explicit action. + +Before delivery, review the exact candidate bytes and run relevant driver-owned checks. +Report only what those checks establish: `verified`, `partially_verified`, `rejected`, +or `blocked`. Offline checks do not prove provider success, completeness, release state, +security, or general correctness. Use [Troubleshooting](references/TROUBLESHOOTING.md) +for actionable failures. diff --git a/skills/agy-worker/references/PROJECT_LIFECYCLE_AND_VERIFICATION.md b/skills/agy-worker/references/PROJECT_LIFECYCLE_AND_VERIFICATION.md index 5c2efa1..410e660 100644 --- a/skills/agy-worker/references/PROJECT_LIFECYCLE_AND_VERIFICATION.md +++ b/skills/agy-worker/references/PROJECT_LIFECYCLE_AND_VERIFICATION.md @@ -9,6 +9,33 @@ Read [Security and compatibility](SECURITY_AND_COMPATIBILITY.md) before a first dispatch. Use [Troubleshooting](TROUBLESHOOTING.md) when a preflight, provider, lifecycle, or verifier step fails. +## Approval, bindings, and launch notices + +Human approval authorizes an exact provider transmission boundary and its intended +work. `launch_approval_sha256`, `transmission_sha256`, `state_sha256`, candidate, and +dispatch SHA values mechanically bind that decision to current reviewed facts; they do +not themselves grant authority. Refresh a binding only when the already-approved +action remains available. Request fresh human authority when content exposure, +destination, isolation mode, scope, or budget materially changes. A normal job needs +no hand-authored JSON or Goal. + +One approved package may cover foreseeable provider work and a bounded repair. For +whole-worktree mode, use the preview's exact `launch_approval_sha256`; for provider +scope, use the exact `transmission_sha256`. Approval reuse does not authorize a Git +action, acceptance, publication, installation, account action, or a new provider +execution beyond the approved job. + +Before every provider-launch attempt (initial start/run, resume, continue, and restart), tell the user in one or two concise user-facing sentences what task is being sent to AGY. +Include a short public-safe task label, caller-selected model information, caller-selected effort when separately selectable, and the exact resolved model slug. +For default selection where no model is selected or the default tier is used, state truthfully that the provider default model is used and that model or effort is unresolved, without inventing a resolved slug or thinking level. +For fixed/compound/literal models where effort is not separately selectable, state that accurately without inferring backend reasoning or inventing a thinking level. +The notice must precede every dispatch attempt and remain accurate afterward. +If preflight fails before provider launch, explicitly state that the task was not sent to AGY. +If provider reach is genuinely uncertain, state that it is unverified rather than claiming success. +Direct model and effort selection remain caller-owned; recommendations are advisory. + +The notice does not require another response while the existing approval applies. + ## Lifecycle at a glance 1. Capture an immutable base commit and create a branch-backed disposable worktree. @@ -179,6 +206,9 @@ At initial dispatch, `--allow-scoped-repair` binds permission to continue the sa scoped task/project within its approved write scope, selected model, conversation, and budgets. Candidate evolution inside that grant does not require a new human approval. External drift or a changed grant is rejected before another provider turn. +Without that initial grant, a scoped candidate is result/finalize-only. A fresh state +or candidate binding does not by itself require fresh human approval, but it cannot +extend the original grant. For optional local checks, the driver prepares an owner-private manifest and supplies `--self-verification-manifest PATH` at initial task/project dispatch. Both options @@ -285,6 +315,16 @@ commands. A bounded repair request may cite failed checks, missing checks, advis results, coverage gaps, or review findings. It must continue the same conversation while budget remains and must be preceded by the provider notice. +## Material planning governance + +For material UX, lifecycle, trust-boundary, security, data-semantics, or other domain plans: +A coordinator and suitable domain expert must co-plan. +Freeze user journeys, acceptance tests, and authority/privacy constraints before implementation. +The final acceptor must be a different agent or fresh context; no planner or implementer may self-accept. +Purely mechanical changes are exempt. +Verification v2 and the controller bind candidate evidence, not agent identity or governance. +The final human-readable handoff must report the planner/reviewer separation. + ## Assurance and preservation The controller validates and persists Codex's exact disposition; it does not infer a diff --git a/skills/agy-worker/references/SECURITY_AND_COMPATIBILITY.md b/skills/agy-worker/references/SECURITY_AND_COMPATIBILITY.md index 891de9a..ac17e65 100644 --- a/skills/agy-worker/references/SECURITY_AND_COMPATIBILITY.md +++ b/skills/agy-worker/references/SECURITY_AND_COMPATIBILITY.md @@ -92,12 +92,30 @@ dispatch retains its separate, explicit authority and does not acquire containme The native profile permits the fresh selected stage, private persistent provider HOME, per-attempt TMP, the exact agy executable, the bound result-schema file as read-only -input, and reviewed system runtime/tool paths. It denies access to the original -checkout, Git administration, and ambient HOME. The +input, and reviewed system runtime/tool paths. The bound provider image also receives +metadata and existence access to its executable's exact parent directory so Core +Foundation can construct the main bundle and SSL policy. It also receives metadata +access to the ancestor chain of private HOME, which SQLite walks when opening a +conversation database. These rules grant no directory listing, sibling-file access, +or ancestor file data; a different executed image loses them. Self-verification +receives no such exception. Apart from the named runtime +exceptions, the profile denies access to the original checkout, Git administration, +and ambient HOME. The whole stage is writable; the write subset is a controller reconciliation boundary, not an OS file-by-file permission list. Fresh copies and prelaunch identity/content checks reject hardlinks and stage drift. +Native provider preparation also generates a private AGY settings file from the +approved scope: staged reads and the exact file/tree write selectors for the job's +bounded attempts. This adds application-level allow rules, without copying or +changing the user's settings or granting commands, URLs, MCP tools, or ambient +paths. The current attempt's native profile still denies access to other stages. +Settings bytes and identity are bound before launch and reused unchanged for repair; +missing or changed settings stop preparation instead of being overwritten. The +profile denies writes to the exact settings file. This is not tamper resistance +against a malicious provider binary or trusted same-user processes. Session mode and +self-verification receive no generated AGY permission settings. + Only the bound agy process image receives non-local TCP 443, DNS resolution through the local mDNSResponder socket, and local TCP bind/listen permissions. It also receives the reviewed Keychain service access. The macOS `localhost` listener selector also permits IPv4 @@ -106,9 +124,18 @@ agy image can expose a listener to the local network. Outbound connections to lo TCP services remain denied. This is neither a Google recipient allowlist nor TLS protocol enforcement. A fork without exec retains that image privilege; exec to a different program removes its network authority. The exact `/usr/bin/security` -helper also receives the same five reviewed Keychain/trust Mach services so AGY can -reuse its saved account session. It receives no additional network or filesystem -access. Seatbelt cannot limit helper arguments, operations, or Keychain items: this +helper also receives the same five reviewed Keychain/trust Mach services and +read-only access to the exact bound default Keychain file. The driver discovers that +file through a bounded, read-only default-Keychain lookup, checks its owner and stable +file identity without reading or hashing its contents, and creates a minimal +`DefaultKeychain` preference in the private provider HOME. It copies no owner +preferences or credential bytes. Prelaunch checks bind the generated preference and +Keychain identity; repair reuses the exact preference without overwriting drift. +Discovery or binding failure stops before AGY starts. This mechanism does not prove +that a usable AGY token exists or that its access controls permit silent login. +The file rule applies only to `/usr/bin/security`; it grants no provider or other +executable access, parent-directory access, file writes, or additional network access. +Seatbelt cannot limit helper arguments, operations, or Keychain items: this permits broader same-user Keychain reads, additions, changes, and deletions where the OS allows them, not only an AGY-token lookup. Other executable images receive neither exception. Local self-verification has no network or Keychain access. diff --git a/skills/agy-worker/runtime/benchmarks/v1/portable-source.json b/skills/agy-worker/runtime/benchmarks/v1/portable-source.json index 97051e0..84942d1 100644 --- a/skills/agy-worker/runtime/benchmarks/v1/portable-source.json +++ b/skills/agy-worker/runtime/benchmarks/v1/portable-source.json @@ -1 +1 @@ -{"files":[{"mode":"100755","path":"benchmark.sh","sha256":"7e0033f6bf3eec1e6007752ef67e8e33db8f39c7136790a5403d651b39eeab8d"},{"mode":"100755","path":"qa-gate.sh","sha256":"878cf09fbc982b70895f8bf1335892d7291a56616434f64b58334c91c8f87d3f"},{"mode":"100755","path":"verify-job.sh","sha256":"b719ebac651b83d27d6fe2af47a0d454a43a4ab649afe98093fcc13d9164073c"},{"mode":"100755","path":"scripts/benchmark.py","sha256":"a6e8527194b6394767d173d66610a6f57e427438765e955741a96239308d6c2e"},{"mode":"100755","path":"scripts/candidate_state.py","sha256":"de6c5c8006ac641a29b37bb2aaf17b4fe0d00d6844806ba382aa19ce6852efa7"},{"mode":"100755","path":"scripts/compatibility.py","sha256":"c7366f2b11864c6f6000bb0c274269e86e47b59a4d76ea06835863a5e222cd80"},{"mode":"100755","path":"scripts/evidence_receipt.py","sha256":"eaa1878355c541bdd7e5f3411002b6e4bb4860f7989f0397ba9237aaec76c3c6"},{"mode":"100755","path":"scripts/model_selection.py","sha256":"9e0be61a5a5900a2f11c7c9ca8799ba204e9e3d006638d3114407937cd7aedea"},{"mode":"100755","path":"scripts/recommendation_record.py","sha256":"2d8eb267535d7fd73c83185c1c8dc20f2639676371b9fc245f9b976bbd5437ba"},{"mode":"100755","path":"scripts/validate-envelope.py","sha256":"87799cbcc981ae38a5f96ab3191043047fd8de91d60ed98ee31e6666bf1091eb"},{"mode":"100644","path":"schemas/benchmark-plan.schema.json","sha256":"34c31fd50bee7e459f9be65bdd64c6af4673010c389763d7c59010065de662dc"},{"mode":"100644","path":"schemas/benchmark-result.schema.json","sha256":"f90ea7a9ff62f943e66349a6a48fef33b619e7191e2fbcbac97620cce1a376ad"},{"mode":"100644","path":"schemas/evidence-receipt.schema.json","sha256":"377e9161580cbf9b802074dcc09897b89b1e31e99f1f60a06bec30c12561f500"},{"mode":"100644","path":"schemas/worker-result.schema.json","sha256":"f2589ae5249b395dc90279af07600298b6b4354d1fbe0e2efe3fa72832e9a3b0"},{"mode":"100644","path":"schemas/worker-result.provider.schema.json","sha256":"d17bf6d47ddf89f57644ce94f154c370cb92f65f174255066c79e7cc233f6fdd"}],"kind":"agy-worker-benchmark-portable-source","schema_version":1,"source_revision":"offline-benchmark-v1"} +{"files":[{"mode":"100755","path":"benchmark.sh","sha256":"7e0033f6bf3eec1e6007752ef67e8e33db8f39c7136790a5403d651b39eeab8d"},{"mode":"100755","path":"qa-gate.sh","sha256":"878cf09fbc982b70895f8bf1335892d7291a56616434f64b58334c91c8f87d3f"},{"mode":"100755","path":"verify-job.sh","sha256":"b719ebac651b83d27d6fe2af47a0d454a43a4ab649afe98093fcc13d9164073c"},{"mode":"100755","path":"scripts/benchmark.py","sha256":"a6e8527194b6394767d173d66610a6f57e427438765e955741a96239308d6c2e"},{"mode":"100755","path":"scripts/candidate_state.py","sha256":"de6c5c8006ac641a29b37bb2aaf17b4fe0d00d6844806ba382aa19ce6852efa7"},{"mode":"100755","path":"scripts/compatibility.py","sha256":"b42cbb740aedfc3e2c183a5ea1bf5e43bf60e992987b38e36b7cc49f285bce5d"},{"mode":"100755","path":"scripts/evidence_receipt.py","sha256":"eaa1878355c541bdd7e5f3411002b6e4bb4860f7989f0397ba9237aaec76c3c6"},{"mode":"100755","path":"scripts/model_selection.py","sha256":"9e0be61a5a5900a2f11c7c9ca8799ba204e9e3d006638d3114407937cd7aedea"},{"mode":"100755","path":"scripts/recommendation_record.py","sha256":"2d8eb267535d7fd73c83185c1c8dc20f2639676371b9fc245f9b976bbd5437ba"},{"mode":"100755","path":"scripts/validate-envelope.py","sha256":"87799cbcc981ae38a5f96ab3191043047fd8de91d60ed98ee31e6666bf1091eb"},{"mode":"100644","path":"schemas/benchmark-plan.schema.json","sha256":"34c31fd50bee7e459f9be65bdd64c6af4673010c389763d7c59010065de662dc"},{"mode":"100644","path":"schemas/benchmark-result.schema.json","sha256":"f90ea7a9ff62f943e66349a6a48fef33b619e7191e2fbcbac97620cce1a376ad"},{"mode":"100644","path":"schemas/evidence-receipt.schema.json","sha256":"377e9161580cbf9b802074dcc09897b89b1e31e99f1f60a06bec30c12561f500"},{"mode":"100644","path":"schemas/worker-result.schema.json","sha256":"f2589ae5249b395dc90279af07600298b6b4354d1fbe0e2efe3fa72832e9a3b0"},{"mode":"100644","path":"schemas/worker-result.provider.schema.json","sha256":"d17bf6d47ddf89f57644ce94f154c370cb92f65f174255066c79e7cc233f6fdd"}],"kind":"agy-worker-benchmark-portable-source","schema_version":1,"source_revision":"offline-benchmark-v1"} diff --git a/skills/agy-worker/runtime/compat/agy-last-reviewed.txt b/skills/agy-worker/runtime/compat/agy-last-reviewed.txt index 98624ac..fce5b20 100644 --- a/skills/agy-worker/runtime/compat/agy-last-reviewed.txt +++ b/skills/agy-worker/runtime/compat/agy-last-reviewed.txt @@ -1 +1 @@ -2026-09-06 +2026-09-13 diff --git a/skills/agy-worker/runtime/compat/agy-model-effort-matrix.json b/skills/agy-worker/runtime/compat/agy-model-effort-matrix.json index b4e0760..3ee4a40 100644 --- a/skills/agy-worker/runtime/compat/agy-model-effort-matrix.json +++ b/skills/agy-worker/runtime/compat/agy-model-effort-matrix.json @@ -2,8 +2,8 @@ "schema_version": 1, "resolution_status": "active", "inventory": { - "agy_version": "1.1.27", - "reviewed_source_revision": "1ae9cb7b51667192c051b73a91099c71e816ca5f", + "agy_version": "1.2.2", + "reviewed_source_revision": "ba985e6b5de2ac8aa09860a154a102831eb7722b", "evidence": [ "agy-models", "official-release", diff --git a/skills/agy-worker/runtime/compat/agy-model-effort-matrix.sha256 b/skills/agy-worker/runtime/compat/agy-model-effort-matrix.sha256 index 4f0eaee..cd08770 100644 --- a/skills/agy-worker/runtime/compat/agy-model-effort-matrix.sha256 +++ b/skills/agy-worker/runtime/compat/agy-model-effort-matrix.sha256 @@ -1 +1 @@ -56ee4cefdf918184e8bae57c49f01c51c18e49b30ff0bd4b322d8801703dfaac +2c12abf09910489b681a01c88b43e8fbaf7df3a68fd715c7280c7f6fff6c89e4 diff --git a/skills/agy-worker/runtime/compat/agy-models-inventory-binding.json b/skills/agy-worker/runtime/compat/agy-models-inventory-binding.json index f485195..e52f9d6 100644 --- a/skills/agy-worker/runtime/compat/agy-models-inventory-binding.json +++ b/skills/agy-worker/runtime/compat/agy-models-inventory-binding.json @@ -1,11 +1,11 @@ { "schema_version": 1, "status": "accepted-current-inventory", - "agy_version": "1.1.27", - "reviewed_source_revision": "1ae9cb7b51667192c051b73a91099c71e816ca5f", - "source_sha256": "d583be1344ea9cfa0c45cff2c1342af7837f4833c4edb65e69bee84776a45caa", - "version_binding_sha256": "a1f8b651123f1e95ef7e744ae45e9066967b6ceb0a710ddfd685e54b14b0b0a6", - "capture_record_sha256": "ac8ddc28fcca90a20e05f6bd0678d32550db451a8e3402e4c287154eab289b33", + "agy_version": "1.2.2", + "reviewed_source_revision": "ba985e6b5de2ac8aa09860a154a102831eb7722b", + "source_sha256": "cabadc15a61944372bede1fdff186701c17467dd9d718e97dc79283055d3c101", + "version_binding_sha256": "cc3bd8bb44b31891e2bed4c4367ed90ae2334e0df29e9fba2e94bce7c3105bef", + "capture_record_sha256": "73d65488b618b721d70d85076cc1ca67c8ead62e0606588502aeb69e5230d032", "capture_stdout_sha256": "d02970e6b6b4e0910461999afca8fb99d757e9094ab2874b557dad18fc75464a", "capture_response_sha256": "b1cc011310435afa07b1e132a5b7f3e22297aa21427177461c858bcbd6a58794", "inventory_normalized_sha256": "d5e58ab55e91ebd4a2cd23841c76cbe12b47d607c62cd8c834fc8f6b9f078ad7", diff --git a/skills/agy-worker/runtime/compat/agy-models-inventory-binding.sha256 b/skills/agy-worker/runtime/compat/agy-models-inventory-binding.sha256 index f2bd8c0..1bf9ecc 100644 --- a/skills/agy-worker/runtime/compat/agy-models-inventory-binding.sha256 +++ b/skills/agy-worker/runtime/compat/agy-models-inventory-binding.sha256 @@ -1 +1 @@ -3a554f90922321700120e2c398f865a4894b2128087a4bac90968ddf2409762d +efcc59a983b0c9f60309a55047188d800f5e34d2dc42a38d9339cdc361e13802 diff --git a/skills/agy-worker/runtime/compat/agy-upstream-head.txt b/skills/agy-worker/runtime/compat/agy-upstream-head.txt index 73e8d81..d0f31fd 100644 --- a/skills/agy-worker/runtime/compat/agy-upstream-head.txt +++ b/skills/agy-worker/runtime/compat/agy-upstream-head.txt @@ -1 +1 @@ -1ae9cb7b51667192c051b73a91099c71e816ca5f +ba985e6b5de2ac8aa09860a154a102831eb7722b diff --git a/skills/agy-worker/runtime/compat/agy-verified-version.txt b/skills/agy-worker/runtime/compat/agy-verified-version.txt index 93325dd..23aa839 100644 --- a/skills/agy-worker/runtime/compat/agy-verified-version.txt +++ b/skills/agy-worker/runtime/compat/agy-verified-version.txt @@ -1 +1 @@ -1.1.27 +1.2.2 diff --git a/skills/agy-worker/runtime/compat/agy-version-manifest.json b/skills/agy-worker/runtime/compat/agy-version-manifest.json index bc69f7c..3636f12 100644 --- a/skills/agy-worker/runtime/compat/agy-version-manifest.json +++ b/skills/agy-worker/runtime/compat/agy-version-manifest.json @@ -2,9 +2,7 @@ "schema_version": 1, "kind": "agy-version-manifest", "versions": { - "1.1.27": { - "version": "1.1.27", - "support_tier": "current", + "1.2.2": { "allowed_operations": [ "activation", "capture", @@ -13,6 +11,57 @@ "reprofile", "version-evidence" ], + "capture_runner_source_sha256": "34fd925d3d66a8fa46d7308ec1f67e5a1578fb4f961197993f5bfb17b6ec8677", + "capture_snapshot_policy": "macos-readonly-mount", + "distribution_sha512": "8a3b5edea51e107a74413cea5eed1c5b02dede945ba21c7625b7c86f477c2c8ac423581de0ed84ff2f97c3bf6818c1fc24ca84cf9a76c2fb02a50e89d8a0d29a", + "distribution_url": "https://storage.googleapis.com/antigravity-public/antigravity-cli/1.2.2-6061403484848128/darwin-arm/cli_mac_arm64.tar.gz", + "expected_stdout": "1.2.2\n", + "output_profile_name": "models.capture.1.2.2.profile.json", + "prior_name": "agy-models-capture-1.2.2.version", + "recovery_binding_sha256": "cc3bd8bb44b31891e2bed4c4367ed90ae2334e0df29e9fba2e94bce7c3105bef", + "recovery_runner_bytes": 48402, + "recovery_runner_sha256": "16fe57ed938bd482aa8df99e5e42914984fdb0b9f22de59919a5ae64aa020f98", + "recovery_stdout": "1.2.2\n", + "recovery_summary_bytes": 260, + "release_commit": "ba985e6b5de2ac8aa09860a154a102831eb7722b", + "source_sha256": "cabadc15a61944372bede1fdff186701c17467dd9d718e97dc79283055d3c101", + "source_size": 180089984, + "support_tier": "current", + "version": "1.2.2", + "historical_recovery_binding_sha256": "a1f8b651123f1e95ef7e744ae45e9066967b6ceb0a710ddfd685e54b14b0b0a6", + "historical_recovery_source_sha256": "d583be1344ea9cfa0c45cff2c1342af7837f4833c4edb65e69bee84776a45caa", + "reprofile_output_name": "models.capture.1.2.2.reprofile.json", + "failure_ruleset_version": "agy-1.2.2-failure-rules-v1", + "capture_record_sha256": "73d65488b618b721d70d85076cc1ca67c8ead62e0606588502aeb69e5230d032", + "capture_stdout_sha256": "d02970e6b6b4e0910461999afca8fb99d757e9094ab2874b557dad18fc75464a", + "capture_response_sha256": "b1cc011310435afa07b1e132a5b7f3e22297aa21427177461c858bcbd6a58794", + "inventory_normalized_sha256": "d5e58ab55e91ebd4a2cd23841c76cbe12b47d607c62cd8c834fc8f6b9f078ad7", + "slug_count": 14, + "slugs": [ + "claude-opus-4-6-thinking", + "claude-sonnet-4-6", + "gemini-3.1-pro-high", + "gemini-3.1-pro-low", + "gemini-3.6-flash-high", + "gemini-3.6-flash-low", + "gemini-3.6-flash-medium", + "gemini-3.7-flash-high", + "gemini-3.7-flash-low", + "gemini-3.7-flash-medium", + "gemini-3.8-flash-high", + "gemini-3.8-flash-low", + "gemini-3.8-flash-medium", + "gpt-oss-120b-medium" + ] + }, + "1.1.27": { + "version": "1.1.27", + "support_tier": "previous", + "allowed_operations": [ + "capture", + "profile", + "version-evidence" + ], "expected_stdout": "1.1.27\n", "source_sha256": "d583be1344ea9cfa0c45cff2c1342af7837f4833c4edb65e69bee84776a45caa", "source_size": 177426912, diff --git a/skills/agy-worker/runtime/compat/agy-version-manifest.sha256 b/skills/agy-worker/runtime/compat/agy-version-manifest.sha256 index 83fd61c..e57cd58 100644 --- a/skills/agy-worker/runtime/compat/agy-version-manifest.sha256 +++ b/skills/agy-worker/runtime/compat/agy-version-manifest.sha256 @@ -1 +1 @@ -898bc921c15aa342b9306600aa1e380bb2930e80de465c76a99b87ef34b0d6e0 +5b28e6f05c6e63c2022b461cf2aac596f9f3a4f4a1aa72957b5a62a5c334428e diff --git a/skills/agy-worker/runtime/doctor.sh b/skills/agy-worker/runtime/doctor.sh index d0ff911..328c7b5 100755 --- a/skills/agy-worker/runtime/doctor.sh +++ b/skills/agy-worker/runtime/doctor.sh @@ -3,7 +3,7 @@ set -uo pipefail DOCTOR_SCHEMA_VERSION=1 -DOCTOR_EXPECTED_AGY_SOURCE_REVISION='1ae9cb7b51667192c051b73a91099c71e816ca5f' +DOCTOR_EXPECTED_AGY_SOURCE_REVISION='ba985e6b5de2ac8aa09860a154a102831eb7722b' doctor_usage() { echo "usage: doctor.sh [--repo DIR] [--format text|json]" >&2 diff --git a/skills/agy-worker/runtime/ground-truth.sh b/skills/agy-worker/runtime/ground-truth.sh index cd37f48..501f325 100755 --- a/skills/agy-worker/runtime/ground-truth.sh +++ b/skills/agy-worker/runtime/ground-truth.sh @@ -69,7 +69,13 @@ PY fi echo cat <<'EOF' -## verified behavioural facts (empirical, 2026-08-01, agy 1.1.9) +## historical behavioural observations (2026-08-01, agy 1.1.9) + +These observations belong to the stated version, not the installed version above. +Revalidate version-sensitive behavior before relying on it. In particular, AGY +1.2.2 deprecates `unsandboxed` rules in favor of `command` rules; this report does +not change account permissions. Do not automatically retry authentication, +permission, quota, or environment blockers. - The prompt is `--print`'s ARGUMENT VALUE. agy ignores stdin in print mode. With `--print` placed before other flags, agy reads the NEXT FLAG as the message. @@ -79,9 +85,9 @@ cat <<'EOF' appears only on stderr. Always check stripped stdout content, never just $?. - Under `--sandbox`, running a shell command needs an `unsandboxed()` allow-rule. A `command()` rule alone is NOT sufficient. -- Authentication is INTERMITTENT: a run may fail with an interactive OAuth prompt - and the identical next run succeeds. Bounded retry handles this; do not conclude - from a single failure that agy is unauthenticated. +- Authentication was observed to be intermittent: an interactive OAuth failure + was followed by a successful identical invocation. That observation does not + authorize retrying an account or authentication blocker. - `stream-json` event shape: {"event":"...","init":...}, then repeated {"event":"step_update",...}, then exactly one {"event":"result","result":{...}}. The schema-validated answer is at result.structured_output. diff --git a/skills/agy-worker/runtime/schemas/model-selection.schema.json b/skills/agy-worker/runtime/schemas/model-selection.schema.json index cc8a14a..d39bbc7 100644 --- a/skills/agy-worker/runtime/schemas/model-selection.schema.json +++ b/skills/agy-worker/runtime/schemas/model-selection.schema.json @@ -9,16 +9,16 @@ "oneOf": [ { "properties": { - "installed_agy_version": {"const": "1.1.27"}, - "matrix_agy_version": {"const": "1.1.27"}, + "installed_agy_version": {"const": "1.2.2"}, + "matrix_agy_version": {"const": "1.2.2"}, "version_relation": {"const": "match"}, "compatibility_status": {"const": "reviewed-version-match"} } }, { "properties": { - "installed_agy_version": {"not": {"const": "1.1.27"}}, - "matrix_agy_version": {"const": "1.1.27"}, + "installed_agy_version": {"not": {"const": "1.2.2"}}, + "matrix_agy_version": {"const": "1.2.2"}, "version_relation": {"const": "drift"}, "compatibility_status": {"const": "critical-interface-compatible-version-drift"} } @@ -27,8 +27,8 @@ }, "v3_approved_help": { "properties": { - "installed_agy_version": {"not": {"const": "1.1.27"}}, - "matrix_agy_version": {"const": "1.1.27"}, + "installed_agy_version": {"not": {"const": "1.2.2"}}, + "matrix_agy_version": {"const": "1.2.2"}, "version_relation": {"const": "drift"}, "compatibility_status": {"const": "critical-interface-compatible-version-drift"} } diff --git a/skills/agy-worker/runtime/scripts/agy_dispatch.py b/skills/agy-worker/runtime/scripts/agy_dispatch.py index 5e1f482..9707a89 100755 --- a/skills/agy-worker/runtime/scripts/agy_dispatch.py +++ b/skills/agy-worker/runtime/scripts/agy_dispatch.py @@ -4097,7 +4097,58 @@ def _event(line: bytes) -> tuple[bool, str | None, str | None]: return True, conversation, event -def _classify_stderr(path: Path, version: str, returncode: int) -> str: +def _reviewed_provider_timeout_lines(version: str, seconds: object) -> set[bytes]: + """Build only the exact 1.2.2 timeout lines bound to this job's limit. + + The installed binary exposes a ``%s`` duration slot but its unavailable + source does not establish whether that slot retains the integer-seconds + flag spelling or uses Go's canonical whole-second duration spelling. The + wrapper always supplies a positive integer number of seconds. Accept only + those two equivalent spellings for that exact bound value. + """ + if version != "1.2.2" or type(seconds) not in (int, float): + return set() + if not math.isfinite(seconds) or seconds <= 0 or seconds != int(seconds): + return set() + total = int(seconds) + if total > 7 * 24 * 3600: + return set() + raw = f"{total}s" + if total < 60: + canonical = raw + elif total < 3600: + minutes, remainder = divmod(total, 60) + canonical = f"{minutes}m{remainder}s" + else: + hours, remainder = divmod(total, 3600) + minutes, remainder = divmod(remainder, 60) + canonical = f"{hours}h{minutes}m{remainder}s" + prefix = "[agy] print timeout after " + suffix = " with turn in progress; returning partial output" + return { + f"{prefix}{duration}{suffix}".encode("ascii") + for duration in {raw, canonical} + } + + +def _has_reviewed_provider_timeout( + path: Path, version: str, seconds: object, +) -> bool: + expected = _reviewed_provider_timeout_lines(version, seconds) + if not expected: + return False + try: + raw = path.read_bytes() + except OSError: + return False + return bool(expected.intersection(raw.splitlines())) + + +def _classify_stderr( + path: Path, version: str, returncode: int, provider_timeout_seconds: object = None, +) -> str: + if _has_reviewed_provider_timeout(path, version, provider_timeout_seconds): + return "provider_timeout" if returncode == 0: return "empty_output" try: @@ -4213,14 +4264,14 @@ def _quota_terminal_failure(stream: Path, version: str) -> tuple[str, int | None return "provider_quota_exhausted", retry -def _has_1_1_27_denied_actions(stream: Path, version: str) -> bool: - """Recognize only the documented 1.1.27 top-level denial signal. +def _has_reviewed_denied_actions(stream: Path, version: str) -> bool: + """Recognize only reviewed exact-version top-level denial signals. Its payload is provider-owned and deliberately never interpreted or copied into public state. Call this only after the terminal envelope has passed schema validation, so presence cannot turn an invalid report into a candidate. """ - if version != "1.1.27": + if version not in {"1.1.27", "1.2.2"}: return False result = _terminal_result(stream, strict=True) return isinstance(result, dict) and "denied_actions" in result @@ -4300,6 +4351,7 @@ def controller(job: Path, ownership_fd: int) -> int: started_mono: float | None = None runtime_end_mono: float | None = None runtime_frozen = False + idle_timeout_with_exited_provider = False def interrupted(number: int, _frame: Any) -> None: nonlocal stop_signal @@ -4584,6 +4636,8 @@ def drain_reaped_streams() -> None: ), allow_keychain=True, read_only_inputs=(contained_argv[schema_index],), + provider_max_cycles=command["max_cycles"], + provider_write_selectors=scope["write"], ) # The prior attempt budget is still a hard stop, but bounded # controller-local proofs do not become a provider timeout. @@ -4917,6 +4971,9 @@ def drain_reaped_streams() -> None: process, contained_root, contained=prepared_containment is not None, ) + idle_timeout_with_exited_provider = bool( + reason == "idle_timeout" and returncode == 0 + ) process = None runtime_end_mono = time.monotonic() # A hard/max boundary stops semantic event processing, but a @@ -4953,13 +5010,21 @@ def drain_reaped_streams() -> None: outer_status: str | None = None provider_retry_after: int | None = None provider_retry_observed: float | None = None + reviewed_idle_partial = bool( + idle_timeout_with_exited_provider + and _has_reviewed_provider_timeout( + stderr_path, + command["agy_version"] if command["agy_version_observed"] else "", + command["max_seconds"], + ) + ) # A deadline is a controller fact, not a reason to discard a # terminal report already emitted by the bounded provider. Parse # that report for candidate/provenance evidence, but never let its # outer SUCCESS/ERROR/CANCELLED disposition publish past the # frozen deadline. Cancellation and binding failures retain their # existing fail-closed precedence and do not enter this path. - if reason in {None, "hard_deadline_exceeded"}: + if reason in {None, "hard_deadline_exceeded"} or reviewed_idle_partial: if reason is None: terminal_failure = _quota_terminal_failure( stream_path, @@ -4977,10 +5042,14 @@ def drain_reaped_streams() -> None: if reason in {None, "hard_deadline_exceeded"} and sizes["stdout"] == 0: if reason is None: reason = ( - _classify_stderr(stderr_path, command["agy_version"], returncode) - if returncode != 0 else "empty_output" + _classify_stderr( + stderr_path, + command["agy_version"] if command["agy_version_observed"] else "", + returncode, + command["max_seconds"], + ) ) - elif reason in {None, "hard_deadline_exceeded"}: + elif reason in {None, "hard_deadline_exceeded"} or reviewed_idle_partial: try: if schema_paths is None: raise DispatchError("dispatch schema binding is unavailable") @@ -4992,14 +5061,24 @@ def drain_reaped_streams() -> None: if result_binding is None and reason is None: reason = "invalid_envelope" elif ( - reason is None - and result_binding is not None - and _has_1_1_27_denied_actions( + result_binding is not None + and reason != "hard_deadline_exceeded" + and _has_reviewed_denied_actions( stream_path, command["agy_version"] if command["agy_version_observed"] else "", ) ): - reason = "permission_required" + reason, limit_kind = "permission_required", None + elif ( + result_binding is not None + and reason != "hard_deadline_exceeded" + and _has_reviewed_provider_timeout( + stderr_path, + command["agy_version"] if command["agy_version_observed"] else "", + command["max_seconds"], + ) + ): + reason, limit_kind = "provider_timeout", None elif reason is None and outer_status == "ERROR": reason = "provider_terminal_error" elif reason is None and outer_status == "CANCELLED": diff --git a/skills/agy-worker/runtime/scripts/agy_dispatch_containment.py b/skills/agy-worker/runtime/scripts/agy_dispatch_containment.py index 451644a..4e47c35 100644 --- a/skills/agy-worker/runtime/scripts/agy_dispatch_containment.py +++ b/skills/agy-worker/runtime/scripts/agy_dispatch_containment.py @@ -23,19 +23,30 @@ import ctypes import dataclasses import hashlib +import json import os from pathlib import Path import platform +import plistlib +import posixpath +import pwd import signal +import select import stat +import subprocess import sys import time from typing import Mapping, Sequence +import uuid SANDBOX_EXEC = Path("/usr/bin/sandbox-exec") +SECURITY_HELPER = Path("/usr/bin/security") MAX_EXECUTABLE_BYTES = 512 * 1024 * 1024 MAX_PROFILE_BYTES = 64 * 1024 +MAX_KEYCHAIN_LOCATOR_BYTES = 4096 +KEYCHAIN_LOCATOR_DEADLINE_SECONDS = 5 +KEYCHAIN_LOCATOR_TERM_GRACE_SECONDS = 1 MAX_GROUP_MEMBERS = 4096 PROCESS_GROUP_CLEANUP_SCOPE = "bound-process-group-only" ROLE_PROVIDER = "provider" @@ -77,6 +88,17 @@ class DirectoryBinding: nlink: int +@dataclasses.dataclass(frozen=True) +class KeychainBinding: + """Metadata-only identity for the database used by Security.framework.""" + + path: str + device: int + inode: int + uid: int + mode: int + + @dataclasses.dataclass(frozen=True) class ProcessIdentity: pid: int @@ -98,6 +120,9 @@ class PreparedContainedLaunch: target: FileBinding profile: FileBinding read_only_inputs: tuple[FileBinding, ...] + keychain: KeychainBinding | None + keychain_preferences: FileBinding | None + provider_settings: FileBinding | None stage: DirectoryBinding private_home: DirectoryBinding attempt_tmp: DirectoryBinding @@ -223,6 +248,140 @@ def _bind_file( os.close(descriptor) +def _bind_keychain(path: str | Path) -> KeychainBinding: + """Bind a default Keychain by metadata without opening its database.""" + candidate = _canonical_existing(path) + try: + before = os.lstat(candidate) + after = os.lstat(candidate) + except OSError as exc: + raise ContainmentError("native provider authentication is unavailable") from exc + mode = stat.S_IMODE(before.st_mode) + stable_identity = lambda item: ( + item.st_dev, item.st_ino, item.st_uid, + stat.S_IFMT(item.st_mode), stat.S_IMODE(item.st_mode), + ) + if ( + stable_identity(before) != stable_identity(after) + or stat.S_ISLNK(before.st_mode) + or not stat.S_ISREG(before.st_mode) + or before.st_uid != os.getuid() + or mode & 0o022 + ): + raise ContainmentError("native provider authentication is unavailable") + return KeychainBinding( + str(candidate), before.st_dev, before.st_ino, before.st_uid, mode, + ) + + +def _terminate_locator( + process: subprocess.Popen[bytes], root: ProcessIdentity, +) -> None: + """Reap a bounded locator without leaving a child process group behind.""" + try: + if not process_group_is_quiescent(root): + terminate_bound_process_group(root, signal.SIGTERM) + deadline = time.monotonic() + KEYCHAIN_LOCATOR_TERM_GRACE_SECONDS + while not process_group_is_quiescent(root) and time.monotonic() < deadline: + time.sleep(0.01) + if not process_group_is_quiescent(root): + terminate_bound_process_group(root, signal.SIGKILL) + deadline = time.monotonic() + KEYCHAIN_LOCATOR_TERM_GRACE_SECONDS + while not process_group_is_quiescent(root) and time.monotonic() < deadline: + time.sleep(0.01) + if process.poll() is None: + process.wait(timeout=KEYCHAIN_LOCATOR_TERM_GRACE_SECONDS) + if not process_group_is_quiescent(root): + raise ContainmentError("native provider authentication is unavailable") + except (ContainmentError, OSError, subprocess.TimeoutExpired) as exc: + raise ContainmentError("native provider authentication is unavailable") from exc + + +def _discover_default_keychain() -> KeychainBinding: + """Discover exactly one default database under the passwd-resolved owner HOME.""" + try: + owner_home = _canonical_existing(pwd.getpwuid(os.getuid()).pw_dir) + owner = os.lstat(owner_home) + if not stat.S_ISDIR(owner.st_mode) or owner.st_uid != os.getuid(): + raise ContainmentError("native provider authentication is unavailable") + helper = _bind_file(SECURITY_HELPER, modes={0o755}, executable=True) + process = subprocess.Popen( + [helper.path, "default-keychain", "-d", "user"], + cwd=str(owner_home), + env={ + "HOME": str(owner_home), "PATH": "/usr/bin:/bin", + "LANG": "C", "LC_ALL": "C", + }, + stdin=subprocess.DEVNULL, + stdout=subprocess.PIPE, + stderr=subprocess.DEVNULL, + start_new_session=True, + close_fds=True, + ) + try: + locator_root = bind_new_process_group(process.pid) + except ContainmentError: + # The direct child is still ours, but without a bound session + # identity we deliberately do not signal a process group. + if process.poll() is None: + process.kill() + process.wait(timeout=KEYCHAIN_LOCATOR_TERM_GRACE_SECONDS) + raise + except (ContainmentError, OSError, KeyError) as exc: + raise ContainmentError("native provider authentication is unavailable") from exc + assert process.stdout is not None + descriptor = process.stdout.fileno() + os.set_blocking(descriptor, False) + output = bytearray() + deadline = time.monotonic() + KEYCHAIN_LOCATOR_DEADLINE_SECONDS + try: + while True: + remaining = deadline - time.monotonic() + if remaining <= 0: + raise ContainmentError("native provider authentication is unavailable") + readable, _, _ = select.select([descriptor], [], [], remaining) + if readable: + chunk = os.read(descriptor, 4096) + if not chunk: + break + output.extend(chunk) + if len(output) > MAX_KEYCHAIN_LOCATOR_BYTES: + raise ContainmentError("native provider authentication is unavailable") + elif process.poll() is not None: + # Polling alone cannot prove stdout is drained, so let select + # observe EOF within the remaining fixed deadline. + continue + if process.wait(timeout=max(0.01, deadline - time.monotonic())) != 0: + raise ContainmentError("native provider authentication is unavailable") + if not process_group_is_quiescent(locator_root): + _terminate_locator(process, locator_root) + raise ContainmentError("native provider authentication is unavailable") + except (ContainmentError, OSError, subprocess.TimeoutExpired) as exc: + if "locator_root" in locals(): + _terminate_locator(process, locator_root) + raise ContainmentError("native provider authentication is unavailable") from exc + finally: + process.stdout.close() + try: + text = bytes(output).decode("utf-8", "strict") + line = text[:-1] if text.endswith("\n") else "" + quoted = line.lstrip(" \t") + if ( + text.count("\n") != 1 + or not text.endswith("\n") + or len(quoted) < 3 + or not quoted.startswith('"') + or not quoted.endswith('"') + ): + raise ValueError("invalid locator output") + path = quoted[1:-1] + if '"' in path: + raise ValueError("invalid locator output") + return _bind_keychain(path) + except (ContainmentError, UnicodeError, ValueError) as exc: + raise ContainmentError("native provider authentication is unavailable") from exc + + def _bind_directory(path: str | Path) -> DirectoryBinding: candidate = _canonical_existing(path) nofollow = getattr(os, "O_NOFOLLOW", 0) @@ -293,6 +452,217 @@ def _publish_profile(path: Path, payload: bytes) -> FileBinding: return _bind_file(path, modes={0o400}, limit=MAX_PROFILE_BYTES) +def _publish_private_file(path: Path, payload: bytes) -> FileBinding: + """Publish a small owner-private internal artifact without replacement.""" + if not payload or len(payload) > MAX_PROFILE_BYTES: + raise ContainmentError("native provider authentication is unavailable") + flags = os.O_WRONLY | os.O_CREAT | os.O_EXCL | getattr(os, "O_NOFOLLOW", 0) + try: + descriptor = os.open(path, flags, 0o600) + except OSError as exc: + raise ContainmentError("native provider authentication is unavailable") from exc + try: + try: + view = memoryview(payload) + while view: + written = os.write(descriptor, view) + if written <= 0: + raise ContainmentError("native provider authentication is unavailable") + view = view[written:] + os.fsync(descriptor) + os.fchmod(descriptor, 0o600) + except OSError as exc: + raise ContainmentError("native provider authentication is unavailable") from exc + finally: + try: + os.close(descriptor) + except OSError as exc: + raise ContainmentError("native provider authentication is unavailable") from exc + return _bind_file(path, modes={0o600}, limit=MAX_PROFILE_BYTES) + + +def _default_keychain_preferences_payload(keychain: KeychainBinding) -> bytes: + """Create the minimal private DefaultKeychain preference from Security's GUID.""" + try: + framework = ctypes.CDLL("/System/Library/Frameworks/Security.framework/Security") + guid = (ctypes.c_ubyte * 16).in_dll(framework, "gGuidAppleCSPDL") + value = { + "DefaultKeychain": [{ + "GUID": "{" + str(uuid.UUID(bytes=bytes(guid))).upper() + "}", + "SubserviceId": 0, + "SubserviceType": 6, + "MajorVersion": 0, + "MinorVersion": 0, + "DbName": keychain.path, + }], + } + return plistlib.dumps(value, fmt=plistlib.FMT_XML, sort_keys=True) + except (AttributeError, OSError, ValueError) as exc: + raise ContainmentError("native provider authentication is unavailable") from exc + + +def _file_binding_record(binding: FileBinding) -> bytes: + return (json.dumps(dataclasses.asdict(binding), sort_keys=True, separators=(",", ":")) + "\n").encode("ascii") + + +def _load_file_binding_record(path: Path) -> FileBinding: + try: + sidecar = _bind_file(path, modes={0o600}, limit=MAX_PROFILE_BYTES) + descriptor = os.open(path, os.O_RDONLY | getattr(os, "O_NOFOLLOW", 0)) + try: + raw = os.read(descriptor, MAX_PROFILE_BYTES + 1) + finally: + os.close(descriptor) + if len(raw) > MAX_PROFILE_BYTES: + raise ValueError("sidecar too large") + value = json.loads(raw.decode("ascii", "strict")) + if set(value) != {field.name for field in dataclasses.fields(FileBinding)}: + raise ValueError("sidecar schema") + binding = FileBinding(**value) + if not isinstance(binding.path, str) or not binding.path.startswith("/"): + raise ValueError("sidecar path") + # Bind after read: sidecar bytes and identity must be stable. + if _bind_file(path, modes={0o600}, limit=MAX_PROFILE_BYTES) != sidecar: + raise ValueError("sidecar changed") + return binding + except (ContainmentError, OSError, UnicodeError, ValueError, TypeError, json.JSONDecodeError) as exc: + raise ContainmentError("native provider authentication is unavailable") from exc + + +def _prepare_keychain_preferences( + job: Path, home: Path, keychain: KeychainBinding, +) -> FileBinding: + """Create once, then only reuse the exact private preference identity.""" + preferences_dir = home / "Library" / "Preferences" + _ensure_private_directory(home / "Library", existing_ok=True) + _ensure_private_directory(preferences_dir, existing_ok=True) + preferences = preferences_dir / "com.apple.security.plist" + sidecar = job / ".provider-keychain-preferences.binding" + payload = _default_keychain_preferences_payload(keychain) + if os.path.lexists(sidecar): + prior = _load_file_binding_record(sidecar) + try: + current = _bind_file(preferences, modes={0o600}, limit=MAX_PROFILE_BYTES) + except ContainmentError as exc: + raise ContainmentError("native provider authentication is unavailable") from exc + if current != prior or current.sha256 != hashlib.sha256(payload).hexdigest(): + raise ContainmentError("native provider authentication is unavailable") + return current + if os.path.lexists(preferences): + raise ContainmentError("native provider authentication is unavailable") + created = _publish_private_file(preferences, payload) + try: + _publish_private_file(sidecar, _file_binding_record(created)) + except ContainmentError: + # Never overwrite or accept a partly-created preference without its + # protected identity record on a later repair attempt. + raise + return created + + +def _validate_provider_write_selectors( + selectors: Sequence[Mapping[str, str]], +) -> tuple[tuple[str, str], ...]: + """Accept only the closed, already-validated scope selector shape.""" + if isinstance(selectors, (str, bytes)): + raise ContainmentError("native provider permission settings are unavailable") + result: list[tuple[str, str]] = [] + previous: str | None = None + forbidden = frozenset("()[]{}*?\\\"'") + for entry in selectors: + if not isinstance(entry, Mapping) or set(entry) != {"kind", "path"}: + raise ContainmentError("native provider permission settings are unavailable") + kind, path = entry["kind"], entry["path"] + if ( + kind not in {"file", "tree"} + or not isinstance(path, str) + or not path + or path.startswith("/") + or path.endswith("/") + or "\x00" in path + or any(character in forbidden for character in path) + ): + raise ContainmentError("native provider permission settings are unavailable") + normalized = posixpath.normpath(path) + parts = normalized.split("/") + if ( + normalized != path + or normalized in {"", ".", ".."} + or normalized.startswith("../") + or any(part in {"", ".", "..", ".git", ".agy-worker-control"} for part in parts) + or previous is not None and path <= previous + ): + raise ContainmentError("native provider permission settings are unavailable") + previous = path + result.append((kind, path)) + return tuple(result) + + +def _provider_settings_payload( + job: Path, max_cycles: int, selectors: tuple[tuple[str, str], ...], +) -> bytes: + if type(max_cycles) is not int or not 1 <= max_cycles <= 5: + raise ContainmentError("native provider permission settings are unavailable") + forbidden = frozenset("()[]{}*?\\\"'") + + def rule_target(path: Path) -> str: + value = str(path) + if ( + not path.is_absolute() + or "\x00" in value + or "\r" in value + or "\n" in value + or any(character in forbidden for character in value) + ): + raise ContainmentError("native provider permission settings are unavailable") + return value + + job_target = rule_target(job) + if Path(os.path.realpath(job_target)) != job: + raise ContainmentError("native provider permission settings are unavailable") + allowed: list[str] = [] + for cycle in range(1, max_cycles + 1): + stage = job / f"stage-{cycle:03d}" + allowed.append(f"read_file({rule_target(stage)})") + allowed.extend( + f"write_file({rule_target(stage / path)})" for _kind, path in selectors + ) + payload = json.dumps( + {"permissions": {"allow": allowed}}, + sort_keys=True, separators=(",", ":"), + ).encode("utf-8") + if not payload or len(payload) > MAX_PROFILE_BYTES: + raise ContainmentError("native provider permission settings are unavailable") + return payload + + +def _prepare_provider_settings( + job: Path, home: Path, max_cycles: int, + selectors: tuple[tuple[str, str], ...], +) -> FileBinding: + """Create once and then exactly rebind the private provider policy.""" + settings_parent = home / ".gemini" / "antigravity-cli" + _ensure_private_directory(home / ".gemini", existing_ok=True) + _ensure_private_directory(settings_parent, existing_ok=True) + settings = settings_parent / "settings.json" + sidecar = job / ".provider-antigravity-settings.binding" + payload = _provider_settings_payload(job, max_cycles, selectors) + if os.path.lexists(sidecar): + prior = _load_file_binding_record(sidecar) + try: + current = _bind_file(settings, modes={0o600}, limit=MAX_PROFILE_BYTES) + except ContainmentError as exc: + raise ContainmentError("native provider permission settings are unavailable") from exc + if current != prior or current.sha256 != hashlib.sha256(payload).hexdigest(): + raise ContainmentError("native provider permission settings are unavailable") + return current + if os.path.lexists(settings): + raise ContainmentError("native provider permission settings are unavailable") + created = _publish_private_file(settings, payload) + _publish_private_file(sidecar, _file_binding_record(created)) + return created + + def _scheme_string(value: str) -> str: if "\0" in value: raise ContainmentError("sandbox profile value contains NUL") @@ -304,6 +674,7 @@ def _scheme_string(value: str) -> str: def render_profile( *, target_executable: str, role: str, network_policy: str, allow_keychain: bool, read_only_inputs: Sequence[str] = (), + keychain_path: str | None = None, provider_settings_path: str | None = None, ) -> bytes: """Render the fixed default-deny profile; dynamic paths use ``-D`` params.""" if role not in _ROLES: @@ -314,6 +685,16 @@ def render_profile( raise ContainmentError("only provider launches may use provider TLS") if role != ROLE_PROVIDER and allow_keychain: raise ContainmentError("only provider launches may access the keychain") + if (allow_keychain and ( + not isinstance(keychain_path, str) or not keychain_path.startswith("/") + )) or (not allow_keychain and keychain_path is not None): + raise ContainmentError("containment keychain input is invalid") + if provider_settings_path is not None and ( + not allow_keychain + or not isinstance(provider_settings_path, str) + or not provider_settings_path.startswith("/") + ): + raise ContainmentError("containment provider settings input is invalid") if len(read_only_inputs) > 8 or any( not isinstance(path, str) or not path.startswith("/") for path in read_only_inputs ): @@ -323,6 +704,17 @@ def render_profile( runtime_reads = "\n(allow file-read*\n" + "".join( f" (literal {_scheme_string(path)})\n" for path in read_only_inputs ) + ")\n" + provider_parent_metadata = "" + if role == ROLE_PROVIDER: + # Core Foundation needs the executable directory's metadata to build + # its main bundle and SSL policy. Keep contents and exec helpers denied. + provider_parent_metadata = f""" +(with-filter (process-path {_scheme_string(target_executable)}) + (allow file-read-metadata file-test-existence + (literal {_scheme_string(str(Path(target_executable).parent))})) + ; SQLite resolves every ancestor before opening its private conversation DB. + (allow file-read-metadata (path-ancestors (param "HOME")))) +""" keychain = "" if allow_keychain: # A fork retains the target image and its exact service authority. An @@ -342,7 +734,9 @@ def render_profile( (global-name "com.apple.securityd.xpc") (global-name "com.apple.securityd.general") (global-name "com.apple.trustd") - (global-name "com.apple.trustd.agent"))) + (global-name "com.apple.trustd.agent")) + (allow file-read* + (literal {_scheme_string(keychain_path)}))) """ network = "" if network_policy == NETWORK_PROVIDER_TLS: @@ -360,6 +754,12 @@ def render_profile( (allow network-bind network-inbound (local tcp "localhost:*")) (allow mach-lookup (global-name "com.apple.mDNSResponder"))) (deny network-outbound (remote ip "localhost:*")) +""" + provider_settings = "" + if provider_settings_path is not None: + provider_settings = f""" +(deny file-write* + (literal {_scheme_string(provider_settings_path)})) """ profile_text = f"""(version 1) (deny default) @@ -419,7 +819,7 @@ def render_profile( (subpath (param "HOME")) (subpath (param "TMPDIR"))) -{runtime_reads}{keychain}{network}""" +{runtime_reads}{provider_parent_metadata}{keychain}{network}{provider_settings}""" return profile_text.encode("utf-8") @@ -445,6 +845,8 @@ def prepare_contained_launch( child_environment: Mapping[str, str], allow_keychain: bool = False, read_only_inputs: Sequence[str | Path] = (), + provider_max_cycles: int | None = None, + provider_write_selectors: Sequence[Mapping[str, str]] = (), ) -> PreparedContainedLaunch: """Create and bind one native selected-content launch envelope.""" require_supported_host() @@ -456,6 +858,12 @@ def prepare_contained_launch( raise ContainmentError("only provider launches may use provider TLS") if role != ROLE_PROVIDER and allow_keychain: raise ContainmentError("only provider launches may access the keychain") + if allow_keychain and provider_max_cycles is None: + raise ContainmentError("native provider permission settings are unavailable") + if not allow_keychain and ( + provider_max_cycles is not None or tuple(provider_write_selectors) + ): + raise ContainmentError("containment provider settings are invalid") if type(attempt) is not int or not 1 <= attempt <= 999: raise ContainmentError("containment attempt is invalid") if not target_argv or any( @@ -473,6 +881,8 @@ def prepare_contained_launch( stage = _bind_directory(stage_dir) if Path(stage.path).parent != Path(job_binding.path): raise ContainmentError("scoped stage must be an immediate job child") + if allow_keychain and Path(stage.path) != Path(job_binding.path) / f"stage-{attempt:03d}": + raise ContainmentError("native provider permission settings are unavailable") launcher = _bind_file(SANDBOX_EXEC, modes={0o755}, executable=True) target = _bind_target(target_executable) if target_argv[0] != target.path: @@ -490,6 +900,33 @@ def prepare_contained_launch( raise ContainmentError("containment read-only inputs are repeated") stem = "provider" if role == ROLE_PROVIDER else "self-verify" home = _ensure_private_directory(job / f"{stem}-home", existing_ok=True) + keychain: KeychainBinding | None = None + keychain_preferences: FileBinding | None = None + provider_settings: FileBinding | None = None + if allow_keychain: + try: + assert provider_max_cycles is not None + selectors = _validate_provider_write_selectors(provider_write_selectors) + if type(provider_max_cycles) is not int or not attempt <= provider_max_cycles <= 5: + raise ContainmentError("native provider permission settings are unavailable") + except ContainmentError as exc: + raise ContainmentError("native provider permission settings are unavailable") from exc + try: + keychain = _discover_default_keychain() + keychain_preferences = _prepare_keychain_preferences( + job, Path(home.path), keychain, + ) + except ContainmentError as exc: + raise ContainmentError("native provider authentication is unavailable") from exc + try: + provider_settings = _prepare_provider_settings( + job, Path(home.path), provider_max_cycles, selectors, + ) + # Creating the private preference subtree changes the directory's + # link count; bind the final provider HOME, not an earlier shape. + home = _bind_directory(home.path) + except ContainmentError as exc: + raise ContainmentError("native provider permission settings are unavailable") from exc attempt_tmp = _ensure_private_directory( job / f"{stem}-tmp-{attempt:03d}", existing_ok=False, ) @@ -499,6 +936,10 @@ def prepare_contained_launch( network_policy=network_policy, allow_keychain=allow_keychain, read_only_inputs=tuple(item.path for item in bound_inputs), + keychain_path=keychain.path if keychain is not None else None, + provider_settings_path=( + provider_settings.path if provider_settings is not None else None + ), ) profile_binding = _publish_profile( job / f"{stem}-sandbox-{attempt:03d}.sb", profile_payload, @@ -523,7 +964,8 @@ def prepare_contained_launch( }) return PreparedContainedLaunch( role, network_policy, platform.release(), platform.version(), launcher, - target, profile_binding, bound_inputs, stage, home, attempt_tmp, tuple(target_argv), + target, profile_binding, bound_inputs, keychain, keychain_preferences, provider_settings, + stage, home, attempt_tmp, tuple(target_argv), tuple(sorted(environment.items())), bool(allow_keychain), ) @@ -556,6 +998,27 @@ def confirm_contained_launch( for item in prepared.read_only_inputs ): raise ContainmentError("containment read-only input changed before launch") + if prepared.allow_keychain: + if prepared.keychain is None or prepared.keychain_preferences is None: + raise ContainmentError("native provider authentication is unavailable") + if _bind_keychain(prepared.keychain.path) != prepared.keychain: + raise ContainmentError("native provider authentication is unavailable") + if _bind_file( + prepared.keychain_preferences.path, modes={0o600}, limit=MAX_PROFILE_BYTES, + ) != prepared.keychain_preferences: + raise ContainmentError("native provider authentication is unavailable") + try: + if prepared.provider_settings is None or _bind_file( + prepared.provider_settings.path, modes={0o600}, limit=MAX_PROFILE_BYTES, + ) != prepared.provider_settings: + raise ContainmentError("native provider permission settings are unavailable") + except ContainmentError as exc: + raise ContainmentError("native provider permission settings are unavailable") from exc + elif ( + prepared.keychain is not None or prepared.keychain_preferences is not None + or prepared.provider_settings is not None + ): + raise ContainmentError("native provider authentication is unavailable") if _bind_directory(prepared.stage.path) != prepared.stage: raise ContainmentError("scoped stage changed before launch") if _bind_directory(prepared.private_home.path) != prepared.private_home: diff --git a/skills/agy-worker/runtime/scripts/codex_usage_report.py b/skills/agy-worker/runtime/scripts/codex_usage_report.py index f266b9a..12e828d 100755 --- a/skills/agy-worker/runtime/scripts/codex_usage_report.py +++ b/skills/agy-worker/runtime/scripts/codex_usage_report.py @@ -738,7 +738,7 @@ def read_responses() -> None: "method": "initialize", "params": { "capabilities": {"experimentalApi": True}, - "clientInfo": {"name": "codex-agy-worker", "version": "0.18.0"}, + "clientInfo": {"name": "codex-agy-worker", "version": "0.19.0"}, }, }) read_responses() diff --git a/skills/agy-worker/runtime/scripts/compatibility.py b/skills/agy-worker/runtime/scripts/compatibility.py index 4954a1e..f5248b7 100755 --- a/skills/agy-worker/runtime/scripts/compatibility.py +++ b/skills/agy-worker/runtime/scripts/compatibility.py @@ -52,11 +52,11 @@ ACTIVE_INVENTORY_BINDING = { "schema_version": 1, "status": "accepted-current-inventory", - "agy_version": "1.1.27", - "reviewed_source_revision": "1ae9cb7b51667192c051b73a91099c71e816ca5f", - "source_sha256": "d583be1344ea9cfa0c45cff2c1342af7837f4833c4edb65e69bee84776a45caa", - "version_binding_sha256": "a1f8b651123f1e95ef7e744ae45e9066967b6ceb0a710ddfd685e54b14b0b0a6", - "capture_record_sha256": "ac8ddc28fcca90a20e05f6bd0678d32550db451a8e3402e4c287154eab289b33", + "agy_version": "1.2.2", + "reviewed_source_revision": "ba985e6b5de2ac8aa09860a154a102831eb7722b", + "source_sha256": "cabadc15a61944372bede1fdff186701c17467dd9d718e97dc79283055d3c101", + "version_binding_sha256": "cc3bd8bb44b31891e2bed4c4367ed90ae2334e0df29e9fba2e94bce7c3105bef", + "capture_record_sha256": "73d65488b618b721d70d85076cc1ca67c8ead62e0606588502aeb69e5230d032", "capture_stdout_sha256": "d02970e6b6b4e0910461999afca8fb99d757e9094ab2874b557dad18fc75464a", "capture_response_sha256": "b1cc011310435afa07b1e132a5b7f3e22297aa21427177461c858bcbd6a58794", "inventory_normalized_sha256": "d5e58ab55e91ebd4a2cd23841c76cbe12b47d607c62cd8c834fc8f6b9f078ad7", @@ -75,8 +75,8 @@ "gemini-3.8-flash-high", "gemini-3.8-flash-low", "gemini-3.8-flash-medium", - "gpt-oss-120b-medium", - ], + "gpt-oss-120b-medium" + ] } diff --git a/tests/agy_worker_remediation_recovery_cases.py b/tests/agy_worker_remediation_recovery_cases.py index e0ff8c8..e832efc 100644 --- a/tests/agy_worker_remediation_recovery_cases.py +++ b/tests/agy_worker_remediation_recovery_cases.py @@ -568,6 +568,11 @@ def narrow_provider_launch_is_gitless_and_reconciles_exact_bytes_and_modes() -> assert containment["prepare"]["executable"] == containment["confirm"]["executable"] assert containment["prepare"]["cwd"] == containment["confirm"]["cwd"] assert containment["prepare"]["environment"] == containment["confirm"]["environment"] + assert containment["prepare"]["provider_max_cycles"] == 2 + assert containment["prepare"]["provider_write_selectors"] == [ + {"kind": "file", "path": "payload.bin"}, + {"kind": "file", "path": "tool.sh"}, + ] assert observed["argv"] == containment["confirm"]["argv"] assert observed["cwd"] == containment["confirm"]["cwd"] for key, value in containment["confirm"]["environment"].items(): diff --git a/tests/agy_worker_remediation_runtime_boundary_cases.py b/tests/agy_worker_remediation_runtime_boundary_cases.py index 5ba2311..6e6f6ce 100644 --- a/tests/agy_worker_remediation_runtime_boundary_cases.py +++ b/tests/agy_worker_remediation_runtime_boundary_cases.py @@ -52,7 +52,7 @@ def fixture( fake = bin_dir / "agy" fake.write_text( "#!/bin/sh\n" - "if [ \"${1:-}\" = --version ] && [ \"$#\" = 1 ]; then printf 'version\\n' >> " + shlex.quote(str(calls)) + "; printf '1.1.27\\n'; exit 0; fi\n" + "if [ \"${1:-}\" = --version ] && [ \"$#\" = 1 ]; then printf 'version\\n' >> " + shlex.quote(str(calls)) + "; printf '1.2.2\\n'; exit 0; fi\n" "if [ \"${1:-}\" = --help ] && [ \"$#\" = 1 ]; then printf 'help\\n' >> " + shlex.quote(str(calls)) + "; sleep \"${FAKE_DIRECT_HELP_DELAY:-0}\"; cat >&2 <<'HELP'\n" + help_text + "HELP\nexit 0\nfi\n" "printf 'provider\\n' >> " + shlex.quote(str(calls)) + "\nprintf '%s\\n' \"$@\" > " + shlex.quote(str(args)) + "\npwd > " + shlex.quote(str(cwd)) + "\n" + "if [ -n \"${FAKE_DIRECT_HEARTBEAT_COUNT:-}\" ]; then\n" @@ -85,7 +85,7 @@ def fixture( command = { "schema_version": 7, "kind": "agy-worker-dispatch-command", "job_id": f"direct-{label}", "workdir": str(repo), "argv": ["agy", "--sandbox", "--mode", "accept-edits", "--add-dir", str(repo), "--json-schema", str(schema), "--model", "gemini-3.6-flash-high", "--print", "task"], - "agy_version": "1.1.27", "agy_version_observed": True, + "agy_version": "1.2.2", "agy_version_observed": True, "selection_path": str(selection_path), "selection_sha256": MODULE.digest(raw), "selection_identity": list(MODULE._identity(info)), "idle_seconds": idle_seconds, "hard_seconds": hard_seconds, "max_seconds": max_seconds, "notice_seconds": 3, "stage_dir": None, "stage_file": None, "child_umask": "022", "workflow": workflow, @@ -129,6 +129,16 @@ def inspect_popen(arguments, *popen_args, **popen_kwargs): assert state["selection_sha256"] == command["selection_sha256"] assert state["selection_identity"] == command["selection_identity"] assert state["attempt_origin"] == origin + label = command["job_id"].removeprefix("direct-") + provider_arguments = (root / f"direct-reprobe-args-{label}").read_text( + encoding="utf-8", + ).splitlines() + if origin in {"conversation-resume", "conversation-continue"}: + assert provider_arguments.count("--conversation") == 1 + conversation_index = provider_arguments.index("--conversation") + assert provider_arguments[conversation_index + 1] == "conversation-1" + else: + assert "--conversation" not in provider_arguments # Initial, resume, and explicit restart each enter the same controller # path and must re-probe directly before their provider Popen. diff --git a/tests/test-agy-1-1-22-activation.py b/tests/test-agy-1-1-22-activation.py index 2d8fbe2..25b185a 100644 --- a/tests/test-agy-1-1-22-activation.py +++ b/tests/test-agy-1-1-22-activation.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -"""Focused offline tests for the active agy 1.1.27 compatibility binding.""" +"""Focused offline tests for the active agy 1.2.2 compatibility binding.""" from __future__ import annotations @@ -14,24 +14,24 @@ ROOT = Path(__file__).resolve().parent.parent RUNTIME = ROOT / "skills" / "agy-worker" / "runtime" MODULE_PATH = RUNTIME / "scripts" / "compatibility.py" -SPEC = importlib.util.spec_from_file_location("agy_1_1_27_compatibility", MODULE_PATH) +SPEC = importlib.util.spec_from_file_location("agy_1_2_2_compatibility", MODULE_PATH) if SPEC is None or SPEC.loader is None: raise SystemExit("cannot load compatibility module") compatibility = importlib.util.module_from_spec(SPEC) SPEC.loader.exec_module(compatibility) -VERSION = "1.1.27" -REVISION = "1ae9cb7b51667192c051b73a91099c71e816ca5f" -SOURCE_SHA256 = "d583be1344ea9cfa0c45cff2c1342af7837f4833c4edb65e69bee84776a45caa" -VERSION_BINDING_SHA256 = "a1f8b651123f1e95ef7e744ae45e9066967b6ceb0a710ddfd685e54b14b0b0a6" -CAPTURE_SHA256 = "ac8ddc28fcca90a20e05f6bd0678d32550db451a8e3402e4c287154eab289b33" +VERSION = "1.2.2" +REVISION = "ba985e6b5de2ac8aa09860a154a102831eb7722b" +SOURCE_SHA256 = "cabadc15a61944372bede1fdff186701c17467dd9d718e97dc79283055d3c101" +VERSION_BINDING_SHA256 = "cc3bd8bb44b31891e2bed4c4367ed90ae2334e0df29e9fba2e94bce7c3105bef" +CAPTURE_SHA256 = "73d65488b618b721d70d85076cc1ca67c8ead62e0606588502aeb69e5230d032" STDOUT_SHA256 = "d02970e6b6b4e0910461999afca8fb99d757e9094ab2874b557dad18fc75464a" RESPONSE_SHA256 = "b1cc011310435afa07b1e132a5b7f3e22297aa21427177461c858bcbd6a58794" NORMALIZED_SHA256 = "d5e58ab55e91ebd4a2cd23841c76cbe12b47d607c62cd8c834fc8f6b9f078ad7" -MATRIX_SHA256 = "56ee4cefdf918184e8bae57c49f01c51c18e49b30ff0bd4b322d8801703dfaac" -BINDING_SHA256 = "3a554f90922321700120e2c398f865a4894b2128087a4bac90968ddf2409762d" -OLD_VERSION = "1.1.16" -OLD_REVISION = "efa16f096dc02fb654b7e86958d268195284d014" +MATRIX_SHA256 = "2c12abf09910489b681a01c88b43e8fbaf7df3a68fd715c7280c7f6fff6c89e4" +BINDING_SHA256 = "efcc59a983b0c9f60309a55047188d800f5e34d2dc42a38d9339cdc361e13802" +OLD_VERSION = "1.1.27" +OLD_REVISION = "1ae9cb7b51667192c051b73a91099c71e816ca5f" TESTS: list[tuple[str, Callable[[], None]]] = [] @@ -86,7 +86,8 @@ def rejects(mutator: Callable[[dict[str, object]], None], *, digest: str | None def _() -> None: assert canonical("agy-verified-version.txt").read_text() == VERSION + "\n" assert canonical("agy-upstream-head.txt").read_text() == REVISION + "\n" - assert canonical("agy-last-reviewed.txt").read_text() == "2026-09-06\n" + reviewed = canonical("agy-last-reviewed.txt").read_text().strip() + assert len(reviewed) == 10 and reviewed[4] == "-" and reviewed[7] == "-" @test("portable active records are byte synchronized") @@ -107,26 +108,28 @@ def _() -> None: @test("active review is additive and prior observation remains historical") def _() -> None: - activation = canonical("reviews/agy-1.1.27-activation.md").read_text() + activation = canonical("reviews/agy-1.2.2-activation.md").read_text() observation = canonical("reviews/agy-1.1.22.md").read_text() - assert "AGY **1.1.27**" in activation - assert "Activation promotes 1.1.27 to current" in activation + assert "AGY **1.2.2**" in activation + assert "Activation promotes 1.2.2 to current" in activation + assert f"Reviewed: {canonical('agy-last-reviewed.txt').read_text().strip()}" in activation assert "does **not** activate 1.1.22" in observation @test("activation preserves the sanitized structured-capture boundary") def _() -> None: - activation = canonical("reviews/agy-1.1.27-activation.md").read_text() + activation = canonical("reviews/agy-1.2.2-activation.md").read_text() assert VERSION_BINDING_SHA256 in activation assert NORMALIZED_SHA256 in activation - assert "account inventory records bind" in activation + assert CAPTURE_SHA256 in activation @test("activation preserves denied-action and closed-binary residuals") def _() -> None: - activation = canonical("reviews/agy-1.1.27-activation.md").read_text() - assert "The denied-action payload shape remains unverified" in activation - assert "The closed-binary provider backend cannot be independently attested" in activation + activation = canonical("reviews/agy-1.2.2-activation.md").read_text() + assert "denied_actions" in activation + assert "status_unavailable" in activation + assert "binding_failure" in activation @test("matrix byte digest is exact") @@ -193,12 +196,12 @@ def _() -> None: assert value["slugs"] == compatibility.matrix_slugs(matrix()) -@test("historical 1.1.16 version binding is rejected") +@test("previous 1.1.27 version binding is rejected") def _() -> None: rejects(lambda value: value.__setitem__("agy_version", OLD_VERSION)) -@test("historical 1.1.16 source binding is rejected") +@test("previous 1.1.27 source binding is rejected") def _() -> None: rejects(lambda value: value.__setitem__("reviewed_source_revision", OLD_REVISION)) @@ -253,7 +256,7 @@ def _() -> None: rejects(lambda value: value["slugs"].__setitem__(0, "unknown-model")) # type: ignore[union-attr] -@test("coordinated historical version and source drift is rejected") +@test("coordinated previous version and source drift is rejected") def _() -> None: def mutate(value: dict[str, object]) -> None: value["agy_version"] = OLD_VERSION @@ -274,7 +277,7 @@ def main() -> None: else: passed += 1 print(f" ok activation: {name}") - print(f"AGY_1_1_26_ACTIVATION_TEST_RESULT passed={passed} failed={failed}") + print(f"AGY_1_2_2_ACTIVATION_TEST_RESULT passed={passed} failed={failed}") raise SystemExit(1 if failed else 0) diff --git a/tests/test-agy-worker-remediation.py b/tests/test-agy-worker-remediation.py index 371ed65..eb0ad93 100644 --- a/tests/test-agy-worker-remediation.py +++ b/tests/test-agy-worker-remediation.py @@ -33,7 +33,7 @@ assert spec.loader is not None spec.loader.exec_module(MODULE) -EXPECTED_CHECKS = 111 +EXPECTED_CHECKS = 112 CHECKS_RUN = 0 FOCUSED_CHECK = os.environ.get("AGY_WORKER_REMEDIATION_FOCUSED_CHECK") # This test-only switch exercises portable controller mechanics on macOS when @@ -41,9 +41,9 @@ PORTABLE_SCOPED_FIXTURE = os.environ.get( "AGY_WORKER_REMEDIATION_PORTABLE_FIXTURE", ) == "1" -# The prior partition labels were transposed: the source contained 61 core and -# 41 recovery calls. The manifest/grant and nested-deletion cases raise core to 63. -GROUP_CHECKS = {"core": 64, "runtime": 1, "recovery": 46} +# The prior partition labels were transposed; keep these explicit inventories +# synchronized with the canonical grouped and ungrouped suite runs. +GROUP_CHECKS = {"core": 65, "runtime": 1, "recovery": 46} def selected_group(arguments: list[str]) -> str | None: @@ -195,7 +195,7 @@ def _record(self): def prepare_contained_launch( self, *, role, network_policy, job_dir, attempt, stage_dir, target_executable, target_argv, child_environment, allow_keychain, - read_only_inputs, + read_only_inputs, provider_max_cycles, provider_write_selectors, ): if role != self.ROLE_PROVIDER or network_policy != self.NETWORK_PROVIDER_TLS: raise self.ContainmentError("test containment policy is invalid") @@ -203,7 +203,12 @@ def prepare_contained_launch( raise self.ContainmentError("test containment path is invalid") if not target_argv or target_argv[0] != str(target_executable): raise self.ContainmentError("test containment target is invalid") - if not allow_keychain or len(read_only_inputs) != 1: + if ( + not allow_keychain or len(read_only_inputs) != 1 + or type(provider_max_cycles) is not int or not (attempt <= provider_max_cycles <= 5) + or not isinstance(provider_write_selectors, list) + or any(set(item) != {"kind", "path"} for item in provider_write_selectors) + ): raise self.ContainmentError("test containment inputs are invalid") home = self.job / "provider-home" attempt_tmp = self.job / f"provider-tmp-{attempt:03d}" @@ -230,6 +235,8 @@ def prepare_contained_launch( "executable": prepared.target_executable, "cwd": prepared.stage_dir, "environment": dict(sorted(prepared.environment.items())), + "provider_max_cycles": provider_max_cycles, + "provider_write_selectors": provider_write_selectors, } self._record() return prepared @@ -2266,25 +2273,40 @@ def controller_preserves_outer_error_candidate() -> None: check("controller maps ERROR plus valid report to failed unreviewed exit 25", controller_preserves_outer_error_candidate) - def exact_1_1_27_denial_signal_preserves_candidate_without_reuse() -> None: + def reviewed_denial_signal_preserves_candidate_without_reuse() -> None: cases = [ - ("denial", "1.1.27", True, "failed", "permission_required", 6), - ("ordinary", "1.1.27", False, "succeeded", None, 0), - ("prior-version", "1.1.26", True, "succeeded", None, 0), + ("denial", "1.1.27", True, True, "failed", "permission_required", 6), + ("ordinary", "1.1.27", False, True, "succeeded", None, 0), + ("denial-1-2-2", "1.2.2", True, True, "failed", "permission_required", 6), + ("ordinary-1-2-2", "1.2.2", False, True, "succeeded", None, 0), + ("unreviewed-intermediate", "1.2.1", True, True, "succeeded", None, 0), + ("prior-version", "1.1.26", True, True, "succeeded", None, 0), + # The live 1.2.2 denial emitted this top-level key but no structured + # report. Presence cannot manufacture a candidate or supersede the + # envelope validator's missing-structured-output classification. + ("live-invalid-1-2-2", "1.2.2", True, False, "failed", "invalid_envelope", 4), ] - for label, version, include_denial, expected_status, expected_reason, expected_exit in cases: + for ( + label, version, include_denial, valid_candidate, + expected_status, expected_reason, expected_exit, + ) in cases: repo = root / f"denied-actions-{label}-repo"; repo.mkdir() subprocess.run(["git", "init", "-q", str(repo)], check=True) job = root / f"denied-actions-{label}-job"; job.mkdir(mode=0o700) bin_dir = root / f"denied-actions-{label}-bin"; bin_dir.mkdir() terminal = { "conversation_id": "conversation-1", "status": "SUCCESS", - "structured_output": report(summary=f"denied-actions-{label}"), + "structured_output": ( + report(summary=f"denied-actions-{label}") if valid_candidate else None + ), } if include_denial: # Presence only: the provider's undocumented payload shape is # never parsed or persisted by the controller. - terminal["denied_actions"] = None + terminal["denied_actions"] = ( + [{"action": "command", "display_name": "RunCommand"}] + if version == "1.2.2" else None + ) events = [ {"event": "init", "init": {}, "conversation_id": "conversation-1"}, {"event": "result", "result": terminal}, @@ -2306,23 +2328,344 @@ def exact_1_1_27_denial_signal_preserves_candidate_without_reuse() -> None: } MODULE.write_atomic(job, MODULE.COMMAND_NAME, command) MODULE.create_state(job, "initial", resume=False) - assert run_controller(job, bin_dir) == expected_exit + actual_exit = run_controller(job, bin_dir) + assert actual_exit == expected_exit, (label, actual_exit, expected_exit) state, _raw, sha = MODULE.load_state(job) assert (state["status"], state["reason"], state["exit_code"]) == ( expected_status, expected_reason, expected_exit, ) - assert state["candidate_recognized"] and state["result_available"] - assert state["candidate_source"] == "provider_success" + assert state["candidate_recognized"] is valid_candidate + assert state["result_available"] is valid_candidate + assert state["candidate_source"] == ( + "provider_success" if valid_candidate else "none" + ) assert state["provider_terminal_status"] == "success" - assert state["failure_stage"] is None - if include_denial and version == "1.1.27": + assert state["failure_stage"] == ( + None if valid_candidate else "missing_structured_output" + ) + if valid_candidate and include_denial and version in {"1.1.27", "1.2.2"}: assert not state["resume_available"] and not state["continue_available"] actions = {item["action"] for item in MODULE.public_status(state, sha, job=job)["available_actions"]} assert "resume" not in actions and "continue" not in actions check( - "exact 1.1.27 denied_actions presence blocks provider reuse but preserves a valid SUCCESS candidate", - exact_1_1_27_denial_signal_preserves_candidate_without_reuse, + "reviewed exact-version denied_actions presence blocks provider reuse but preserves a valid SUCCESS candidate", + reviewed_denial_signal_preserves_candidate_without_reuse, + ) + + def exact_1_2_2_partial_timeout_signal_preserves_candidate() -> None: + assert MODULE._reviewed_provider_timeout_lines("1.2.2", 8) == { + b"[agy] print timeout after 8s with turn in progress; returning partial output", + } + assert MODULE._reviewed_provider_timeout_lines("1.2.2", 7200) == { + b"[agy] print timeout after 7200s with turn in progress; returning partial output", + b"[agy] print timeout after 2h0m0s with turn in progress; returning partial output", + } + assert MODULE._reviewed_provider_timeout_lines("1.2.1", 20) == set() + timeout_line = next(iter(MODULE._reviewed_provider_timeout_lines("1.2.2", 20))) + + cases = [ + ("exact", "1.2.2", True, False, timeout_line, report(summary="partial"), "provider_timeout", 17, True), + ("timeout-and-denial", "1.2.2", True, True, timeout_line, report(summary="denied-partial"), "permission_required", 6, True), + ("near-miss", "1.2.2", True, False, timeout_line + b".", report(summary="complete"), None, 0, True), + ("unreviewed-version", "1.2.1", True, False, timeout_line, report(summary="complete"), None, 0, True), + ("unobserved-version", "1.2.2", False, False, timeout_line, report(summary="complete"), None, 0, True), + ("invalid-envelope", "1.2.2", True, False, timeout_line, None, "invalid_envelope", 4, False), + ] + for ( + label, version, version_observed, include_denial, stderr_line, candidate, + expected_reason, expected_exit, expected_candidate, + ) in cases: + repo = root / f"partial-timeout-{label}-repo"; repo.mkdir() + subprocess.run(["git", "init", "-q", str(repo)], check=True) + job = root / f"partial-timeout-{label}-job"; job.mkdir(mode=0o700) + bin_dir = root / f"partial-timeout-{label}-bin"; bin_dir.mkdir() + terminal = { + "conversation_id": "conversation-1", "status": "SUCCESS", + "structured_output": candidate, + } + if include_denial: + terminal["denied_actions"] = [ + {"action": "command", "display_name": "RunCommand"}, + ] + events = [ + {"event": "init", "init": {}, "conversation_id": "conversation-1"}, + {"event": "result", "result": terminal}, + ] + fake = bin_dir / "agy" + payload = b"".join( + json.dumps(item).encode("utf-8") + b"\n" for item in events + ) + fake.write_text( + "#!/usr/bin/env python3\n" + "import os\n" + f"os.write(1, {payload!r})\n" + f"os.write(2, {(stderr_line + bytes([10]))!r})\n", + encoding="utf-8", + ); fake.chmod(0o755) + bound_provider = root / f"partial-timeout-{label}-provider.json" + provider_schema(bound_provider) + command = { + "schema_version": 3, "kind": "agy-worker-dispatch-command", "job_id": f"partial-timeout-{label}", + "workdir": str(repo), "argv": ["agy", "--json-schema", str(bound_provider), "--print", "task"], + "agy_version": version, "agy_version_observed": version_observed, + "idle_seconds": 2, "hard_seconds": 3, "max_seconds": 20, "notice_seconds": 3, + "stage_dir": None, "stage_file": None, "child_umask": "022", "workflow": "task", + "max_cycles": 2, "resume_prompt": "resume", "continue_prompt": "continue", + } + MODULE.write_atomic(job, MODULE.COMMAND_NAME, command) + MODULE.create_state(job, "initial", resume=False) + actual_exit = run_controller(job, bin_dir) + state, _raw, state_sha = MODULE.load_state(job) + assert actual_exit == expected_exit, ( + label, actual_exit, expected_exit, state["reason"], state["failure_stage"], + (job / "stream.ndjson").read_bytes(), + ) + assert (state["status"], state["reason"], state["exit_code"]) == ( + "succeeded" if expected_reason is None else "failed", + expected_reason, + expected_exit, + ) + assert state["candidate_recognized"] is expected_candidate + assert state["result_available"] is expected_candidate + assert state["provider_terminal_status"] == "success" + assert state["driver_disposition"] == ( + "unreviewed" if expected_candidate else "not_applicable" + ) + if expected_reason == "provider_timeout": + assert state["candidate_source"] == "provider_success" + assert state["continue_available"] + public = MODULE.public_status(state, state_sha, job=job) + actions = {item["action"] for item in public["available_actions"]} + assert public["result_available"] and "result" in actions + assert public["continue_available"] and "continue" in actions + elif expected_reason == "permission_required": + assert state["candidate_source"] == "provider_success" + assert not state["resume_available"] and not state["continue_available"] + + # A leader can publish the timeout result and exit while one of its + # descendants still owns the stream pipes. The controller must keep + # its process-group deadline, reap that descendant, and then retain the + # already-bound partial candidate under the provider timeout signal. + repo = root / "partial-timeout-descendant-repo"; repo.mkdir() + subprocess.run(["git", "init", "-q", str(repo)], check=True) + job = root / "partial-timeout-descendant-job"; job.mkdir(mode=0o700) + bin_dir = root / "partial-timeout-descendant-bin"; bin_dir.mkdir() + child_record = root / "partial-timeout-descendant-child" + events = [ + {"event": "init", "init": {}, "conversation_id": "conversation-1"}, + {"event": "result", "result": { + "conversation_id": "conversation-1", "status": "SUCCESS", + "structured_output": report(summary="partial-with-live-descendant"), + }}, + ] + payload = b"".join( + json.dumps(item).encode("utf-8") + b"\n" for item in events + ) + fake = bin_dir / "agy" + fake.write_text( + "#!/usr/bin/env python3\n" + "import os, signal, time\n" + f"record = {str(child_record)!r}\n" + "child = os.fork()\n" + "if child == 0:\n" + " signal.signal(signal.SIGTERM, signal.SIG_IGN)\n" + " while True: time.sleep(60)\n" + "with open(record, 'w', encoding='ascii') as handle:\n" + " handle.write(f'{child} {os.getpgrp()}\\n')\n" + f"os.write(1, {payload!r})\n" + f"os.write(2, {(timeout_line + bytes([10]))!r})\n" + "os._exit(0)\n", + encoding="utf-8", + ); fake.chmod(0o755) + bound_provider = root / "partial-timeout-descendant-provider.json" + provider_schema(bound_provider) + command = { + "schema_version": 3, "kind": "agy-worker-dispatch-command", "job_id": "partial-timeout-descendant", + "workdir": str(repo), "argv": ["agy", "--json-schema", str(bound_provider), "--print", "task"], + "agy_version": "1.2.2", "agy_version_observed": True, + "idle_seconds": 1, "hard_seconds": 4, "max_seconds": 20, "notice_seconds": 3, + "stage_dir": None, "stage_file": None, "child_umask": "022", "workflow": "task", + "max_cycles": 2, "resume_prompt": "resume", "continue_prompt": "continue", + } + MODULE.write_atomic(job, MODULE.COMMAND_NAME, command) + MODULE.create_state(job, "initial", resume=False) + actual_exit = run_controller(job, bin_dir) + state, _raw, _sha = MODULE.load_state(job) + assert actual_exit == MODULE.EXIT_BY_REASON["provider_timeout"], ( + actual_exit, state["reason"], state["failure_stage"], state["agy_returncode"], + (job / "stream.ndjson").read_bytes(), (job / "stderr.txt").read_bytes(), + ) + assert (state["status"], state["reason"], state["limit_kind"]) == ( + "failed", "provider_timeout", None, + ) + assert state["candidate_recognized"] and state["result_available"] + assert state["provider_terminal_status"] == "success" + child, group = map(int, child_record.read_text(encoding="ascii").split()) + assert child != group + try: + os.killpg(group, 0) + except ProcessLookupError: + pass + else: + raise AssertionError("partial-timeout descendant process group survived") + + # Reaping a descendant-held pipe is not itself permission to recover a + # report after idle timeout. The leader must have exited cleanly and + # the exact reviewed timeout marker must be present. A missing marker + # and a leader still running both retain the ordinary idle-timeout path. + for label, exited_leader in ( + ("idle-exited-no-marker", True), + ("idle-live-no-marker", False), + ): + repo = root / f"{label}-repo"; repo.mkdir() + subprocess.run(["git", "init", "-q", str(repo)], check=True) + job = root / f"{label}-job"; job.mkdir(mode=0o700) + bin_dir = root / f"{label}-bin"; bin_dir.mkdir() + child_record = root / f"{label}-child" + fake = bin_dir / "agy" + if exited_leader: + program = ( + "#!/usr/bin/env python3\n" + "import os, signal, time\n" + f"record = {str(child_record)!r}\n" + "child = os.fork()\n" + "if child == 0:\n" + " signal.signal(signal.SIGTERM, signal.SIG_IGN)\n" + " while True: time.sleep(60)\n" + "with open(record, 'w', encoding='ascii') as handle:\n" + " handle.write(f'{child} {os.getpgrp()}\\n')\n" + f"os.write(1, {payload!r})\n" + "os._exit(0)\n" + ) + else: + program = ( + "#!/usr/bin/env python3\n" + "import os, time\n" + f"os.write(1, {payload!r})\n" + "time.sleep(60)\n" + ) + fake.write_text(program, encoding="utf-8"); fake.chmod(0o755) + bound_provider = root / f"{label}-provider.json" + provider_schema(bound_provider) + command = { + "schema_version": 3, "kind": "agy-worker-dispatch-command", "job_id": label, + "workdir": str(repo), "argv": ["agy", "--json-schema", str(bound_provider), "--print", "task"], + "agy_version": "1.2.2", "agy_version_observed": True, + "idle_seconds": 1, "hard_seconds": 4, "max_seconds": 20, "notice_seconds": 3, + "stage_dir": None, "stage_file": None, "child_umask": "022", "workflow": "task", + "max_cycles": 2, "resume_prompt": "resume", "continue_prompt": "continue", + } + MODULE.write_atomic(job, MODULE.COMMAND_NAME, command) + MODULE.create_state(job, "initial", resume=False) + assert run_controller(job, bin_dir) == MODULE.EXIT_BY_REASON["idle_timeout"] + state, _raw, _sha = MODULE.load_state(job) + assert (state["status"], state["reason"], state["limit_kind"]) == ( + "failed", "idle_timeout", "idle", + ) + assert not state["candidate_recognized"] and not state["result_available"] + assert state["candidate_source"] == "none" + assert state["provider_terminal_status"] == "unknown" + assert state["agy_returncode"] == (0 if exited_leader else -signal.SIGTERM) + if exited_leader: + child, group = map(int, child_record.read_text(encoding="ascii").split()) + assert child != group + try: + os.killpg(group, 0) + except ProcessLookupError: + pass + else: + raise AssertionError("unmarked idle descendant process group survived") + + # Marker-driven recovery after an idle timeout still obeys denial + # precedence and cannot make the retained conversation reusable. + repo = root / "partial-timeout-denied-descendant-repo"; repo.mkdir() + subprocess.run(["git", "init", "-q", str(repo)], check=True) + job = root / "partial-timeout-denied-descendant-job"; job.mkdir(mode=0o700) + bin_dir = root / "partial-timeout-denied-descendant-bin"; bin_dir.mkdir() + denied_events = [ + {"event": "init", "init": {}, "conversation_id": "conversation-1"}, + {"event": "result", "result": { + "conversation_id": "conversation-1", "status": "SUCCESS", + "structured_output": report(summary="denied-partial-with-live-descendant"), + "denied_actions": [{"action": "command", "display_name": "RunCommand"}], + }}, + ] + denied_payload = b"".join( + json.dumps(item).encode("utf-8") + b"\n" for item in denied_events + ) + fake = bin_dir / "agy" + fake.write_text( + "#!/usr/bin/env python3\n" + "import os, signal, time\n" + "child = os.fork()\n" + "if child == 0:\n" + " signal.signal(signal.SIGTERM, signal.SIG_IGN)\n" + " while True: time.sleep(60)\n" + f"os.write(1, {denied_payload!r})\n" + f"os.write(2, {(timeout_line + bytes([10]))!r})\n" + "os._exit(0)\n", + encoding="utf-8", + ); fake.chmod(0o755) + bound_provider = root / "partial-timeout-denied-descendant-provider.json" + provider_schema(bound_provider) + command = { + "schema_version": 3, "kind": "agy-worker-dispatch-command", + "job_id": "partial-timeout-denied-descendant", "workdir": str(repo), + "argv": ["agy", "--json-schema", str(bound_provider), "--print", "task"], + "agy_version": "1.2.2", "agy_version_observed": True, + "idle_seconds": 1, "hard_seconds": 4, "max_seconds": 20, "notice_seconds": 3, + "stage_dir": None, "stage_file": None, "child_umask": "022", "workflow": "task", + "max_cycles": 2, "resume_prompt": "resume", "continue_prompt": "continue", + } + MODULE.write_atomic(job, MODULE.COMMAND_NAME, command) + MODULE.create_state(job, "initial", resume=False) + assert run_controller(job, bin_dir) == MODULE.EXIT_BY_REASON["permission_required"] + state, _raw, _sha = MODULE.load_state(job) + assert (state["status"], state["reason"], state["limit_kind"]) == ( + "failed", "permission_required", None, + ) + assert state["candidate_recognized"] and state["result_available"] + assert state["candidate_source"] == "provider_success" + assert not state["resume_available"] and not state["continue_available"] + + # A provider marker cannot weaken a controller-owned hard boundary. + repo = root / "partial-timeout-hard-repo"; repo.mkdir() + subprocess.run(["git", "init", "-q", str(repo)], check=True) + job = root / "partial-timeout-hard-job"; job.mkdir(mode=0o700) + bin_dir = root / "partial-timeout-hard-bin"; bin_dir.mkdir() + fake = bin_dir / "agy" + fake.write_text( + "#!/usr/bin/env python3\n" + "import os, time\n" + f"os.write(1, {payload!r})\n" + f"os.write(2, {(timeout_line + bytes([10]))!r})\n" + "time.sleep(60)\n", + encoding="utf-8", + ); fake.chmod(0o755) + bound_provider = root / "partial-timeout-hard-provider.json" + provider_schema(bound_provider) + command = { + "schema_version": 3, "kind": "agy-worker-dispatch-command", "job_id": "partial-timeout-hard", + "workdir": str(repo), "argv": ["agy", "--json-schema", str(bound_provider), "--print", "task"], + "agy_version": "1.2.2", "agy_version_observed": True, + "idle_seconds": 1, "hard_seconds": 1, "max_seconds": 20, "notice_seconds": 3, + "stage_dir": None, "stage_file": None, "child_umask": "022", "workflow": "task", + "max_cycles": 2, "resume_prompt": "resume", "continue_prompt": "continue", + } + MODULE.write_atomic(job, MODULE.COMMAND_NAME, command) + MODULE.create_state(job, "initial", resume=False) + assert run_controller(job, bin_dir) == MODULE.EXIT_BY_REASON["hard_deadline_exceeded"] + state, _raw, _sha = MODULE.load_state(job) + assert (state["status"], state["reason"], state["limit_kind"]) == ( + "failed", "hard_deadline_exceeded", "hard", + ) + assert state["candidate_recognized"] and state["result_available"] + assert state["provider_terminal_status"] == "success" + + check( + "exact reviewed 1.2.2 print-timeout stderr marks provider failure while preserving a valid partial candidate", + exact_1_2_2_partial_timeout_signal_preserves_candidate, ) def invalid_error_and_cancelled_candidate_are_separate() -> None: diff --git a/tests/test-agy-worker.sh b/tests/test-agy-worker.sh index 3541e7e..a6fa023 100755 --- a/tests/test-agy-worker.sh +++ b/tests/test-agy-worker.sh @@ -124,7 +124,7 @@ assert value["user_model"] == model assert value.get("user_effort", "") == effort assert value["resolved_agy_model"] == resolved assert len(value["matrix_sha256"]) == 64 -assert value["matrix_agy_version"] == "1.1.27" +assert value["matrix_agy_version"] == "1.2.2" assert len(value["matrix_source_revision"]) == 40 assert value["recommendation_only"] is True assert value["applied"] is False @@ -792,14 +792,14 @@ fi if [[ "${1:-}" == "--version" && $# -eq 1 ]]; then printf 'version\n' >> "$FAKE_CALLS_FILE" case "${FAKE_VERSION_MODE:-ready}" in - ready) printf '1.1.27\n' ;; + ready) printf '1.2.2\n' ;; quota113) printf '1.1.13\n' ;; - prefixed) printf 'agy 1.1.27\n' ;; + prefixed) printf 'agy 1.2.2\n' ;; drift) printf '1.1.11\n' ;; drift117) printf '1.1.17\n' ;; drift999) printf '9.9.9\n' ;; empty) : ;; - malformed) printf 'version 1.1.27\n' ;; + malformed) printf 'version 1.2.2\n' ;; oversize) i=0; while [[ $i -lt 140 ]]; do printf x; i=$((i+1)); done; printf '\n' ;; stream) while :; do printf 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'; done ;; child-stream) @@ -2320,8 +2320,8 @@ assert record.get("user_effort", "") == user_effort assert record["user_model_source"] == model_source assert record.get("user_effort_source", "") == effort_source assert record["resolved_agy_model"] == expected -assert record["installed_agy_version"] == "1.1.27" -assert record["matrix_agy_version"] == "1.1.27" +assert record["installed_agy_version"] == "1.2.2" +assert record["matrix_agy_version"] == "1.2.2" assert record["version_relation"] == "match" assert record["critical_interface_probe_version"] == 1 assert record["critical_interface_status"] == "compatible" @@ -2929,7 +2929,7 @@ assert value == { "schema_version": 1, "kind": "agy-worker-compatibility-review-evidence", "installed_agy_version": "1.1.17", - "matrix_agy_version": "1.1.27", + "matrix_agy_version": "1.2.2", "version_relation": "drift", "compatibility_status": "direct-selection-review-required", "critical_interface_status": "compatible", diff --git a/tests/test-doctor.sh b/tests/test-doctor.sh index f588c76..987176e 100755 --- a/tests/test-doctor.sh +++ b/tests/test-doctor.sh @@ -47,7 +47,7 @@ make_fixture() { "$destination/tmp" cp -R "$ROOT/skills/agy-worker/runtime" "$destination/runtime" rm -rf "$destination/runtime/__pycache__" "$destination/runtime/scripts/__pycache__" - printf '1.1.27\n' > "$destination/runtime/compat/agy-verified-version.txt" + printf '1.2.2\n' > "$destination/runtime/compat/agy-verified-version.txt" "$HOST_PYTHON" -B -c 'from datetime import date; print(date.today().isoformat())' \ > "$destination/runtime/compat/agy-last-reviewed.txt" printf 'CONFIG_SECRET_DO_NOT_READ\n' > "$destination/home/.codex/config.toml" @@ -61,8 +61,8 @@ make_fixture() { '[[ "${FAKE_PYTHON_MODE:-ready}" == "fail" ]] && exit 7' \ 'if [[ "${2:-}" == *doctor-metadata.py && "${3:-}" == "capture-agy-version" ]]; then' \ ' case "${FAKE_PYTHON_CAPTURE_MODE:-real}" in' \ - ' no-newline) printf "1.1.27"; exit 0 ;;' \ - ' multiline) printf "1.1.27\\nextra\\n"; exit 0 ;;' \ + ' no-newline) printf "1.2.2"; exit 0 ;;' \ + ' multiline) printf "1.2.2\\nextra\\n"; exit 0 ;;' \ ' short-write) exec 1>&-; exit 0 ;;' \ ' disk-full) exit 74 ;;' \ ' esac' \ @@ -94,14 +94,14 @@ make_fixture() { 'printf "%s\\n" "$*" >> "$DOCTOR_AGY_CALLS"' \ '"$DOCTOR_TEST_PYTHON" -B -c '"'"'import os,stat,sys; p=sys.argv[1]; c=os.path.join(p,"agy-version"); open(sys.argv[2],"a").write(f"agy:{p}:{stat.S_IMODE(os.stat(p).st_mode):03o}:{stat.S_IMODE(os.stat(c).st_mode):03o}\\n")'"'"' "$TMPDIR" "${DOCTOR_TMP_OBSERVATIONS:-/dev/null}"' \ 'case "${FAKE_AGY_MODE:-ready}" in' \ - ' ready) printf "agy 1.1.27\\n" ;;' \ - ' bare) printf "1.1.27\\n" ;;' \ - ' no-newline) printf "agy 1.1.27" ;;' \ - ' two-newlines) printf "agy 1.1.27\\n\\n" ;;' \ - ' carriage-return) printf "agy 1.1.27\\r\\n" ;;' \ - ' control) printf "agy 1.1.27\\t" ;;' \ - ' nul) printf "agy 1.1.27\\0" ;;' \ - ' prefix-junk) printf "version: agy 1.1.27\\n" ;;' \ + ' ready) printf "agy 1.2.2\\n" ;;' \ + ' bare) printf "1.2.2\\n" ;;' \ + ' no-newline) printf "agy 1.2.2" ;;' \ + ' two-newlines) printf "agy 1.2.2\\n\\n" ;;' \ + ' carriage-return) printf "agy 1.2.2\\r\\n" ;;' \ + ' control) printf "agy 1.2.2\\t" ;;' \ + ' nul) printf "agy 1.2.2\\0" ;;' \ + ' prefix-junk) printf "version: agy 1.2.2\\n" ;;' \ ' oversize) printf "agy "; i=0; while [[ $i -lt 140 ]]; do printf "1"; i=$((i+1)); done ;;' \ ' huge) printf "agy "; i=0; while [[ $i -lt 4096 ]]; do printf "1"; i=$((i+1)); done ;;' \ ' signal) kill -TERM "$PPID"; exit 7 ;;' \ @@ -116,7 +116,7 @@ make_fixture() { ' drift) printf "agy 1.1.11\\n" ;;' \ ' empty) : ;;' \ ' usage) printf "usage: agy [options]\\n" ;;' \ - ' multiline) printf "agy 1.1.27\\nextra\\n" ;;' \ + ' multiline) printf "agy 1.2.2\\nextra\\n" ;;' \ ' fail) exit 7 ;;' \ 'esac' > "$destination/bin/agy" printf '%s\n' '#!/usr/bin/env bash' \ diff --git a/tests/test-evidence-receipt.sh b/tests/test-evidence-receipt.sh index f9da378..47639de 100755 --- a/tests/test-evidence-receipt.sh +++ b/tests/test-evidence-receipt.sh @@ -674,7 +674,7 @@ cat > "$TMP/selection-bin/agy" <<'SH' #!/usr/bin/env bash case "$*" in --version) - printf '1.1.27\n' + printf '1.2.2\n' ;; --help) printf '%s\n' \ diff --git a/tests/test-model-evidence-campaign.py b/tests/test-model-evidence-campaign.py index d106fc9..d7f1832 100644 --- a/tests/test-model-evidence-campaign.py +++ b/tests/test-model-evidence-campaign.py @@ -160,7 +160,7 @@ def make_valid_record( "measured_metadata": ({ "provenance_type": "local", "source_uri": "local://campaign/synthetic", - "agy_version": "1.1.27", + "agy_version": "1.2.2", "effort": "high", "accounting": "observed_actual", "tokenizer": "cl100k_base", diff --git a/tests/test-official-distribution.py b/tests/test-official-distribution.py index 5a812c8..2290dd7 100644 --- a/tests/test-official-distribution.py +++ b/tests/test-official-distribution.py @@ -29,11 +29,11 @@ distribution = importlib.util.module_from_spec(SPEC) SPEC.loader.exec_module(distribution) -VERSION = "1.1.27" -SHA512 = "cd627f798e059f84a88bfe21dbde54cc5262e2d93d1943f120eb4b386823f4a1f7c4a57d4d0f876557709b6061a4c02f1fc13157f0047c5df6f300afa471e411" +VERSION = "1.2.2" +SHA512 = "8a3b5edea51e107a74413cea5eed1c5b02dede945ba21c7625b7c86f477c2c8ac423581de0ed84ff2f97c3bf6818c1fc24ca84cf9a76c2fb02a50e89d8a0d29a" ARCHIVE_URL = ( "https://storage.googleapis.com/antigravity-public/antigravity-cli/" - "1.1.27-5211191891591168/darwin-arm/cli_mac_arm64.tar.gz" + "1.2.2-6061403484848128/darwin-arm/cli_mac_arm64.tar.gz" ) @@ -526,7 +526,7 @@ def _() -> None: def _() -> None: expect_error( "invalid archive policy", - lambda: parse(url=replace_url("1.1.27-", "1.1.9-")), + lambda: parse(url=replace_url("1.2.2-", "1.1.9-")), ) @@ -534,7 +534,7 @@ def _() -> None: def _() -> None: expect_error( "invalid archive policy", - lambda: parse(url=replace_url("5211191891591168", "build-secret")), + lambda: parse(url=replace_url("6061403484848128", "build-secret")), ) @@ -564,13 +564,13 @@ def _() -> None: def _() -> None: value = parse() status, detail = distribution.evaluate_manifest(value, "1.1.9", value) - assert status == 3 and "1.1.27" in detail and "1.1.9" in detail + assert status == 3 and "1.2.2" in detail and "1.1.9" in detail @test("same-version archive build drift is drift-review") def _() -> None: observed = parse() - snapshot = parse(url=replace_url("5211191891591168", "6607970839166975")) + snapshot = parse(url=replace_url("6061403484848128", "6607970839166975")) assert distribution.evaluate_manifest(observed, VERSION, snapshot)[0] == 3 @@ -600,7 +600,7 @@ def _() -> None: assert [call[0].full_url for call in opener.calls] == [distribution.MANIFEST_URL] -@test("observed 1.1.27 against a historical 1.1.16 snapshot is drift-review") +@test("observed 1.2.2 against a historical 1.1.16 snapshot is drift-review") def _() -> None: with tempfile.TemporaryDirectory() as temporary: root = Path(temporary) @@ -613,7 +613,7 @@ def _() -> None: ) opener = FakeOpener(FakeResponse(manifest_bytes())) status, detail = distribution.check_production_manifest(root=root, opener=opener) - assert status == 3 and "1.1.27" in detail and "1.1.16" in detail + assert status == 3 and "1.2.2" in detail and "1.1.16" in detail assert [call[0].full_url for call in opener.calls] == [distribution.MANIFEST_URL] diff --git a/tests/test-packaging.sh b/tests/test-packaging.sh index 78015a5..804245e 100755 --- a/tests/test-packaging.sh +++ b/tests/test-packaging.sh @@ -1374,7 +1374,7 @@ security_reference = package_root / "references/SECURITY_AND_COMPATIBILITY.md" lifecycle_reference = package_root / "references/PROJECT_LIFECYCLE_AND_VERIFICATION.md" troubleshooting_reference = package_root / "references/TROUBLESHOOTING.md" assert manifest["name"] == "codex-agy-worker" -assert manifest["version"] == "0.18.0" +assert manifest["version"] == "0.19.0" assert manifest["skills"] == "./skills/" assert manifest["license"] == "MIT" assert manifest["interface"]["privacyPolicyURL"].startswith("https://") @@ -2439,8 +2439,10 @@ if cmp -s "$ROOT/compat/agy-verified-version.txt" \ "$ROOT/skills/agy-worker/runtime/compat/agy-models-inventory-binding.json" \ && cmp -s "$ROOT/compat/agy-models-inventory-binding.sha256" \ "$ROOT/skills/agy-worker/runtime/compat/agy-models-inventory-binding.sha256" \ - && [[ "$(<"$ROOT/compat/agy-verified-version.txt")" == "1.1.27" ]] \ - && [[ "$(<"$ROOT/compat/agy-last-reviewed.txt")" == "2026-09-06" ]]; then + && [[ "$(<"$ROOT/compat/agy-verified-version.txt")" == "1.2.2" ]] \ + && grep -Eq '^[0-9]{4}-[0-9]{2}-[0-9]{2}$' "$ROOT/compat/agy-last-reviewed.txt" \ + && grep -Fxq "Reviewed: $(<"$ROOT/compat/agy-last-reviewed.txt")" \ + "$ROOT/compat/reviews/agy-1.2.2-activation.md"; then ok "portable doctor metadata is byte-synchronized with canonical compatibility records" else bad "portable doctor metadata is byte-synchronized with canonical compatibility records" @@ -2871,7 +2873,7 @@ fi mkdir -p "$TMP/selector-bin" printf '%s\n' '#!/usr/bin/env bash' \ 'case "$*" in' \ - ' --version) printf "1.1.27\n" ;;' \ + ' --version) printf "1.2.2\n" ;;' \ ' --help) printf "%s\n" "Usage of agy:" " --add-dir Add a directory" " --conversation Resume a conversation" " --disable-slash-commands Disable slash commands" " --json-schema Schema path" " --mode Execution mode (accept-edits, plan)" " --model Select a model" " --output-format Format (text, json, stream-json)" " --print Run a prompt" " --print-timeout Print timeout" " --sandbox Sandboxed" >&2 ;;' \ ' *) exit 97 ;;' \ 'esac' > "$TMP/selector-bin/agy" @@ -2895,7 +2897,7 @@ fi if [[ "$rc" == 0 ]] \ && grep -Fq '"resolved_agy_model": "gemini-3.6-flash-high"' \ "$TMP/copied-selection.json" \ - && grep -Fq '"matrix_sha256": "56ee4cefdf918184e8bae57c49f01c51c18e49b30ff0bd4b322d8801703dfaac"' \ + && grep -Fq '"matrix_sha256": "2c12abf09910489b681a01c88b43e8fbaf7df3a68fd715c7280c7f6fff6c89e4"' \ "$TMP/copied-selection.json" \ && [[ "$copied_selection_v2" == 1 ]] \ && [[ ! -e "$TMP/network-called" ]]; then @@ -3076,26 +3078,26 @@ governance_clauses=( 'The final human-readable handoff must report the planner/reviewer separation.' ) -governance_skill_contract() { - local skill_path="$1" clause +governance_lifecycle_contract() { + local lifecycle_path="$1" clause for clause in "${governance_clauses[@]}"; do - [[ "$(grep -Fxc "$clause" "$skill_path")" == "1" ]] || return 1 + [[ "$(grep -Fxc "$clause" "$lifecycle_path")" == "1" ]] || return 1 done } if [[ "$installed_root" == "$(cd "$ROOT" && pwd -P)" ]] \ - && governance_skill_contract "$TMP/installed/agy-worker/SKILL.md"; then - ok "installed skill preserves independent material-plan governance and handoff disclosure" + && governance_lifecycle_contract "$TMP/installed/agy-worker/references/PROJECT_LIFECYCLE_AND_VERIFICATION.md"; then + ok "installed lifecycle guide preserves independent material-plan governance and handoff disclosure" else - bad "installed skill preserves independent material-plan governance and handoff disclosure" + bad "installed lifecycle guide preserves independent material-plan governance and handoff disclosure" fi governance_mutants_rejected=1 governance_mutant_index=0 for clause in "${governance_clauses[@]}"; do governance_mutant_index=$((governance_mutant_index + 1)) - mutant="$TMP/governance-skill-mutant-$governance_mutant_index.md" - if ! python3 -B - "$TMP/installed/agy-worker/SKILL.md" "$mutant" "$clause" <<'PY' + mutant="$TMP/governance-lifecycle-mutant-$governance_mutant_index.md" + if ! python3 -B - "$TMP/installed/agy-worker/references/PROJECT_LIFECYCLE_AND_VERIFICATION.md" "$mutant" "$clause" <<'PY' from pathlib import Path import sys @@ -3111,7 +3113,7 @@ PY governance_mutants_rejected=0 break fi - if governance_skill_contract "$mutant"; then + if governance_lifecycle_contract "$mutant"; then governance_mutants_rejected=0 break fi @@ -3133,27 +3135,27 @@ provider_notice_clauses=( 'Direct model and effort selection remain caller-owned; recommendations are advisory.' ) -provider_notice_skill_contract() { - local skill_path="$1" clause +provider_notice_lifecycle_contract() { + local lifecycle_path="$1" clause for clause in "${provider_notice_clauses[@]}"; do - [[ "$(grep -Fxc "$clause" "$skill_path")" == "1" ]] || return 1 + [[ "$(grep -Fxc "$clause" "$lifecycle_path")" == "1" ]] || return 1 done } if [[ "$installed_root" == "$(cd "$ROOT" && pwd -P)" ]] \ - && provider_notice_skill_contract "$TMP/installed/agy-worker/SKILL.md" \ - && provider_notice_skill_contract "$ROOT/skills/agy-worker/SKILL.md"; then - ok "installed skill preserves user-facing provider dispatch notice and boundary contract" + && provider_notice_lifecycle_contract "$TMP/installed/agy-worker/references/PROJECT_LIFECYCLE_AND_VERIFICATION.md" \ + && provider_notice_lifecycle_contract "$ROOT/skills/agy-worker/references/PROJECT_LIFECYCLE_AND_VERIFICATION.md"; then + ok "installed lifecycle guide preserves user-facing provider dispatch notice and boundary contract" else - bad "installed skill preserves user-facing provider dispatch notice and boundary contract" + bad "installed lifecycle guide preserves user-facing provider dispatch notice and boundary contract" fi provider_notice_mutants_rejected=1 provider_notice_mutant_index=0 for clause in "${provider_notice_clauses[@]}"; do provider_notice_mutant_index=$((provider_notice_mutant_index + 1)) - mutant="$TMP/provider-notice-skill-mutant-$provider_notice_mutant_index.md" - if ! python3 -B - "$TMP/installed/agy-worker/SKILL.md" "$mutant" "$clause" <<'PY' + mutant="$TMP/provider-notice-lifecycle-mutant-$provider_notice_mutant_index.md" + if ! python3 -B - "$TMP/installed/agy-worker/references/PROJECT_LIFECYCLE_AND_VERIFICATION.md" "$mutant" "$clause" <<'PY' from pathlib import Path import sys @@ -3169,7 +3171,7 @@ PY provider_notice_mutants_rejected=0 break fi - if provider_notice_skill_contract "$mutant"; then + if provider_notice_lifecycle_contract "$mutant"; then provider_notice_mutants_rejected=0 break fi @@ -3197,7 +3199,7 @@ for pair in "${weakening_replacements[@]}"; do old_fragment="${pair%%::*}" new_fragment="${pair##*::}" mutant="$TMP/provider-notice-weakened-$provider_notice_weakening_mutant_index.md" - if ! python3 -B - "$TMP/installed/agy-worker/SKILL.md" "$mutant" "$old_fragment" "$new_fragment" <<'PY' + if ! python3 -B - "$TMP/installed/agy-worker/references/PROJECT_LIFECYCLE_AND_VERIFICATION.md" "$mutant" "$old_fragment" "$new_fragment" <<'PY' from pathlib import Path import sys @@ -3214,7 +3216,7 @@ PY provider_notice_weakening_mutants_rejected=0 break fi - if provider_notice_skill_contract "$mutant"; then + if provider_notice_lifecycle_contract "$mutant"; then provider_notice_weakening_mutants_rejected=0 break fi @@ -3225,6 +3227,16 @@ else bad "installed provider notice contract rejects every clause weakening mutation" fi +if [[ "$installed_root" == "$(cd "$ROOT" && pwd -P)" ]] \ + && grep -Fq '[Project lifecycle and verification](references/PROJECT_LIFECYCLE_AND_VERIFICATION.md#approval-bindings-and-launch-notices)' "$ROOT/skills/agy-worker/SKILL.md" \ + && grep -Fq '[Project lifecycle and verification](references/PROJECT_LIFECYCLE_AND_VERIFICATION.md#approval-bindings-and-launch-notices)' "$TMP/installed/agy-worker/SKILL.md" \ + && grep -Fq '[Project lifecycle and verification](references/PROJECT_LIFECYCLE_AND_VERIFICATION.md#material-planning-governance)' "$ROOT/skills/agy-worker/SKILL.md" \ + && grep -Fq '[Project lifecycle and verification](references/PROJECT_LIFECYCLE_AND_VERIFICATION.md#material-planning-governance)' "$TMP/installed/agy-worker/SKILL.md"; then + ok "source and installed skill entrypoints link to lifecycle-owned notice and governance contracts" +else + bad "source and installed skill entrypoints link to lifecycle-owned notice and governance contracts" +fi + provider_read_scope_clauses=( 'Prefer `--provider-scope FILE --approve-transmission-sha SHA256` for bounded jobs. It binds exact reviewed read entries, their selected-content digest, and a write subset, then stages only selected entries in a fresh owner-private mode-`0700` Gitless provider cwd.' 'Whole-worktree dispatch remains an explicit exception. Treat the entire disposable worktree passed as `--workdir` as worker-readable and potentially transmissible to Google/Gemini, regardless of requested edit paths; `--add-dir`, prompt denylist instructions, `qa-gate --only`, and `--allow` do not narrow that read boundary.' @@ -3539,8 +3551,8 @@ if grep -Fq '`--compatibility-disposition proceed --approve-help-sha SHA256`' \ && grep -Fq 'Every emitted action or stale-approval rerun command uses' \ "$ROOT/skills/agy-worker/references/PROJECT_LIFECYCLE_AND_VERIFICATION.md" \ && [[ "$(grep -Fc '`tests/test-agy-worker.sh` (302 cases)' "$ROOT/docs/REPO_MAP.md")" == 1 ]] \ - && grep -Fq 'EXPECTED_CHECKS = 111' "$ROOT/tests/test-agy-worker-remediation.py" \ - && grep -Fq '`tests/test-agy-worker-remediation.py` (111 focused cases)' "$ROOT/docs/REPO_MAP.md" \ + && grep -Fq 'EXPECTED_CHECKS = 112' "$ROOT/tests/test-agy-worker-remediation.py" \ + && grep -Fq '`tests/test-agy-worker-remediation.py` (112 focused cases)' "$ROOT/docs/REPO_MAP.md" \ && grep -Fq '`tests/test-doctor.sh` (219 cases)' "$ROOT/docs/REPO_MAP.md" \ && grep -Fq 'Do not pin exact suite counts in this instruction file' "$ROOT/AGENTS.md" \ && grep -Fq '`docs/REPO_MAP.md` owns focused-suite inventory' "$ROOT/AGENTS.md" \ @@ -3575,7 +3587,7 @@ if grep -Fq 'tests/test-version-attestation-runner.py` (165 cases)' \ "$ROOT/docs/REPO_MAP.md" \ && grep -Fq '`tests/test-version-manifest-engine.py` (28 offline cases)' \ "$ROOT/docs/REPO_MAP.md" \ - && grep -Fq 'previous 1.1.26 permits only generic version-evidence/profile/capture' \ + && grep -Fq 'previous 1.1.27 and 1.1.26 permit only generic version-evidence/profile/capture' \ "$ROOT/docs/REPO_MAP.md" \ && grep -Fq 'historical 1.1.24, 1.1.16, and 1.1.12 permit no executable operation' \ "$ROOT/docs/REPO_MAP.md" \ @@ -4294,7 +4306,7 @@ if grep -Fq '24 quota exhausted' "$ROOT/skills/agy-worker/runtime/agy-worker.sh" && grep -Fq 'Before every reviewed direct' "$ROOT/docs/INSTALLATION.md" \ && grep -Fq 'dispatch, including an exact-version match, Codex must inspect current bounded raw' \ "$ROOT/docs/INSTALLATION.md" \ - && grep -Fq 'Before every' "$ROOT/skills/agy-worker/SKILL.md" \ + && grep -Fq 'Before every' "$ROOT/skills/agy-worker/references/PROJECT_LIFECYCLE_AND_VERIFICATION.md" \ && grep -Fq 'reviewed direct dispatch, including an exact-version match, Codex must inspect' \ "$ROOT/skills/agy-worker/references/TROUBLESHOOTING.md" \ && grep -Fq 'Codex inspects current bounded raw help before every reviewed direct dispatch' \ diff --git a/tests/test-provider-containment.py b/tests/test-provider-containment.py index aaafaba..9de5e8c 100644 --- a/tests/test-provider-containment.py +++ b/tests/test-provider-containment.py @@ -101,18 +101,33 @@ def prepare( network_policy: str = MODULE.NETWORK_DENY_ALL, read_only_inputs: tuple[Path, ...] = (), ) -> object: - return MODULE.prepare_contained_launch( - role=role, - network_policy=network_policy, - job_dir=job, - attempt=1, - stage_dir=stage, - target_executable=executable, - target_argv=argv, - child_environment=environment, - allow_keychain=allow_keychain, - read_only_inputs=read_only_inputs, - ) + original_discovery = MODULE._discover_default_keychain + if allow_keychain: + synthetic = job.parent / "synthetic-default.keychain-db" + synthetic.write_bytes(b"synthetic keychain metadata fixture\n") + synthetic.chmod(0o600) + binding = MODULE._bind_keychain(synthetic) + MODULE._discover_default_keychain = lambda: binding + try: + return MODULE.prepare_contained_launch( + role=role, + network_policy=network_policy, + job_dir=job, + attempt=1, + stage_dir=stage, + target_executable=executable, + target_argv=argv, + child_environment=environment, + allow_keychain=allow_keychain, + read_only_inputs=read_only_inputs, + provider_max_cycles=1 if allow_keychain else None, + provider_write_selectors=( + ({"kind": "file", "path": "candidate.py"},) + if allow_keychain else () + ), + ) + finally: + MODULE._discover_default_keychain = original_discovery def shell_probe(stage: Path) -> Path: @@ -428,6 +443,8 @@ def role_and_network_policy_fail_closed() -> bool: job_dir=job, attempt=1, stage_dir=stage, target_executable="/usr/bin/true", target_argv=["/usr/bin/true"], child_environment={}, allow_keychain=True, + provider_max_cycles=1, + provider_write_selectors=({"kind": "file", "path": "candidate.py"},), )) return bad_network and self_verify_network and bad_keychain finally: @@ -445,6 +462,7 @@ def security_helper_keychain_exception_is_exact_and_provider_only() -> bool: profile = MODULE.render_profile( target_executable=target, role=MODULE.ROLE_PROVIDER, network_policy=MODULE.NETWORK_PROVIDER_TLS, allow_keychain=True, + keychain_path="/private/tmp/agyworker-synthetic.keychain-db", ).decode("utf-8") helper = """(with-filter (process-path \"/usr/bin/security\") (allow mach-lookup @@ -452,7 +470,9 @@ def security_helper_keychain_exception_is_exact_and_provider_only() -> bool: (global-name \"com.apple.securityd.xpc\") (global-name \"com.apple.securityd.general\") (global-name \"com.apple.trustd\") - (global-name \"com.apple.trustd.agent\"))) + (global-name \"com.apple.trustd.agent\")) + (allow file-read* + (literal \"/private/tmp/agyworker-synthetic.keychain-db\"))) """ self_verify = MODULE.render_profile( target_executable="/usr/bin/true", role=MODULE.ROLE_SELF_VERIFY, @@ -508,6 +528,520 @@ def runtime_input_drift_fails_closed() -> bool: shutil.rmtree(root) +def keychain_preferences_create_once_and_reject_drift() -> bool: + """The generated preference never replaces a changed private artifact.""" + root, job, _stage, _checkout, _ambient = fixture("keychain-preferences") + original_payload = MODULE._default_keychain_preferences_payload + try: + keychain_file = root / "default.keychain-db" + keychain_file.write_bytes(b"metadata fixture\n") + keychain_file.chmod(0o644) + keychain = MODULE._bind_keychain(keychain_file) + home = MODULE._ensure_private_directory(job / "provider-home", existing_ok=True) + MODULE._default_keychain_preferences_payload = lambda _binding: b"synthetic plist\n" + first = MODULE._prepare_keychain_preferences(job, Path(home.path), keychain) + repaired = MODULE._prepare_keychain_preferences(job, Path(home.path), keychain) + preferences = Path(first.path) + sidecar = job / ".provider-keychain-preferences.binding" + initial = ( + first == repaired + and preferences.read_bytes() == b"synthetic plist\n" + and stat.S_IMODE(preferences.stat().st_mode) == 0o600 + and stat.S_IMODE(sidecar.stat().st_mode) == 0o600 + ) + preferences.write_bytes(b"changed\n") + changed_rejected = rejects(lambda: MODULE._prepare_keychain_preferences( + job, Path(home.path), keychain, + )) + preferences.unlink() + deleted_rejected = rejects(lambda: MODULE._prepare_keychain_preferences( + job, Path(home.path), keychain, + )) + preferences.symlink_to(keychain_file) + symlink_rejected = rejects(lambda: MODULE._prepare_keychain_preferences( + job, Path(home.path), keychain, + )) + return initial and changed_rejected and deleted_rejected and symlink_rejected + finally: + MODULE._default_keychain_preferences_payload = original_payload + shutil.rmtree(root) + + +def provider_settings_are_precomputed_private_and_rebound() -> bool: + root, job, _stage, _checkout, _ambient = fixture("provider-settings") + try: + home = MODULE._ensure_private_directory(job / "provider-home", existing_ok=True) + selectors = ( + {"kind": "file", "path": "candidate.py"}, + {"kind": "tree", "path": "output"}, + ) + validated = MODULE._validate_provider_write_selectors(selectors) + first = MODULE._prepare_provider_settings(job, Path(home.path), 2, validated) + repaired = MODULE._prepare_provider_settings(job, Path(home.path), 2, validated) + settings = Path(first.path) + sidecar = job / ".provider-antigravity-settings.binding" + expected = { + "permissions": {"allow": [ + f"read_file({job / 'stage-001'})", + f"write_file({job / 'stage-001' / 'candidate.py'})", + f"write_file({job / 'stage-001' / 'output'})", + f"read_file({job / 'stage-002'})", + f"write_file({job / 'stage-002' / 'candidate.py'})", + f"write_file({job / 'stage-002' / 'output'})", + ]}, + } + initial = ( + first == repaired + and json.loads(settings.read_text(encoding="utf-8")) == expected + and stat.S_IMODE(settings.stat().st_mode) == 0o600 + and stat.S_IMODE(sidecar.stat().st_mode) == 0o600 + ) + settings.write_bytes(b"tampered") + changed_rejected = rejects(lambda: MODULE._prepare_provider_settings( + job, Path(home.path), 2, validated, + )) + settings.unlink() + deleted_rejected = rejects(lambda: MODULE._prepare_provider_settings( + job, Path(home.path), 2, validated, + )) + settings.symlink_to(root / "outside-settings.json") + symlink_rejected = rejects(lambda: MODULE._prepare_provider_settings( + job, Path(home.path), 2, validated, + )) + invalid_selector_rejected = rejects(lambda: MODULE._validate_provider_write_selectors(( + {"kind": "file", "path": "../escape"}, + ))) and rejects(lambda: MODULE._validate_provider_write_selectors(( + {"kind": "tree", "path": ".git"}, + ))) and rejects(lambda: MODULE._validate_provider_write_selectors(( + {"kind": "file", "path": "literal*rule"}, + ))) + return ( + initial and changed_rejected and deleted_rejected and symlink_rejected + and invalid_selector_rejected + ) + finally: + shutil.rmtree(root) + + +def provider_settings_leaf_is_immutable_but_home_state_stays_writable() -> bool: + if sys.platform != "darwin": + return None + root, job, stage, _checkout, _ambient = fixture("provider-settings-profile") + try: + script = stage / "settings-probe.py" + script.write_text( + "from pathlib import Path\n" + "import os,json\n" + "home = Path(os.environ['HOME'])\n" + "result = Path(os.environ['TMPDIR']) / 'settings-result'\n" + "(home / 'ordinary-state').write_text('allowed')\n" + "settings = home / '.gemini/antigravity-cli/settings.json'\n" + "replacement = home / 'replacement-settings'\n" + "replacement.write_text('tampered')\n" + "observed = {'readable': bool(settings.read_bytes())}\n" + "for name,action in [('write', lambda: settings.write_text('tampered')), ('unlink', settings.unlink), ('replace', lambda: os.replace(replacement, settings))]:\n" + " try: action()\n" + " except PermissionError: observed[name] = 'denied'\n" + " else: observed[name] = 'allowed'\n" + "result.write_text(json.dumps(observed))\n", + encoding="utf-8", + ) + script.chmod(0o600) + prepared = prepare( + job, stage, "/usr/bin/python3", + ["/usr/bin/python3", "-I", "-S", "-B", str(script)], + {}, allow_keychain=True, + ) + confirmed = run_confirmed(prepared) + profile = Path(prepared.profile.path).read_text(encoding="utf-8") + settings = Path(prepared.provider_settings.path) + result = Path(prepared.attempt_tmp.path) / "settings-result" + confirmed_rebinds = confirmed.returncode == 0 + settings.write_text("tampered", encoding="utf-8") + try: + MODULE.confirm_contained_launch(prepared) + except MODULE.ContainmentError as exc: + settings_error = str(exc) == "native provider permission settings are unavailable" + else: + settings_error = False + return ( + confirmed.returncode == 0 + and (Path(prepared.private_home.path) / "ordinary-state").read_text() == "allowed" + and json.loads(result.read_text()) == { + "readable": True, "write": "denied", "unlink": "denied", "replace": "denied", + } + and f'(deny file-write*\n (literal "{settings}"))' in profile + and confirmed_rebinds + and settings_error + ) + finally: + shutil.rmtree(root) + + +def provider_settings_rule_targets_and_failures_are_exact() -> bool: + if sys.platform != "darwin": + return None + root, job, stage, _checkout, _ambient = fixture("provider-settings-boundary") + original_discovery = MODULE._discover_default_keychain + original_preferences = MODULE._prepare_keychain_preferences + original_settings = MODULE._prepare_provider_settings + try: + selectors = MODULE._validate_provider_write_selectors(( + {"kind": "file", "path": "candidate.py"}, + )) + grammar_job = root / "job*rule" + grammar_job.mkdir(mode=0o700) + grammar_rejected = rejects(lambda: MODULE._provider_settings_payload( + grammar_job, 1, selectors, + )) + mismatched = job / "stage-other" + mismatched.mkdir(mode=0o700) + stage_rejected = rejects(lambda: MODULE.prepare_contained_launch( + role=MODULE.ROLE_PROVIDER, network_policy=MODULE.NETWORK_DENY_ALL, + job_dir=job, attempt=1, stage_dir=mismatched, + target_executable="/usr/bin/true", target_argv=["/usr/bin/true"], + child_environment={}, allow_keychain=True, provider_max_cycles=1, + provider_write_selectors=({"kind": "file", "path": "candidate.py"},), + )) + verification_copy = job / "verification-copy" + verification_copy.mkdir(mode=0o700) + self_verify = MODULE.prepare_contained_launch( + role=MODULE.ROLE_SELF_VERIFY, network_policy=MODULE.NETWORK_DENY_ALL, + job_dir=job, attempt=1, stage_dir=verification_copy, + target_executable="/usr/bin/true", target_argv=["/usr/bin/true"], + child_environment={}, + ) + + def exact_error(action: Callable[[], object], expected: str) -> bool: + try: + action() + except MODULE.ContainmentError as exc: + return str(exc) == expected + return False + + settings_validation = exact_error(lambda: MODULE.prepare_contained_launch( + role=MODULE.ROLE_PROVIDER, network_policy=MODULE.NETWORK_DENY_ALL, + job_dir=job, attempt=1, stage_dir=stage, + target_executable="/usr/bin/true", target_argv=["/usr/bin/true"], + child_environment={}, allow_keychain=True, provider_max_cycles=1, + provider_write_selectors=({"kind": "file", "path": "bad*selector"},), + ), "native provider permission settings are unavailable") + MODULE._discover_default_keychain = lambda: (_ for _ in ()).throw( + MODULE.ContainmentError("synthetic auth failure"), + ) + authentication = exact_error(lambda: MODULE.prepare_contained_launch( + role=MODULE.ROLE_PROVIDER, network_policy=MODULE.NETWORK_DENY_ALL, + job_dir=job, attempt=1, stage_dir=stage, + target_executable="/usr/bin/true", target_argv=["/usr/bin/true"], + child_environment={}, allow_keychain=True, provider_max_cycles=1, + provider_write_selectors=({"kind": "file", "path": "candidate.py"},), + ), "native provider authentication is unavailable") + keychain_file = root / "synthetic.keychain-db" + keychain_file.write_bytes(b"metadata only\n") + keychain_file.chmod(0o600) + MODULE._discover_default_keychain = lambda: MODULE._bind_keychain(keychain_file) + MODULE._prepare_keychain_preferences = lambda *_args: object() + MODULE._prepare_provider_settings = lambda *_args: (_ for _ in ()).throw( + MODULE.ContainmentError("synthetic settings failure"), + ) + settings_preparation = exact_error(lambda: MODULE.prepare_contained_launch( + role=MODULE.ROLE_PROVIDER, network_policy=MODULE.NETWORK_DENY_ALL, + job_dir=job, attempt=1, stage_dir=stage, + target_executable="/usr/bin/true", target_argv=["/usr/bin/true"], + child_environment={}, allow_keychain=True, provider_max_cycles=1, + provider_write_selectors=({"kind": "file", "path": "candidate.py"},), + ), "native provider permission settings are unavailable") + return ( + grammar_rejected and stage_rejected + and self_verify.stage.path == str(verification_copy) + and self_verify.provider_settings is None + and not (Path(self_verify.private_home.path) / ".gemini").exists() + and settings_validation + and authentication and settings_preparation + ) + finally: + MODULE._discover_default_keychain = original_discovery + MODULE._prepare_keychain_preferences = original_preferences + MODULE._prepare_provider_settings = original_settings + shutil.rmtree(root) + + +def keychain_binding_is_metadata_only_and_rebinds_identity() -> bool: + if sys.platform != "darwin": + return None + root, job, stage, _checkout, _ambient = fixture("keychain-binding") + original_discovery = MODULE._discover_default_keychain + original_payload = MODULE._default_keychain_preferences_payload + original_read_hash = MODULE._read_hash + original_lstat = MODULE.os.lstat + original_canonical = MODULE._canonical_existing + try: + keychain_file = root / "default.keychain-db" + keychain_file.write_bytes(b"first metadata bytes\n") + keychain_file.chmod(0o644) + lstat_calls = 0 + + def metadata_churn(path: object) -> object: + nonlocal lstat_calls + result = original_lstat(path) + if os.fspath(path) == str(keychain_file): + lstat_calls += 1 + if lstat_calls == 1: + keychain_file.write_bytes(b"between-lstat content churn\n") + return result + + MODULE._canonical_existing = lambda _path: keychain_file + MODULE.os.lstat = metadata_churn + churn_tolerated = MODULE._bind_keychain(keychain_file) + MODULE.os.lstat = original_lstat + MODULE._canonical_existing = original_canonical + MODULE._read_hash = lambda *_args, **_kwargs: (_ for _ in ()).throw( + AssertionError("keychain database must not be hashed"), + ) + keychain = MODULE._bind_keychain(keychain_file) + MODULE._read_hash = original_read_hash + MODULE._default_keychain_preferences_payload = lambda _binding: b"synthetic plist\n" + MODULE._discover_default_keychain = lambda: keychain + prepared = MODULE.prepare_contained_launch( + role=MODULE.ROLE_PROVIDER, network_policy=MODULE.NETWORK_DENY_ALL, + job_dir=job, attempt=1, stage_dir=stage, + target_executable="/usr/bin/true", target_argv=["/usr/bin/true"], + child_environment={}, allow_keychain=True, + provider_max_cycles=1, + provider_write_selectors=({"kind": "file", "path": "candidate.py"},), + ) + keychain_file.write_bytes(b"changed content is allowed\n") + unchanged_identity_passes = MODULE.confirm_contained_launch(prepared).argv[0] == "/usr/bin/sandbox-exec" + replacement = root / "replacement.keychain-db" + replacement.write_bytes(b"replacement\n") + replacement.chmod(0o644) + replacement.replace(keychain_file) + identity_drift_rejected = rejects(lambda: MODULE.confirm_contained_launch(prepared)) + return ( + churn_tolerated.path == str(keychain_file) + and lstat_calls == 2 + and unchanged_identity_passes and identity_drift_rejected + ) + finally: + MODULE._discover_default_keychain = original_discovery + MODULE._default_keychain_preferences_payload = original_payload + MODULE._read_hash = original_read_hash + MODULE.os.lstat = original_lstat + MODULE._canonical_existing = original_canonical + shutil.rmtree(root) + + +def keychain_policy_is_helper_only_and_self_verify_never_discovers() -> bool: + keychain = "/private/tmp/agyworker-helper-only.keychain-db" + profile = MODULE.render_profile( + target_executable="/usr/bin/true", role=MODULE.ROLE_PROVIDER, + network_policy=MODULE.NETWORK_DENY_ALL, allow_keychain=True, + keychain_path=keychain, + ).decode("utf-8") + helper_clause = ( + '(with-filter (process-path "/usr/bin/security")\n' + ' (allow mach-lookup\n' + ) + return ( + helper_clause in profile + and profile.count(f'(literal "{keychain}")') == 1 + and f'(with-filter (process-path "/bin/cat")' not in profile + and rejects(lambda: MODULE.render_profile( + target_executable="/usr/bin/true", role=MODULE.ROLE_SELF_VERIFY, + network_policy=MODULE.NETWORK_DENY_ALL, allow_keychain=False, + keychain_path=keychain, + )) + ) + + +def keychain_locator_is_bounded_closed_and_strict() -> bool: + """Exercise only synthetic locator scripts; no owner Keychain is queried.""" + if sys.platform != "darwin": + return None + root, _job, _stage, _checkout, _ambient = fixture("keychain-locator") + original_helper = MODULE.SECURITY_HELPER + original_deadline = MODULE.KEYCHAIN_LOCATOR_DEADLINE_SECONDS + original_grace = MODULE.KEYCHAIN_LOCATOR_TERM_GRACE_SECONDS + try: + database = root / "default.keychain-db" + database.write_bytes(b"metadata only\n") + database.chmod(0o644) + + def locator_script(name: str, body: str) -> Path: + script = root / name + script.write_text("#!/bin/sh\n" + body + "\n", encoding="utf-8") + script.chmod(0o755) + return script + + def discovered(script: Path) -> object: + MODULE.SECURITY_HELPER = script + return MODULE._discover_default_keychain() + + quoted = f"printf '\"%s\"\\n' \"{database}\"" + valid = discovered(locator_script("valid-security", quoted)) + expected = MODULE._bind_keychain(database) + indented = locator_script( + "indented-security", f"printf ' \"%s\"\\n' \"{database}\"", + ) + indented_valid = discovered(indented) + invalid = locator_script("invalid-security", "printf 'not quoted\\n'") + multiple = locator_script("multiple-security", quoted + "; " + quoted) + trailing = locator_script( + "trailing-security", f"printf '\"%s\" trailing\\n' \"{database}\"", + ) + symlink = root / "linked.keychain-db" + symlink.symlink_to(database) + linked = locator_script( + "linked-security", f"printf '\"%s\"\\n' \"{symlink}\"", + ) + directory = root / "not-a-keychain" + directory.mkdir(mode=0o700) + nonregular = locator_script( + "directory-security", f"printf '\"%s\"\\n' \"{directory}\"", + ) + writable = root / "writable.keychain-db" + writable.write_bytes(b"metadata only\n") + writable.chmod(0o666) + writable_script = locator_script( + "writable-security", f"printf '\"%s\"\\n' \"{writable}\"", + ) + oversized = locator_script( + "oversized-security", "while :; do printf 0123456789abcdef; done", + ) + timeout = locator_script("timeout-security", "sleep 2") + MODULE.KEYCHAIN_LOCATOR_DEADLINE_SECONDS = 0.05 + MODULE.KEYCHAIN_LOCATOR_TERM_GRACE_SECONDS = 0.05 + return ( + valid == expected + and indented_valid == expected + and rejects(lambda: discovered(invalid)) + and rejects(lambda: discovered(multiple)) + and rejects(lambda: discovered(trailing)) + and rejects(lambda: discovered(linked)) + and rejects(lambda: discovered(nonregular)) + and rejects(lambda: discovered(writable_script)) + and rejects(lambda: discovered(oversized)) + and rejects(lambda: discovered(timeout)) + ) + finally: + MODULE.SECURITY_HELPER = original_helper + MODULE.KEYCHAIN_LOCATOR_DEADLINE_SECONDS = original_deadline + MODULE.KEYCHAIN_LOCATOR_TERM_GRACE_SECONDS = original_grace + shutil.rmtree(root) + + +def keychain_locator_reaps_leaderless_descendants() -> bool: + """An exited locator leader cannot leave its fresh session running.""" + if sys.platform != "darwin": + return None + root, _job, _stage, _checkout, _ambient = fixture("keychain-locator-reap") + original_helper = MODULE.SECURITY_HELPER + original_deadline = MODULE.KEYCHAIN_LOCATOR_DEADLINE_SECONDS + original_grace = MODULE.KEYCHAIN_LOCATOR_TERM_GRACE_SECONDS + try: + database = root / "default.keychain-db" + database.write_bytes(b"metadata only\n") + database.chmod(0o644) + + def helper(name: str, body: str) -> Path: + script = root / name + script.write_text("#!/bin/sh\n" + body + "\n", encoding="utf-8") + script.chmod(0o755) + return script + + def run(script: Path) -> bool: + MODULE.SECURITY_HELPER = script + return rejects(MODULE._discover_default_keychain) + + def gone(pid_path: Path) -> bool: + pid = int(pid_path.read_text(encoding="ascii").strip()) + deadline = time.monotonic() + 2 + while time.monotonic() < deadline: + try: + os.kill(pid, 0) + except ProcessLookupError: + return True + time.sleep(0.02) + return False + + MODULE.KEYCHAIN_LOCATOR_DEADLINE_SECONDS = 0.3 + MODULE.KEYCHAIN_LOCATOR_TERM_GRACE_SECONDS = 0.3 + holding_pid = root / "holding.pid" + holding = helper( + "holding-stdout-security", + f"sleep 30 & echo $! > '{holding_pid}'; exit 0", + ) + closes_pid = root / "closed.pid" + closes = helper( + "closed-stdout-security", + f"sleep 30 >/dev/null 2>&1 & echo $! > '{closes_pid}'; " + f"printf '\"%s\"\\n' '{database}'; exit 0", + ) + holding_rejected = run(holding) + holding_gone = holding_pid.exists() and gone(holding_pid) + closed_rejected = run(closes) + closed_gone = closes_pid.exists() and gone(closes_pid) + return holding_rejected and holding_gone and closed_rejected and closed_gone + finally: + MODULE.SECURITY_HELPER = original_helper + MODULE.KEYCHAIN_LOCATOR_DEADLINE_SECONDS = original_deadline + MODULE.KEYCHAIN_LOCATOR_TERM_GRACE_SECONDS = original_grace + shutil.rmtree(root) + + +def private_keychain_publication_failures_are_sanitized() -> bool: + root, _job, _stage, _checkout, _ambient = fixture("keychain-publish-failure") + original_write = MODULE.os.write + original_fsync = MODULE.os.fsync + original_fchmod = MODULE.os.fchmod + try: + def normalized(path: Path) -> bool: + try: + MODULE._publish_private_file(path, b"payload\n") + except MODULE.ContainmentError as exc: + return str(exc) == "native provider authentication is unavailable" + return False + + MODULE.os.write = lambda *_args: (_ for _ in ()).throw(OSError("synthetic write")) + write_failure = normalized(root / "write.plist") + MODULE.os.write = original_write + MODULE.os.fsync = lambda *_args: (_ for _ in ()).throw(OSError("synthetic fsync")) + fsync_failure = normalized(root / "fsync.plist") + MODULE.os.fsync = original_fsync + MODULE.os.fchmod = lambda *_args: (_ for _ in ()).throw(OSError("synthetic chmod")) + chmod_failure = normalized(root / "chmod.plist") + return write_failure and fsync_failure and chmod_failure + finally: + MODULE.os.write = original_write + MODULE.os.fsync = original_fsync + MODULE.os.fchmod = original_fchmod + shutil.rmtree(root) + + +def self_verify_never_runs_keychain_discovery() -> bool: + if sys.platform != "darwin": + return None + root, job, stage, _checkout, _ambient = fixture("no-self-verify-keychain") + original_discovery = MODULE._discover_default_keychain + calls = 0 + try: + def unavailable() -> object: + nonlocal calls + calls += 1 + raise AssertionError("self-verification must not discover a Keychain") + MODULE._discover_default_keychain = unavailable + prepared = MODULE.prepare_contained_launch( + role=MODULE.ROLE_SELF_VERIFY, network_policy=MODULE.NETWORK_DENY_ALL, + job_dir=job, attempt=1, stage_dir=stage, + target_executable="/usr/bin/true", target_argv=["/usr/bin/true"], + child_environment={}, allow_keychain=False, + ) + return calls == 0 and prepared.keychain is None and prepared.keychain_preferences is None + finally: + MODULE._discover_default_keychain = original_discovery + shutil.rmtree(root) + + def unsupported_host_fails_before_creation() -> bool: root, job, stage, _checkout, _ambient = fixture("unsupported") original_platform = MODULE.sys.platform @@ -910,6 +1444,11 @@ def integrated_stage_rebind_and_outside_write_denial() -> bool: stage, scope, selected, stage_identity, stage_sha, ) target = DISPATCH.CONTAINMENT + synthetic_keychain = root / "synthetic-default.keychain-db" + synthetic_keychain.write_bytes(b"integration metadata fixture\n") + synthetic_keychain.chmod(0o600) + original_discovery = target._discover_default_keychain + target._discover_default_keychain = lambda: target._bind_keychain(synthetic_keychain) python = CLT_PYTHON_EXECUTABLE script = ( "import errno,os,time\n" @@ -955,6 +1494,8 @@ def integrated_stage_rebind_and_outside_write_denial() -> bool: child_environment={"PATH": "/usr/bin:/bin", "SECRET_SHOULD_NOT_PASS": "provider-approved"}, allow_keychain=True, read_only_inputs=(schema,), + provider_max_cycles=3, + provider_write_selectors=scope["write"], ) confirmed = target.confirm_contained_launch(prepared) process = subprocess.Popen( @@ -1034,16 +1575,275 @@ def observed_text(path: Path) -> str: ) return True finally: + if "target" in locals() and "original_discovery" in locals(): + target._discover_default_keychain = original_discovery if "inherited_fd" in locals() and inherited_fd >= 0: os.close(inherited_fd) shutil.rmtree(root) +def provider_parent_metadata_rule_is_exact() -> bool: + target = '/private/owned parent "quoted"/provider' + provider = MODULE.render_profile( + target_executable=target, role=MODULE.ROLE_PROVIDER, + network_policy=MODULE.NETWORK_DENY_ALL, allow_keychain=False, + ).decode() + verifier = MODULE.render_profile( + target_executable=target, role=MODULE.ROLE_SELF_VERIFY, + network_policy=MODULE.NETWORK_DENY_ALL, allow_keychain=False, + ).decode() + addition = ( + f'\n(with-filter (process-path {MODULE._scheme_string(target)})\n' + ' (allow file-read-metadata file-test-existence\n' + f' (literal {MODULE._scheme_string(str(Path(target).parent))}))\n' + ' ; SQLite resolves every ancestor before opening its private conversation DB.\n' + ' (allow file-read-metadata (path-ancestors (param "HOME"))))\n' + ) + return provider.count(addition) == 1 and provider.replace(addition, "") == verifier + + +def provider_home_ancestor_metadata_is_image_bound() -> bool: + """Exercise the SQLite-required ancestor lstat grant in a native profile.""" + if sys.platform != "darwin": + return None + root, _job, _stage, _checkout, _ambient = fixture("home-ancestor-metadata") + try: + source = root / "ancestor-probe.c" + source.write_text(r''' +#include +#include +#include +#include +#include +#include +#include +#include +#include + +static int denied(void) { return errno == EACCES || errno == EPERM; } + +static int parent_path(const char *home, char path[PATH_MAX]) { + if (strlcpy(path, home, PATH_MAX) >= PATH_MAX) return 0; + char *slash = strrchr(path, '/'); + if (!slash || slash == path) return 0; + *slash = '\0'; + return 1; +} + +static int trim_parent(char path[PATH_MAX]) { + char *slash = strrchr(path, '/'); + if (!slash) return 0; + if (slash == path) { + if (path[1] == '\0') return 0; + path[1] = '\0'; + return 1; + } + *slash = '\0'; + return 1; +} + +static int every_ancestor_lstat(const char *home) { + char path[PATH_MAX]; struct stat st; + if (!parent_path(home, path)) return 0; + for (;;) { + if (lstat(path, &st) != 0) return 0; + if (!trim_parent(path)) return 1; + } +} + +static int immediate_ancestor_directory_listing_denied(const char *home) { + char path[PATH_MAX]; + if (!parent_path(home, path)) return 0; + errno = 0; DIR *directory = opendir(path); + if (directory || !denied()) { if (directory) closedir(directory); return 0; } + return 1; +} + +static int immediate_parent_lstat_denied(const char *home) { + char path[PATH_MAX]; struct stat st; + if (!parent_path(home, path)) return 0; + errno = 0; + return lstat(path, &st) == -1 && denied(); +} + +int main(int argc, char **argv) { + if (argc == 3 && strcmp(argv[1], "child") == 0) + return immediate_parent_lstat_denied(argv[2]) ? 0 : 1; + if (argc != 5) return 90; + struct stat st; + int ancestors_lstat = every_ancestor_lstat(argv[1]); + int ancestors_listing_denied = immediate_ancestor_directory_listing_denied(argv[1]); + errno = 0; int ancestor_data = open(argv[3], O_RDONLY); + int ancestor_data_denied = ancestor_data == -1 && denied(); + if (ancestor_data >= 0) close(ancestor_data); + errno = 0; int sibling_metadata_denied = lstat(argv[2], &st) == -1 && denied(); + errno = 0; int descriptor = open(argv[2], O_RDONLY); + int sibling_read_denied = descriptor == -1 && denied(); + if (descriptor >= 0) close(descriptor); + pid_t child = fork(); + if (child < 0) return 91; + if (!child) { execl(argv[4], argv[4], "child", argv[1], (char *)NULL); _exit(92); } + int status; + if (waitpid(child, &status, 0) != child || !WIFEXITED(status)) return 93; + printf("%d %d %d %d %d %d\n", ancestors_lstat, ancestors_listing_denied, + ancestor_data_denied, sibling_metadata_denied, sibling_read_denied, + WEXITSTATUS(status) == 0); + return 0; +} +''', encoding="utf-8") + for role, expected in ( + (MODULE.ROLE_PROVIDER, "1 1 1 1 1 1"), + (MODULE.ROLE_SELF_VERIFY, "0 1 1 1 1 1"), + ): + job = root / f"{role}-job" + stage = job / "stage-001" + job.mkdir(mode=0o700) + stage.mkdir(mode=0o700) + target = stage / "ancestor-probe" + child = stage / "other-image" + sibling = job / "private-sibling" + ancestor_data = job / "ancestor-data" + sibling.write_text("not readable or metadata-visible\n", encoding="utf-8") + ancestor_data.write_text("not readable from a HOME ancestor\n", encoding="utf-8") + subprocess.run( + ["/usr/bin/clang", "-O0", "-Wall", "-Wextra", "-Werror", + str(source), "-o", str(target)], + check=True, stdin=subprocess.DEVNULL, + stdout=subprocess.PIPE, stderr=subprocess.PIPE, timeout=30, + ) + shutil.copy2(target, child) + target.chmod(0o700) + child.chmod(0o700) + prepared = prepare( + job, stage, target, + [str(target), str(job / "provider-home"), str(sibling), str(ancestor_data), str(child)], + {"PATH": "/usr/bin:/bin"}, role=role, + ) + result = run_confirmed(prepared) + observed = result.stdout.decode("utf-8", errors="strict").strip() + if result.returncode != 0 or observed != expected: + raise AssertionError( + f"{role} ancestor metadata mismatch: rc={result.returncode} " + f"stdout={result.stdout!r} stderr={result.stderr!r} expected={expected!r}" + ) + return True + finally: + shutil.rmtree(root) + + +def external_provider_bundle_preserves_metadata_boundary() -> bool: + if sys.platform != "darwin": + return None + root, job, stage, _checkout, _ambient = fixture("bundle-metadata") + try: + image_dir = root / "external-provider" + image_dir.mkdir(mode=0o700) + target = image_dir / "provider" + sibling = image_dir / "private-sentinel" + sibling.write_text("synthetic private sibling\n", encoding="utf-8") + source = root / "bundle-probe.c" + source.write_text(r''' +#include +#include +#include +#include +#include +#include +#include +#include + +static int denied(void) { return errno == EACCES || errno == EPERM; } +int main(int argc, char **argv) { + if (argc != 3) return 90; + struct stat st; + int parent_stat = stat(argv[1], &st) == 0; + DIR *dir = opendir(argv[1]); + int listing_denied = !dir && denied(); + if (dir) closedir(dir); + FILE *file = fopen(argv[2], "r"); + int sibling_read_denied = !file && denied(); + if (file) fclose(file); + int sibling_stat_denied = stat(argv[2], &st) == -1 && denied(); + pid_t child = fork(); + if (child < 0) return 91; + if (!child) _exit(stat(argv[1], &st) == 0 ? 0 : 1); + int status; + if (waitpid(child, &status, 0) != child || !WIFEXITED(status)) return 92; + int fork_stat = WEXITSTATUS(status) == 0; + child = fork(); + if (child < 0) return 93; + if (!child) { + if (!freopen("/dev/null", "w", stderr)) _exit(94); + execl("/usr/bin/stat", "stat", "-f", "%i", argv[1], (char *)NULL); + _exit(95); + } + if (waitpid(child, &status, 0) != child || !WIFEXITED(status)) return 96; + int exec_stat_denied = WEXITSTATUS(status) == 1; + CFBundleRef bundle = CFBundleGetMainBundle(); + CFDictionaryRef info = bundle ? CFBundleGetInfoDictionary(bundle) : NULL; + SecPolicyRef policy = SecPolicyCreateSSL(true, CFSTR("example.com")); + printf("{\"parent_stat\":%d,\"listing_denied\":%d," + "\"sibling_read_denied\":%d,\"sibling_stat_denied\":%d," + "\"fork_stat\":%d,\"exec_stat_denied\":%d," + "\"bundle\":%d,\"info\":%d,\"policy\":%d}\n", + parent_stat, listing_denied, sibling_read_denied, sibling_stat_denied, + fork_stat, exec_stat_denied, bundle != NULL, info != NULL, policy != NULL); + if (policy) CFRelease(policy); + return 0; +} +''', encoding="utf-8") + subprocess.run( + ["/usr/bin/clang", "-Wall", "-Wextra", "-Werror", "-framework", + "CoreFoundation", "-framework", "Security", str(source), "-o", str(target)], + check=True, capture_output=True, timeout=30, + ) + target.chmod(0o700) + for role, allowed in ((MODULE.ROLE_SELF_VERIFY, 0), (MODULE.ROLE_PROVIDER, 1)): + role_job = root / role + role_job.mkdir(mode=0o700) + role_stage = role_job / "stage-001" + role_stage.mkdir(mode=0o700) + prepared = prepare( + role_job, role_stage, target, + [str(target), str(image_dir), str(sibling)], + {"PATH": "/usr/bin:/bin", "LANG": "C.UTF-8"}, role=role, + ) + confirmed = MODULE.confirm_contained_launch(prepared) + result = subprocess.run( + confirmed.argv, cwd=confirmed.cwd, env=confirmed.environment, + capture_output=True, text=True, timeout=15, + ) + expected = { + "parent_stat": allowed, "listing_denied": 1, + "sibling_read_denied": 1, "sibling_stat_denied": 1, + "fork_stat": allowed, "exec_stat_denied": 1, + "bundle": allowed, "info": allowed, "policy": allowed, + } + if result.returncode != 0 or json.loads(result.stdout) != expected: + raise AssertionError(f"{role}: {result!r}; expected {expected!r}") + return True + finally: + shutil.rmtree(root) + + +check("provider parent metadata is the only safely escaped role-specific read delta", provider_parent_metadata_rule_is_exact) +check("provider HOME ancestor metadata is exact, non-readable, and removed by another image", provider_home_ancestor_metadata_is_image_bound) +check("external provider SSL policy needs only image-bound parent metadata", external_provider_bundle_preserves_metadata_boundary) check("profile, role, HOME, TMP, and exact target are launch-bound", profile_and_environment_are_private) check("invalid network and self-verification provider authority fail closed", role_and_network_policy_fail_closed) check("Keychain helper exception is exact, provider-only, and network-free", security_helper_keychain_exception_is_exact_and_provider_only) check("profile identity drift fails before native launch", binding_drift_fails_closed) check("exact read-only runtime input drift fails before native launch", runtime_input_drift_fails_closed) +check("private generated DefaultKeychain preferences create once and reject repair drift", keychain_preferences_create_once_and_reject_drift) +check("precomputed provider settings bind all repair stages and reject drift", provider_settings_are_precomputed_private_and_rebound) +check("provider settings leaf stays immutable while ordinary HOME state remains writable", provider_settings_leaf_is_immutable_but_home_state_stays_writable) +check("provider settings rule targets, stages, and failure labels stay bounded", provider_settings_rule_targets_and_failures_are_exact) +check("default keychain identity is metadata-only and allows content churn only", keychain_binding_is_metadata_only_and_rebinds_identity) +check("Keychain read authority is helper-only and self-verification has none", keychain_policy_is_helper_only_and_self_verify_never_discovers) +check("synthetic default-Keychain locator is closed, bounded, and strict", keychain_locator_is_bounded_closed_and_strict) +check("locator cleanup reaps descendants after leader exit on timeout and apparent success", keychain_locator_reaps_leaderless_descendants) +check("private Keychain preference publication failures are sanitized before launch", private_keychain_publication_failures_are_sanitized) +check("self-verification skips all Keychain discovery and preference state", self_verify_never_runs_keychain_discovery) check("unsupported hosts fail before creating containment state", unsupported_host_fails_before_creation) check("native stage boundary denies checkout, Git, HOME, siblings, and local deputies", native_boundary_is_enforced) check("Apple localhost token denies IPv4, IPv6, and interface-local owned listeners", localhost_predicate_blocks_owned_local_endpoints) diff --git a/tests/test-update.sh b/tests/test-update.sh index fd3b4d8..a433c3b 100755 --- a/tests/test-update.sh +++ b/tests/test-update.sh @@ -116,7 +116,7 @@ git -C "$UPSTREAM_SOURCE" config user.name test printf 'reviewed upstream\n' > "$UPSTREAM_SOURCE/README.md" git -C "$UPSTREAM_SOURCE" add README.md git -C "$UPSTREAM_SOURCE" commit -qm 'reviewed upstream fixture' -git -C "$UPSTREAM_SOURCE" tag v1.1.27 +git -C "$UPSTREAM_SOURCE" tag v1.2.2 UPSTREAM_HEAD="$(git -C "$UPSTREAM_SOURCE" rev-parse HEAD)" git init -q --bare "$UPSTREAM_REMOTE" git -C "$UPSTREAM_SOURCE" remote add publish "$UPSTREAM_REMOTE" @@ -154,7 +154,7 @@ case "${FAKE_AGY_MODE:-version}" in usage) printf 'Usage: agy [options] [command]\n'; exit 0 ;; fail) exit 7 ;; esac -printf '%s\n' "${FAKE_AGY_OUTPUT:-${FAKE_AGY_VERSION:-1.1.27}}" +printf '%s\n' "${FAKE_AGY_OUTPUT:-${FAKE_AGY_VERSION:-1.2.2}}" STUB cat > "$TMP/bin/codex" <<'STUB' #!/usr/bin/env bash @@ -200,7 +200,7 @@ case "${1:-}" in unavailable) exit 2 ;; malformed) printf '%s\n' 'credential-bearing malformed official bytes'; exit 0 ;; drift) printf 'agy\t%s\t%s\n' "${FAKE_AGY_OFFICIAL_VERSION:-1.1.13}" "${FAKE_AGY_OFFICIAL_HEAD:-$FAKE_AGY_HEAD}" ;; - *) printf 'agy\t%s\t%s\n' "${FAKE_AGY_OFFICIAL_VERSION:-1.1.27}" "${FAKE_AGY_OFFICIAL_HEAD:-$FAKE_AGY_HEAD}" ;; + *) printf 'agy\t%s\t%s\n' "${FAKE_AGY_OFFICIAL_VERSION:-1.2.2}" "${FAKE_AGY_OFFICIAL_HEAD:-$FAKE_AGY_HEAD}" ;; esac ;; official-codex) @@ -235,10 +235,10 @@ raise SystemExit(0 if module.MANIFEST_URL == expected else 1) fi case "${FAKE_MANIFEST_RESULT:-unchanged}" in unchanged) - printf '%s\n' ' distribution manifest: unchanged (1.1.27)' + printf '%s\n' ' distribution manifest: unchanged (1.2.2)' exit 0 ;; drift) - printf '%s\n' ' distribution manifest: drift-review (official distribution 1.1.28; verified 1.1.27)' + printf '%s\n' ' distribution manifest: drift-review (official distribution 1.2.3; verified 1.2.2)' exit 3 ;; unavailable) printf '%s\n' ' distribution manifest: evidence-unavailable (network evidence unavailable)' @@ -279,7 +279,7 @@ git -C "$SOURCE" remote add publish "$REMOTE" git -C "$SOURCE" push -q publish main --tags git --git-dir="$REMOTE" symbolic-ref HEAD refs/heads/main export FAKE_PROJECT_REMOTE="$REMOTE" -export FAKE_AGY_HEAD="1ae9cb7b51667192c051b73a91099c71e816ca5f" +export FAKE_AGY_HEAD="ba985e6b5de2ac8aa09860a154a102831eb7722b" export FAKE_CODEX_HEAD="$CODEX_UPSTREAM_HEAD" git init -q --bare "$NO_TAG_REMOTE" git -C "$SOURCE" push -q "$NO_TAG_REMOTE" main diff --git a/tests/test-version-manifest-engine.py b/tests/test-version-manifest-engine.py index 47e98b5..08d652c 100644 --- a/tests/test-version-manifest-engine.py +++ b/tests/test-version-manifest-engine.py @@ -48,7 +48,7 @@ def write_manifest(root: Path, data: dict[str, object]) -> Path: class VersionManifestEngineTests(unittest.TestCase): def candidate_record(self) -> dict[str, object]: - current = engine.get_version_spec("1.1.27").as_dict() + current = engine.get_version_spec("1.2.2").as_dict() fields = { "version", "support_tier", "allowed_operations", "expected_stdout", "source_sha256", "source_size", "release_commit", "distribution_url", @@ -113,7 +113,7 @@ def test_candidate_cannot_capture_or_activate(self) -> None: self.assertEqual(result.stdout, b"", name) def test_candidate_rejects_premature_or_partial_evidence(self) -> None: - current = engine.get_version_spec("1.1.27").as_dict() + current = engine.get_version_spec("1.2.2").as_dict() record = self.candidate_record() for key in set(current) - set(record): with self.subTest(key=key), self.assertRaises(engine.EngineError): @@ -132,7 +132,10 @@ def test_01_manifest_and_digest_are_exact(self) -> None: raw = MANIFEST_PATH.read_bytes() expected = MANIFEST_PATH.with_suffix(".sha256").read_text(encoding="ascii").strip() self.assertEqual(hashlib.sha256(raw).hexdigest(), expected) - self.assertEqual(set(engine.load_manifest(MANIFEST_PATH)), {"1.1.12", "1.1.16", "1.1.22", "1.1.24", "1.1.26", "1.1.27"}) + self.assertEqual( + set(engine.load_manifest(MANIFEST_PATH)), + {"1.1.12", "1.1.16", "1.1.22", "1.1.24", "1.1.26", "1.1.27", "1.2.2"}, + ) def test_02_portable_artifacts_are_byte_identical(self) -> None: for relative in ( @@ -142,16 +145,16 @@ def test_02_portable_artifacts_are_byte_identical(self) -> None: self.assertEqual((ROOT / relative).read_bytes(), (PORTABLE / relative).read_bytes(), relative) def test_03_current_spec_is_exact(self) -> None: - spec = engine.get_version_spec("1.1.27", MANIFEST_PATH) - self.assertEqual(spec.version, "1.1.27") + spec = engine.get_version_spec("1.2.2", MANIFEST_PATH) + self.assertEqual(spec.version, "1.2.2") self.assertEqual(spec.support_tier, "current") self.assertEqual( spec.allowed_operations, ("activation", "capture", "classifier", "profile", "reprofile", "version-evidence"), ) - self.assertEqual(spec.expected_stdout, b"1.1.27\n") - self.assertEqual(spec.source_sha256, "d583be1344ea9cfa0c45cff2c1342af7837f4833c4edb65e69bee84776a45caa") - self.assertEqual(spec.release_commit, "1ae9cb7b51667192c051b73a91099c71e816ca5f") + self.assertEqual(spec.expected_stdout, b"1.2.2\n") + self.assertEqual(spec.source_sha256, "cabadc15a61944372bede1fdff186701c17467dd9d718e97dc79283055d3c101") + self.assertEqual(spec.release_commit, "ba985e6b5de2ac8aa09860a154a102831eb7722b") self.assertEqual(spec.slug_count, 14) self.assertEqual(spec.capture_snapshot_policy, "macos-readonly-mount") @@ -162,6 +165,11 @@ def test_03_current_spec_is_exact(self) -> None: ("capture", "classifier", "profile", "reprofile", "version-evidence"), ) + previous = engine.get_version_spec("1.1.27", MANIFEST_PATH) + self.assertEqual(previous.support_tier, "previous") + self.assertEqual(previous.allowed_operations, ("capture", "profile", "version-evidence")) + self.assertEqual(previous.expected_stdout, b"1.1.27\n") + previous = engine.get_version_spec("1.1.26", MANIFEST_PATH) self.assertEqual(previous.support_tier, "previous") self.assertEqual(previous.allowed_operations, ("capture", "profile", "version-evidence")) @@ -210,7 +218,7 @@ def test_08_operation_constants_match_reprofile_adapter(self) -> None: def test_09_data_only_new_version_self_heals_all_bindings(self) -> None: data = json.loads(MANIFEST_PATH.read_text(encoding="utf-8")) - new = copy.deepcopy(data["versions"]["1.1.27"]) + new = copy.deepcopy(data["versions"]["1.2.2"]) new.update({ "version": "1.1.28", "expected_stdout": "1.1.28\n", "source_sha256": "1" * 64, "source_size": 180_000_000, @@ -368,7 +376,7 @@ def test_09e_support_policy_is_closed_and_digest_bound(self) -> None: def test_10_exact_activation_binding_passes(self) -> None: binding = json.loads((ROOT / "compat/agy-models-inventory-binding.json").read_text(encoding="utf-8")) - engine.validate_activation_binding(binding, engine.get_version_spec("1.1.27")) + engine.validate_activation_binding(binding, engine.get_version_spec("1.2.2")) def test_11_activation_drift_fails_closed(self) -> None: binding = json.loads((ROOT / "compat/agy-models-inventory-binding.json").read_text(encoding="utf-8")) @@ -376,7 +384,7 @@ def test_11_activation_drift_fails_closed(self) -> None: changed = copy.deepcopy(binding) changed[key] = "0" * 64 with self.assertRaises(engine.EngineError): - engine.validate_activation_binding(changed, engine.get_version_spec("1.1.27")) + engine.validate_activation_binding(changed, engine.get_version_spec("1.2.2")) def test_12_stale_digest_fails_closed(self) -> None: with tempfile.TemporaryDirectory() as directory: @@ -410,7 +418,7 @@ def test_15_unknown_version_and_operation_fail_closed(self) -> None: with self.assertRaises(engine.EngineError): engine.get_version_spec("9.9.9") with self.assertRaises(engine.EngineError): - engine.operation_constants(engine.get_version_spec("1.1.27"), "unknown") + engine.operation_constants(engine.get_version_spec("1.2.2"), "unknown") def test_16_reprofile_transition_accepts_only_nlink_drift(self) -> None: current = os.stat_result((stat.S_IFDIR | 0o700, 3, 2, 9, os.getuid(), 4, 0, 0, 0, 0)) @@ -470,12 +478,12 @@ def test_19_engine_cli_audits_bound_manifest(self) -> None: stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=False, ) self.assertEqual(result.returncode, 0, result.stderr) - self.assertEqual(result.stdout, b"manifest valid: 6 versions loaded\n") + self.assertEqual(result.stdout, b"manifest valid: 7 versions loaded\n") def test_20_test_is_read_only_for_production_artifacts(self) -> None: paths = [MANIFEST_PATH, ENGINE_PATH, ROOT / "scripts/version_manifest_capture_runner.py"] before = {path: hashlib.sha256(path.read_bytes()).hexdigest() for path in paths} - engine.get_version_spec("1.1.27") + engine.get_version_spec("1.2.2") after = {path: hashlib.sha256(path.read_bytes()).hexdigest() for path in paths} self.assertEqual(before, after) diff --git a/tests/test-workflow-integration.py b/tests/test-workflow-integration.py index 0b78740..4591089 100644 --- a/tests/test-workflow-integration.py +++ b/tests/test-workflow-integration.py @@ -116,7 +116,7 @@ def _write_fake_agy(self) -> None: with calls.open("a", encoding="utf-8") as handle: handle.write(json.dumps({{"kind": kind, "argv": args}}, separators=(",", ":")) + "\\n") if args == ["--version"]: - print("1.1.27") + print("1.2.2") raise SystemExit(0) if args == ["--help"]: sys.stderr.write({AGY_HELP!r}) From 5db9f2416c7bc53fbf4a7bf05e11d30c9fede01e Mon Sep 17 00:00:00 2001 From: cyurekli Date: Sun, 13 Sep 2026 14:42:28 +0200 Subject: [PATCH 2/2] Pin native settings probe to the canonical CLT Python image --- tests/test-provider-containment.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test-provider-containment.py b/tests/test-provider-containment.py index 9de5e8c..fa0bc3f 100644 --- a/tests/test-provider-containment.py +++ b/tests/test-provider-containment.py @@ -648,8 +648,8 @@ def provider_settings_leaf_is_immutable_but_home_state_stays_writable() -> bool: ) script.chmod(0o600) prepared = prepare( - job, stage, "/usr/bin/python3", - ["/usr/bin/python3", "-I", "-S", "-B", str(script)], + job, stage, CLT_PYTHON_EXECUTABLE, + [CLT_PYTHON_EXECUTABLE, "-I", "-S", "-B", str(script)], {}, allow_keychain=True, ) confirmed = run_confirmed(prepared)