diff --git a/.ban-nondeterminism-allowlist b/.ban-nondeterminism-allowlist index 4f6187ca..b29cb2a2 100644 --- a/.ban-nondeterminism-allowlist +++ b/.ban-nondeterminism-allowlist @@ -15,6 +15,9 @@ std-fs crates/warp-core/tests/causal_wal_hardening_tests.rs WAL filesystem fixtu std-env crates/warp-core/tests/external_action_protocol_tests.rs external-action filesystem WAL fixture temp directory selection only. std-fs crates/warp-core/tests/external_action_protocol_tests.rs external-action filesystem WAL fixture I/O only. std-process crates/warp-core/tests/external_action_protocol_tests.rs external-action filesystem WAL fixture temp directory disambiguation only. +std-env crates/warp-core/tests/bounded_workspace_observation_tests.rs bounded-observation filesystem fixture temp directory selection only. +std-fs crates/warp-core/tests/bounded_workspace_observation_tests.rs bounded-observation filesystem fixture I/O only. +std-process crates/warp-core/tests/bounded_workspace_observation_tests.rs bounded-observation filesystem fixture temp directory disambiguation only. std-fs crates/warp-core/tests/external_consumer_contract_fixture_tests.rs installed-contract restart WAL fixture I/O only. std-fs crates/warp-core/tests/executable_operation_pipeline_tests.rs executable-operation restart WAL fixture I/O only. std-fs crates/warp-core/tests/provider_contract_admission_tests.rs provider invocation restart WAL fixture I/O only. diff --git a/CHANGELOG.md b/CHANGELOG.md index 1eb58a1e..f3aaf206 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,21 @@ ### Added +- Echo now independently admits compiler-produced Edict Core and Target IR for + one non-callable external request, verifies its exact source, target profile, + result, basis, and capability closure, independently corroborates the complete + Target IR request against Core, and evaluates argument-rooted runtime fields + under both compiler-declared and Echo-owned step, allocation, and output + ceilings. Admission also requires enough capacity for every terminal + settlement posture. The derived generic request invokes no provider. The + first operation-specific adapter observes an explicit relative-path set + through a capability-rooted directory after request and claim commits. It + refuses traversal, duplicate or unauthorized paths, symlinks, special files, + stale bases, malformed settlements, substituted success apertures, and + aggregate byte-budget overruns; revalidates registry authority before + settlement; retains canonical path/content bytes plus complete basis + evidence; admits `OutcomeUnknown` explicitly; and replays settled bytes + without reopening the workspace. - Echo now admits domain-neutral external actions through separate request-before-effect, bounded claim, and settlement-before-resumption WAL transactions (ADR 0026). Canonical requests bind worldline, operation, @@ -112,7 +127,7 @@ and 277,836 bytes / `279738ffeea40027eb493c15e873b87cf3aa0677a57f9f03fb824698e532322f`, respectively. The resulting 25-file package has provider identity - `sha256:fe1a1f1c05e88bb3caeadb2d77fb17a906a4819674c50d75b2dcaca0fb6058ec`. + `sha256:6685b7c629ae6955515d69158feb1d7db06af2193de7e5d13e1095101670b977`. This package build proves generic compiler/provider lowering and independent verification. The separate `run-edict-operation` witness now consumes that crossing through Echo-owned runtime execution. diff --git a/Cargo.lock b/Cargo.lock index 4f4d3d33..5867b94d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2209,6 +2209,8 @@ dependencies = [ "blake3", "bytemuck", "bytes", + "cap-fs-ext", + "cap-std", "echo-cas", "echo-dry-tests", "echo-edict-canonical", diff --git a/crates/echo-wesley-gen/assets/v1/edict-provider/package/v1/generated/evidence/provenance.provider-generation.json b/crates/echo-wesley-gen/assets/v1/edict-provider/package/v1/generated/evidence/provenance.provider-generation.json index 632d8807..c4072d2d 100644 --- a/crates/echo-wesley-gen/assets/v1/edict-provider/package/v1/generated/evidence/provenance.provider-generation.json +++ b/crates/echo-wesley-gen/assets/v1/edict-provider/package/v1/generated/evidence/provenance.provider-generation.json @@ -1 +1 @@ -{"apiVersion":"wesley.generation-provenance-manifest/v1","contractVersions":{"generatorAbi":"wesley.extension-generator/v1","inputSchema":"wesley.extension-generation-input/v1","provenanceSchema":"wesley.generation-provenance-manifest/v1"},"emittedArtifacts":[{"coordinate":"echo.dpo-authority-facts@1","digest":"sha256:e2a6a539a7156296c18ff1bc02f80dcabf62b42bb4314d0737527869e43986d6"},{"coordinate":"echo.dpo-lawpack-authority-facts@1","digest":"sha256:ce1af559551f8b8242c4b290b620c5a28323b9e43f8bc6bb161a191001b6f4b9"},{"coordinate":"echo.dpo-lawpack@1","digest":"sha256:8c570362671a0b1cd1a992d8210e4a90ffd153ad06dcdf1e0cb36033cc971e2c"},{"coordinate":"echo.dpo.registration/v1","digest":"sha256:4ef3aaad0d2131ba7129a3e2ae178d10c44f7e9b062af2c1fe211de781462f34"},{"coordinate":"echo.dpo@1","digest":"sha256:1b105d1b1f6cdf5fecdef98b7adeb238525047d43581fe9fd8c44fd213e1788e"},{"coordinate":"echo.provider-artifacts.cddl@1","digest":"sha256:730ce72f6658fe8cc9302221c32ad5992492e190674ddd69c1b164749d2bb669"}],"generationInputDigest":"sha256:57f5f67172544014af9c92e658301b9fcec69fd77fba9d251b258b59e3591380","generator":{"coordinate":"echo-wesley-gen.provider-artifact-generator@1","digest":"sha256:434467463d311ae5df7b3f778b75cb19113ec31ec2b82e572316c2f788f35346","version":"0.1.0"},"settingsDigest":"sha256:0f708e76898de6fdb8186352e81d0f5c445adf94bb6c7de9204952d9fe913d4a","sourceArtifacts":[{"coordinate":"echo.semantic-schema@1","digest":"sha256:322d9eb5eb8273108eaa602f2eb58ba591efcf829fecb10bb705717efae18852"},{"coordinate":"edict.provider-contract-pack.cddl@1","digest":"sha256:ff5405708185e0bcf33dac263000fcb772c79e21145ad69da7cad0692e1a9552"},{"coordinate":"edict.provider-contract-pack.manifest@1","digest":"sha256:e31d0452bf9d8ba84eaa1ce8c09fa1c25f740decd0ab08cd9c2fde533bdef1fa"}]} \ No newline at end of file +{"apiVersion":"wesley.generation-provenance-manifest/v1","contractVersions":{"generatorAbi":"wesley.extension-generator/v1","inputSchema":"wesley.extension-generation-input/v1","provenanceSchema":"wesley.generation-provenance-manifest/v1"},"emittedArtifacts":[{"coordinate":"echo.dpo-authority-facts@1","digest":"sha256:e2a6a539a7156296c18ff1bc02f80dcabf62b42bb4314d0737527869e43986d6"},{"coordinate":"echo.dpo-lawpack-authority-facts@1","digest":"sha256:ce1af559551f8b8242c4b290b620c5a28323b9e43f8bc6bb161a191001b6f4b9"},{"coordinate":"echo.dpo-lawpack@1","digest":"sha256:8c570362671a0b1cd1a992d8210e4a90ffd153ad06dcdf1e0cb36033cc971e2c"},{"coordinate":"echo.dpo.registration/v1","digest":"sha256:4ef3aaad0d2131ba7129a3e2ae178d10c44f7e9b062af2c1fe211de781462f34"},{"coordinate":"echo.dpo@1","digest":"sha256:1b105d1b1f6cdf5fecdef98b7adeb238525047d43581fe9fd8c44fd213e1788e"},{"coordinate":"echo.provider-artifacts.cddl@1","digest":"sha256:730ce72f6658fe8cc9302221c32ad5992492e190674ddd69c1b164749d2bb669"}],"generationInputDigest":"sha256:57f5f67172544014af9c92e658301b9fcec69fd77fba9d251b258b59e3591380","generator":{"coordinate":"echo-wesley-gen.provider-artifact-generator@1","digest":"sha256:48e583f8c29af45bb89d0e4f883b4fc478f7ec65f887612b865ebd4e1895b0a8","version":"0.1.0"},"settingsDigest":"sha256:0f708e76898de6fdb8186352e81d0f5c445adf94bb6c7de9204952d9fe913d4a","sourceArtifacts":[{"coordinate":"echo.semantic-schema@1","digest":"sha256:322d9eb5eb8273108eaa602f2eb58ba591efcf829fecb10bb705717efae18852"},{"coordinate":"edict.provider-contract-pack.cddl@1","digest":"sha256:ff5405708185e0bcf33dac263000fcb772c79e21145ad69da7cad0692e1a9552"},{"coordinate":"edict.provider-contract-pack.manifest@1","digest":"sha256:e31d0452bf9d8ba84eaa1ce8c09fa1c25f740decd0ab08cd9c2fde533bdef1fa"}]} \ No newline at end of file diff --git a/crates/echo-wesley-gen/assets/v1/edict-provider/package/v1/generated/evidence/review.provider-generation.json b/crates/echo-wesley-gen/assets/v1/edict-provider/package/v1/generated/evidence/review.provider-generation.json index 0ba69ca3..1179388c 100644 --- a/crates/echo-wesley-gen/assets/v1/edict-provider/package/v1/generated/evidence/review.provider-generation.json +++ b/crates/echo-wesley-gen/assets/v1/edict-provider/package/v1/generated/evidence/review.provider-generation.json @@ -1 +1 @@ -{"apiVersion":"wesley.generation-review/v1","authoritative":false,"emittedArtifacts":[{"coordinate":"echo.dpo-authority-facts@1","digest":"sha256:e2a6a539a7156296c18ff1bc02f80dcabf62b42bb4314d0737527869e43986d6"},{"coordinate":"echo.dpo-lawpack-authority-facts@1","digest":"sha256:ce1af559551f8b8242c4b290b620c5a28323b9e43f8bc6bb161a191001b6f4b9"},{"coordinate":"echo.dpo-lawpack@1","digest":"sha256:8c570362671a0b1cd1a992d8210e4a90ffd153ad06dcdf1e0cb36033cc971e2c"},{"coordinate":"echo.dpo.registration/v1","digest":"sha256:4ef3aaad0d2131ba7129a3e2ae178d10c44f7e9b062af2c1fe211de781462f34"},{"coordinate":"echo.dpo@1","digest":"sha256:1b105d1b1f6cdf5fecdef98b7adeb238525047d43581fe9fd8c44fd213e1788e"},{"coordinate":"echo.provider-artifacts.cddl@1","digest":"sha256:730ce72f6658fe8cc9302221c32ad5992492e190674ddd69c1b164749d2bb669"}],"generationInputDigest":"sha256:57f5f67172544014af9c92e658301b9fcec69fd77fba9d251b258b59e3591380","generator":{"coordinate":"echo-wesley-gen.provider-artifact-generator@1","digest":"sha256:434467463d311ae5df7b3f778b75cb19113ec31ec2b82e572316c2f788f35346","version":"0.1.0"},"projectionRoles":["authority-facts.echo-dpo","authority-facts.echo-lawpack","generated-artifact-profile.echo-dpo-registration","lawpack.echo-dpo","schema.echo-provider-artifacts","target-profile.echo-dpo"],"provenanceManifestDigest":"sha256:998426658fbcbd44bff4f28d0d4c82df7aaf05bdbcdb736db9cf481a1948ec82","sourceArtifacts":[{"coordinate":"echo.semantic-schema@1","digest":"sha256:322d9eb5eb8273108eaa602f2eb58ba591efcf829fecb10bb705717efae18852"},{"coordinate":"edict.provider-contract-pack.cddl@1","digest":"sha256:ff5405708185e0bcf33dac263000fcb772c79e21145ad69da7cad0692e1a9552"},{"coordinate":"edict.provider-contract-pack.manifest@1","digest":"sha256:e31d0452bf9d8ba84eaa1ce8c09fa1c25f740decd0ab08cd9c2fde533bdef1fa"}]} \ No newline at end of file +{"apiVersion":"wesley.generation-review/v1","authoritative":false,"emittedArtifacts":[{"coordinate":"echo.dpo-authority-facts@1","digest":"sha256:e2a6a539a7156296c18ff1bc02f80dcabf62b42bb4314d0737527869e43986d6"},{"coordinate":"echo.dpo-lawpack-authority-facts@1","digest":"sha256:ce1af559551f8b8242c4b290b620c5a28323b9e43f8bc6bb161a191001b6f4b9"},{"coordinate":"echo.dpo-lawpack@1","digest":"sha256:8c570362671a0b1cd1a992d8210e4a90ffd153ad06dcdf1e0cb36033cc971e2c"},{"coordinate":"echo.dpo.registration/v1","digest":"sha256:4ef3aaad0d2131ba7129a3e2ae178d10c44f7e9b062af2c1fe211de781462f34"},{"coordinate":"echo.dpo@1","digest":"sha256:1b105d1b1f6cdf5fecdef98b7adeb238525047d43581fe9fd8c44fd213e1788e"},{"coordinate":"echo.provider-artifacts.cddl@1","digest":"sha256:730ce72f6658fe8cc9302221c32ad5992492e190674ddd69c1b164749d2bb669"}],"generationInputDigest":"sha256:57f5f67172544014af9c92e658301b9fcec69fd77fba9d251b258b59e3591380","generator":{"coordinate":"echo-wesley-gen.provider-artifact-generator@1","digest":"sha256:48e583f8c29af45bb89d0e4f883b4fc478f7ec65f887612b865ebd4e1895b0a8","version":"0.1.0"},"projectionRoles":["authority-facts.echo-dpo","authority-facts.echo-lawpack","generated-artifact-profile.echo-dpo-registration","lawpack.echo-dpo","schema.echo-provider-artifacts","target-profile.echo-dpo"],"provenanceManifestDigest":"sha256:45670aa10682d15f8349865c7e94d3938b8229f83dab91cb4b7f4865b035d656","sourceArtifacts":[{"coordinate":"echo.semantic-schema@1","digest":"sha256:322d9eb5eb8273108eaa602f2eb58ba591efcf829fecb10bb705717efae18852"},{"coordinate":"edict.provider-contract-pack.cddl@1","digest":"sha256:ff5405708185e0bcf33dac263000fcb772c79e21145ad69da7cad0692e1a9552"},{"coordinate":"edict.provider-contract-pack.manifest@1","digest":"sha256:e31d0452bf9d8ba84eaa1ce8c09fa1c25f740decd0ab08cd9c2fde533bdef1fa"}]} \ No newline at end of file diff --git a/crates/echo-wesley-gen/assets/v1/edict-provider/package/v1/provider-manifest.echo.json b/crates/echo-wesley-gen/assets/v1/edict-provider/package/v1/provider-manifest.echo.json index 729f3463..d8227f27 100644 --- a/crates/echo-wesley-gen/assets/v1/edict-provider/package/v1/provider-manifest.echo.json +++ b/crates/echo-wesley-gen/assets/v1/edict-provider/package/v1/provider-manifest.echo.json @@ -3,7 +3,7 @@ "providerAbi": "edict:target-provider@1.0.0", "provider": { "coordinate": "echo.edict-provider@1", - "digest": "sha256:fe1a1f1c05e88bb3caeadb2d77fb17a906a4819674c50d75b2dcaca0fb6058ec" + "digest": "sha256:6685b7c629ae6955515d69158feb1d7db06af2193de7e5d13e1095101670b977" }, "artifacts": [ { @@ -21,7 +21,7 @@ }, "generator": { "coordinate": "echo-wesley-gen.provider-artifact-generator@1", - "digest": "sha256:434467463d311ae5df7b3f778b75cb19113ec31ec2b82e572316c2f788f35346" + "digest": "sha256:48e583f8c29af45bb89d0e4f883b4fc478f7ec65f887612b865ebd4e1895b0a8" } } }, @@ -40,7 +40,7 @@ }, "generator": { "coordinate": "echo-wesley-gen.provider-artifact-generator@1", - "digest": "sha256:434467463d311ae5df7b3f778b75cb19113ec31ec2b82e572316c2f788f35346" + "digest": "sha256:48e583f8c29af45bb89d0e4f883b4fc478f7ec65f887612b865ebd4e1895b0a8" } } }, @@ -59,7 +59,7 @@ }, "generator": { "coordinate": "echo-wesley-gen.provider-artifact-generator@1", - "digest": "sha256:434467463d311ae5df7b3f778b75cb19113ec31ec2b82e572316c2f788f35346" + "digest": "sha256:48e583f8c29af45bb89d0e4f883b4fc478f7ec65f887612b865ebd4e1895b0a8" } } }, @@ -78,7 +78,7 @@ }, "generator": { "coordinate": "echo-wesley-gen.provider-artifact-generator@1", - "digest": "sha256:434467463d311ae5df7b3f778b75cb19113ec31ec2b82e572316c2f788f35346" + "digest": "sha256:48e583f8c29af45bb89d0e4f883b4fc478f7ec65f887612b865ebd4e1895b0a8" } } }, @@ -102,7 +102,7 @@ "artifactKind": "generationProvenance", "resource": { "coordinate": "echo.edict-provider-generation-provenance@1", - "digest": "sha256:720076fdd6462ab5500e944408199a141d07eca8e4e73d542e01e7fcdc715ac4" + "digest": "sha256:42f43a9bd874fe7808f3edce7491899ec21c991d33526e5802e6a3ad1edd2020" }, "source": { "kind": "generated", @@ -112,7 +112,7 @@ }, "generator": { "coordinate": "echo-wesley-gen.provider-artifact-generator@1", - "digest": "sha256:434467463d311ae5df7b3f778b75cb19113ec31ec2b82e572316c2f788f35346" + "digest": "sha256:48e583f8c29af45bb89d0e4f883b4fc478f7ec65f887612b865ebd4e1895b0a8" } } }, @@ -121,7 +121,7 @@ "artifactKind": "reviewArtifact", "resource": { "coordinate": "echo.edict-provider-generation-review@1", - "digest": "sha256:592af76dab9f354a4afb663365990e8a14bf07c1721cdc2242319ad457ee5e2b" + "digest": "sha256:1459a631c3ca083de2ccfa3e73eded405b8cdf9b969a0ec6519c1e0484e34877" }, "source": { "kind": "generated", @@ -131,7 +131,7 @@ }, "generator": { "coordinate": "echo-wesley-gen.provider-artifact-generator@1", - "digest": "sha256:434467463d311ae5df7b3f778b75cb19113ec31ec2b82e572316c2f788f35346" + "digest": "sha256:48e583f8c29af45bb89d0e4f883b4fc478f7ec65f887612b865ebd4e1895b0a8" } } }, @@ -150,7 +150,7 @@ }, "generator": { "coordinate": "echo-wesley-gen.provider-artifact-generator@1", - "digest": "sha256:434467463d311ae5df7b3f778b75cb19113ec31ec2b82e572316c2f788f35346" + "digest": "sha256:48e583f8c29af45bb89d0e4f883b4fc478f7ec65f887612b865ebd4e1895b0a8" } } }, @@ -169,7 +169,7 @@ }, "generator": { "coordinate": "echo-wesley-gen.provider-artifact-generator@1", - "digest": "sha256:434467463d311ae5df7b3f778b75cb19113ec31ec2b82e572316c2f788f35346" + "digest": "sha256:48e583f8c29af45bb89d0e4f883b4fc478f7ec65f887612b865ebd4e1895b0a8" } } }, diff --git a/crates/echo-wesley-gen/assets/v1/repository/Cargo.lock.source b/crates/echo-wesley-gen/assets/v1/repository/Cargo.lock.source index 4f4d3d33..5867b94d 100644 --- a/crates/echo-wesley-gen/assets/v1/repository/Cargo.lock.source +++ b/crates/echo-wesley-gen/assets/v1/repository/Cargo.lock.source @@ -2209,6 +2209,8 @@ dependencies = [ "blake3", "bytemuck", "bytes", + "cap-fs-ext", + "cap-std", "echo-cas", "echo-dry-tests", "echo-edict-canonical", diff --git a/crates/echo-wesley-gen/tests/provider_package.rs b/crates/echo-wesley-gen/tests/provider_package.rs index 393b25fc..5ba2e797 100644 --- a/crates/echo-wesley-gen/tests/provider_package.rs +++ b/crates/echo-wesley-gen/tests/provider_package.rs @@ -65,7 +65,7 @@ const SEMANTIC_DIGEST: &str = const RELEASE_DIGEST: &str = "sha256:c39449495281b51f978468d08c21e93bcfa423176063b41675da61e4674b0066"; const PACKAGE_ARTIFACT_SHA256: &str = - "fe1a1f1c05e88bb3caeadb2d77fb17a906a4819674c50d75b2dcaca0fb6058ec"; + "6685b7c629ae6955515d69158feb1d7db06af2193de7e5d13e1095101670b977"; const OTHER_PACKAGE_ARTIFACT_SHA256: &str = "eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee"; diff --git a/crates/echo-wesley-gen/tests/provider_package_corpus.rs b/crates/echo-wesley-gen/tests/provider_package_corpus.rs index 188a8e5e..f7a9df02 100644 --- a/crates/echo-wesley-gen/tests/provider_package_corpus.rs +++ b/crates/echo-wesley-gen/tests/provider_package_corpus.rs @@ -11,9 +11,9 @@ use std::sync::atomic::{AtomicU64, Ordering}; use sha2::{Digest as _, Sha256}; const EXPECTED_PROVIDER_DIGEST: &str = - "sha256:fe1a1f1c05e88bb3caeadb2d77fb17a906a4819674c50d75b2dcaca0fb6058ec"; + "sha256:6685b7c629ae6955515d69158feb1d7db06af2193de7e5d13e1095101670b977"; const EXPECTED_MANIFEST_RAW_SHA256: &str = - "49bf01c6c2a7c5295d011e58b9b2a6e0c0436f067a1eb94144dbe27cdad3fef2"; + "3a31b133321f9611d2b90ecf99162aa05027f4d6d600740550259dd2596722e9"; const PACKAGE_PATHS: [&str; 25] = [ "components/lowerer.echo-dpo.component.wasm", diff --git a/crates/warp-core/Cargo.toml b/crates/warp-core/Cargo.toml index 2cda23ab..49fc5fa8 100644 --- a/crates/warp-core/Cargo.toml +++ b/crates/warp-core/Cargo.toml @@ -27,6 +27,10 @@ echo-registry-api = { workspace = true } echo-edict-canonical = { workspace = true } warp-math = { workspace = true } +[target.'cfg(not(target_arch = "wasm32"))'.dependencies] +cap-fs-ext = "=4.0.2" +cap-std = "=4.0.2" + [dev-dependencies] proptest = { version = "1.5" } echo-dry-tests = { workspace = true } diff --git a/crates/warp-core/src/external_action_adapter.rs b/crates/warp-core/src/external_action_adapter.rs new file mode 100644 index 00000000..f5167460 --- /dev/null +++ b/crates/warp-core/src/external_action_adapter.rs @@ -0,0 +1,1519 @@ +// SPDX-License-Identifier: Apache-2.0 +// © James Ross Ω FLYING•ROBOTS +//! Compiler-authored external requests and capability-rooted observation. +//! +//! This module is a trusted-host boundary. Edict contributes canonical request +//! data but receives no filesystem authority. The adapter accepts work only +//! through an [`ExternalActionClaimGrantV1`], which is constructible only after +//! Echo durably commits the corresponding request and claim. + +use std::collections::BTreeSet; +use std::io::Read; +use std::path::{Component, Path}; + +use cap_fs_ext::{DirExt, FollowSymlinks, OpenOptionsFollowExt, OpenOptionsSyncExt}; +use cap_std::{ + ambient_authority, + fs::{Dir, OpenOptions}, +}; +use echo_edict_canonical::{ + decode_canonical_cbor_v1, digest_canonical_value_v1, encode_canonical_cbor_v1, + CanonicalValueErrorKind, CanonicalValueV1, +}; +use thiserror::Error; + +use crate::causal_wal::WalStorePort; +use crate::external_action::{ + admit_external_action_settlement, AdmittedExternalActionSettlementV1, + ExternalActionAdapterBindingV1, ExternalActionAdapterIdV1, ExternalActionBudgetV1, + ExternalActionClaimGrantV1, ExternalActionCoordinatorV1, ExternalActionOperationIdV1, + ExternalActionProtocolErrorV1, ExternalActionRequestV1, ExternalActionSettlementCandidateV1, + ExternalActionSettlementKindV1, ExternalActionTransactionContextV1, +}; +use crate::{Hash, WorldlineId}; + +const CORE_DIGEST_DOMAIN: &str = "edict.core.module/v1"; +const TARGET_IR_DIGEST_DOMAIN: &str = "edict.target-ir.artifact/v1"; +const ECHO_TARGET_IR_DOMAIN: &str = "echo.span-ir/v1"; +const ECHO_TARGET_PROFILE_COORDINATE: &str = "echo.dpo@1"; +const EXTERNAL_REQUEST_OPERATION_PROFILE: &str = "continuum.profile.read-only/v1"; +const RESOURCE_ID_DOMAIN: &[u8] = b"echo.external-action.resource-id/v1"; +const TARGET_OPERATION_ID_DOMAIN: &[u8] = b"echo.external-action.target-operation-id/v1"; +const INPUT_DIGEST_DOMAIN: &[u8] = b"echo.external-action.input/v1"; +const OBSERVATION_INPUT_KIND: &str = "boundedWorkspaceObservationInput"; +const OBSERVATION_SETTLEMENT_KIND: &str = "boundedWorkspaceObservationSettlement"; +const OBSERVATION_BASIS_DOMAIN: &[u8] = b"echo.bounded-observation.basis/v1"; +const OBSERVATION_SCHEMA_EVIDENCE_DOMAIN: &[u8] = b"echo.bounded-observation.schema-evidence/v1"; +const OBSERVATION_REFUSAL_EVIDENCE_DOMAIN: &[u8] = b"echo.bounded-observation.refusal-evidence/v1"; +const MAX_CORE_EVALUATION_STEPS_V1: u64 = 4_096; +const MAX_CORE_EVALUATION_ALLOCATED_BYTES_V1: u64 = 4 * 1_024 * 1_024; +const MAX_CORE_EVALUATION_OUTPUT_BYTES_V1: u64 = 1_024 * 1_024; + +/// One canonical Edict request admitted from exact Core and Target IR bytes. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct AdmittedEdictExternalActionRequestV1 { + request: ExternalActionRequestV1, + canonical_operation_input: Vec, + source_core_digest: String, + target_ir_digest: String, + operation_coordinate: String, +} + +impl AdmittedEdictExternalActionRequestV1 { + /// Returns the generic Echo request derived from compiler-owned data. + #[must_use] + pub const fn request(&self) -> ExternalActionRequestV1 { + self.request + } + + /// Returns the exact canonical operation input resolved from application input. + #[must_use] + pub fn canonical_operation_input(&self) -> &[u8] { + &self.canonical_operation_input + } + + /// Returns the independently recomputed reviewed Core digest. + #[must_use] + pub fn source_core_digest(&self) -> &str { + &self.source_core_digest + } + + /// Returns the independently recomputed reviewed Target IR digest. + #[must_use] + pub fn target_ir_digest(&self) -> &str { + &self.target_ir_digest + } + + /// Returns the compiler-declared operation coordinate for diagnostics. + #[must_use] + pub fn operation_coordinate(&self) -> &str { + &self.operation_coordinate + } +} + +/// Independent compiler-artifact admission failures. +#[derive(Debug, Error, PartialEq, Eq)] +pub enum EdictExternalActionAdmissionErrorV1 { + /// The canonical decoder rejected one compiler artifact or runtime value. + #[error("canonical Edict value was rejected: {0:?}")] + Canonical(CanonicalValueErrorKind), + /// The Target IR shape is not the exact supported request-only contract. + #[error("Edict Target IR artifact shape is unsupported")] + ArtifactShape, + /// The requested intent is absent. + #[error("Edict Target IR intent is absent")] + MissingIntent, + /// The intent does not contain exactly one external request. + #[error("Edict Target IR intent must contain exactly one external request")] + RequestCardinality, + /// A request-only artifact also contains callable target steps. + #[error("Edict external-request Target IR contains callable target steps")] + CallableStepsPresent, + /// The Target IR omitted its exact source/capability closure. + #[error("Edict Target IR semantic closure is absent")] + MissingSemanticClosure, + /// The supplied Core bytes do not match the Target IR source-Core binding. + #[error("Edict source Core digest does not match Target IR")] + CoreDigestMismatch, + /// The supplied Target IR request is not the independent projection of Core. + #[error("Edict Target IR request is not derived from the supplied Core")] + TargetDerivationMismatch, + /// The request operation is absent or substituted in the capability closure. + #[error("Edict external request is outside the exact capability closure")] + CapabilityClosureMismatch, + /// The supported runtime expression subset could not resolve a request field. + #[error("Edict external request uses an unsupported runtime expression")] + UnsupportedExpression, + /// A runtime value has the wrong type or bounds. + #[error("Edict external request runtime value is invalid")] + InvalidRuntimeValue, + /// Runtime expression evaluation exceeded the declared or host-owned budget. + #[error("Edict external request expression evaluation exceeded its budget")] + EvaluationBudgetExceeded, + /// A reviewed digest does not use the required lowercase SHA-256 rendering. + #[error("Edict external request contains an invalid reviewed digest")] + InvalidDigest, + /// The generic request protocol rejected the compiler-derived request. + #[error(transparent)] + Protocol(#[from] ExternalActionProtocolErrorV1), +} + +/// Admits one request-only Edict Target IR artifact without invoking a provider. +#[allow(clippy::too_many_arguments)] +pub fn admit_edict_external_action_request_v1( + worldline_id: WorldlineId, + canonical_core_bytes: &[u8], + canonical_target_ir_bytes: &[u8], + intent_name: &str, + canonical_application_input_bytes: &[u8], +) -> Result { + let core = decode_canonical_cbor_v1(canonical_core_bytes) + .map_err(|error| EdictExternalActionAdmissionErrorV1::Canonical(error.kind()))?; + let target_ir = decode_canonical_cbor_v1(canonical_target_ir_bytes) + .map_err(|error| EdictExternalActionAdmissionErrorV1::Canonical(error.kind()))?; + let application_input = decode_canonical_cbor_v1(canonical_application_input_bytes) + .map_err(|error| EdictExternalActionAdmissionErrorV1::Canonical(error.kind()))?; + + let source_core_digest = digest_canonical_value_v1(CORE_DIGEST_DOMAIN, &core) + .map_err(|error| EdictExternalActionAdmissionErrorV1::Canonical(error.kind()))?; + let target_ir_digest = digest_canonical_value_v1(TARGET_IR_DIGEST_DOMAIN, &target_ir) + .map_err(|error| EdictExternalActionAdmissionErrorV1::Canonical(error.kind()))?; + + let core_map = expect_map(&core)?; + require_text_field(core_map, "apiVersion", "edict.core/v1")?; + let core_coordinate = require_nonempty_text(core_map, "coordinate")?; + + let target_map = expect_map(&target_ir)?; + require_text_field(target_map, "kind", "targetIrArtifact")?; + require_text_field(target_map, "domain", ECHO_TARGET_IR_DOMAIN)?; + let source_core_coordinate = require_nonempty_text(target_map, "sourceCoreCoordinate")?; + let target_profile = parse_resource(require_field(target_map, "targetProfile")?)?; + if target_profile.coordinate != ECHO_TARGET_PROFILE_COORDINATE { + return Err(EdictExternalActionAdmissionErrorV1::ArtifactShape); + } + + let closure_map = expect_map( + require_field(target_map, "semanticClosure") + .map_err(|_| EdictExternalActionAdmissionErrorV1::MissingSemanticClosure)?, + )?; + let closure_source = parse_resource(require_field(closure_map, "sourceCore")?)?; + if source_core_coordinate != core_coordinate + || closure_source.coordinate != core_coordinate + || closure_source.review_digest() != source_core_digest + { + return Err(EdictExternalActionAdmissionErrorV1::CoreDigestMismatch); + } + let lawpacks = expect_array( + require_field(closure_map, "lawpacks") + .map_err(|_| EdictExternalActionAdmissionErrorV1::MissingSemanticClosure)?, + )? + .iter() + .map(parse_resource) + .collect::, _>>()?; + let capabilities = expect_array( + require_field(closure_map, "capabilities") + .map_err(|_| EdictExternalActionAdmissionErrorV1::CapabilityClosureMismatch)?, + )? + .iter() + .map(parse_resource) + .collect::, _>>()?; + + let intents = expect_map(require_field(target_map, "intents")?)?; + let intent = map_field(intents, intent_name) + .ok_or(EdictExternalActionAdmissionErrorV1::MissingIntent)?; + let intent_map = expect_map(intent)?; + require_text_field( + intent_map, + "operationProfile", + EXTERNAL_REQUEST_OPERATION_PROFILE, + )?; + if !expect_array(require_field(intent_map, "inputConstraints")?)?.is_empty() + || !expect_array(require_field(intent_map, "requirements")?)?.is_empty() + { + return Err(EdictExternalActionAdmissionErrorV1::ArtifactShape); + } + if !expect_array(require_field(intent_map, "steps")?)?.is_empty() { + return Err(EdictExternalActionAdmissionErrorV1::CallableStepsPresent); + } + let requests = expect_array(require_field(intent_map, "externalActionRequests")?)?; + let [request_value] = requests else { + return Err(EdictExternalActionAdmissionErrorV1::RequestCardinality); + }; + let request_map = expect_map(request_value)?; + require_nonempty_text(request_map, "id")?; + require_text_field(request_map, "state", "awaitingSettlement")?; + require_text_field(request_map, "settlementAdmission", "schemaRequired")?; + let binding = parse_local_ref(require_field(request_map, "binding")?)?; + let result = expect_map(require_field(intent_map, "result")?)?; + require_text_field(result, "kind", "local")?; + let result_reference = parse_local_ref(require_field(result, "ref")?)?; + if result_reference != binding { + return Err(EdictExternalActionAdmissionErrorV1::ArtifactShape); + } + if require_field(intent_map, "basis")? != require_field(request_map, "basis")? { + return Err(EdictExternalActionAdmissionErrorV1::ArtifactShape); + } + let operation = parse_resource(require_field(request_map, "operation")?)?; + if !capabilities + .iter() + .any(|capability| capability == &operation) + { + return Err(EdictExternalActionAdmissionErrorV1::CapabilityClosureMismatch); + } + verify_target_derivation( + core_map, + intent_name, + intent_map, + request_map, + &lawpacks, + &capabilities, + )?; + let input_schema = parse_resource(require_field(request_map, "inputSchema")?)?; + let settlement_schema = parse_resource(require_field(request_map, "settlementSchema")?)?; + let reconciliation_law = parse_resource(require_field(request_map, "reconciliationLaw")?)?; + let input_type = require_nonempty_text(request_map, "inputType")?; + let settlement_type = require_nonempty_text(request_map, "settlementType")?; + + let evaluation_budget = + parse_evaluation_budget(require_field(intent_map, "coreEvaluationBudget")?)?; + let mut evaluator = BoundedExpressionEvaluatorV1::new(&application_input, evaluation_budget)?; + let operation_input_value = evaluator.evaluate_root(require_field(request_map, "input")?)?; + let operation_input = expect_bytes(&operation_input_value)?.to_vec(); + if operation_input.len() > bytes_type_max(input_type)? { + return Err(EdictExternalActionAdmissionErrorV1::InvalidRuntimeValue); + } + let authority_scope_digest = + expect_hash(&evaluator.evaluate_root(require_field(request_map, "authorityScope")?)?)?; + let basis_digest = + expect_hash(&evaluator.evaluate_root(require_field(request_map, "basis")?)?)?; + let budget_map = expect_map(require_field(request_map, "budget")?)?; + let max_settlement_bytes = + expect_u64(&evaluator.evaluate_root(require_field(budget_map, "maxSettlementBytes")?)?)?; + let max_attempts = + expect_u32(&evaluator.evaluate_root(require_field(budget_map, "maxAttempts")?)?)?; + if max_settlement_bytes > u64::try_from(bytes_type_max(settlement_type)?).unwrap_or(u64::MAX) { + return Err(EdictExternalActionAdmissionErrorV1::InvalidRuntimeValue); + } + if max_settlement_bytes < minimum_terminal_settlement_bytes_v1(basis_digest)? { + return Err(EdictExternalActionAdmissionErrorV1::InvalidRuntimeValue); + } + + let request = ExternalActionRequestV1::new( + worldline_id, + ExternalActionOperationIdV1::from_hash(target_operation_identity( + &target_profile, + &operation, + )), + input_schema.digest, + settlement_schema.digest, + authority_scope_digest, + basis_digest, + ExternalActionBudgetV1 { + max_settlement_bytes, + max_attempts, + }, + input_identity(&operation_input), + resource_identity(&reconciliation_law), + )?; + Ok(AdmittedEdictExternalActionRequestV1 { + request, + canonical_operation_input: operation_input, + source_core_digest, + target_ir_digest, + operation_coordinate: operation.coordinate, + }) +} + +/// Runtime-owner profile binding a validator and adapter to compiler identities. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct BoundedWorkspaceObservationProfileV1 { + /// Exact compiler-declared operation family. + pub operation_id: ExternalActionOperationIdV1, + /// Exact compiler-declared input schema. + pub input_schema_digest: Hash, + /// Exact compiler-declared settlement schema. + pub settlement_schema_digest: Hash, + /// Exact compiler-declared reconciliation law. + pub reconciliation_law_digest: Hash, + /// Maximum authority scope delegated by the runtime owner. + pub authority_scope_digest: Hash, + /// Runtime-owned adapter identity. + pub adapter_id: ExternalActionAdapterIdV1, +} + +/// Bounded, read-only host adapter rooted in one capability directory. +pub struct BoundedWorkspaceObservationAdapterV1 { + root: Dir, + permitted_paths: BTreeSet, + profile: BoundedWorkspaceObservationProfileV1, +} + +impl BoundedWorkspaceObservationAdapterV1 { + /// Opens the configured root once and retains only directory-relative authority. + pub fn open( + root: &Path, + permitted_paths: impl IntoIterator, + profile: BoundedWorkspaceObservationProfileV1, + ) -> Result { + if profile.operation_id.as_hash() == [0; 32] + || profile.input_schema_digest == [0; 32] + || profile.settlement_schema_digest == [0; 32] + || profile.reconciliation_law_digest == [0; 32] + || profile.authority_scope_digest == [0; 32] + || profile.adapter_id.as_hash() == [0; 32] + { + return Err(BoundedWorkspaceObservationErrorV1::ProfileMismatch); + } + let permitted_paths = permitted_paths.into_iter().collect::>(); + for path in &permitted_paths { + validate_relative_path(path)?; + } + let root = Dir::open_ambient_dir(root, ambient_authority()) + .map_err(|_| BoundedWorkspaceObservationErrorV1::Io)?; + Ok(Self { + root, + permitted_paths, + profile, + }) + } + + /// Returns the runtime registry binding for this attenuated adapter. + #[must_use] + pub const fn adapter_binding(&self) -> ExternalActionAdapterBindingV1 { + ExternalActionAdapterBindingV1 { + adapter_id: self.profile.adapter_id, + operation_id: self.profile.operation_id, + authority_scope_digest: self.profile.authority_scope_digest, + } + } + + /// Performs one bounded observation after request and claim durability. + pub fn observe( + &self, + grant: &ExternalActionClaimGrantV1, + admitted: &AdmittedEdictExternalActionRequestV1, + ) -> Result { + self.validate_grant(grant, admitted)?; + let paths = match decode_observation_input(admitted.canonical_operation_input()) { + Ok(paths) => paths, + Err(BoundedWorkspaceObservationErrorV1::Canonical) => { + return self.refused_candidate(grant, "malformed-input", ""); + } + Err(error) => return Err(error), + }; + if paths.is_empty() { + return self.refused_candidate(grant, "empty-path-set", ""); + } + let mut unique_paths = BTreeSet::new(); + for path in &paths { + if validate_relative_path(path).is_err() { + return self.refused_candidate(grant, "invalid-path", path); + } + if !unique_paths.insert(path.as_str()) { + return self.refused_candidate(grant, "duplicate-path", path); + } + if !self.permitted_paths.contains(path) { + return self.refused_candidate(grant, "unauthorized-path", path); + } + } + + let mut observed = Vec::with_capacity(paths.len()); + let mut retained_input_bytes = 0_u64; + for path in paths { + let remaining = grant + .request() + .budget + .max_settlement_bytes + .saturating_sub(retained_input_bytes); + match self.read_regular_file(&path, remaining) { + Ok(bytes) => { + let byte_count = u64::try_from(bytes.len()).unwrap_or(u64::MAX); + if byte_count > remaining { + return self.refused_candidate(grant, "settlement-budget-exceeded", &path); + } + retained_input_bytes = retained_input_bytes.saturating_add(byte_count); + observed.push(ObservedFileV1 { path, bytes }); + } + Err(BoundedWorkspaceObservationErrorV1::SymlinkRefused) => { + return self.refused_candidate(grant, "symlink-refused", &path); + } + Err(BoundedWorkspaceObservationErrorV1::NotRegularFile) => { + return self.refused_candidate(grant, "not-regular-file", &path); + } + Err(BoundedWorkspaceObservationErrorV1::Io) => { + return self.failed_candidate(grant, "io-failure", &path); + } + Err(error) => return Err(error), + } + } + observed.sort_by(|left, right| left.path.cmp(&right.path)); + let observed_basis = bounded_workspace_observation_basis_v1( + observed + .iter() + .map(|file| (file.path.as_str(), file.bytes.as_slice())), + ); + if observed_basis != grant.request().basis_digest { + return self.refused_candidate_with_evidence(grant, "stale-basis", observed_basis); + } + let success = encode_observation_settlement( + "succeeded", + grant.request().basis_digest, + observed_basis, + &observed, + None, + )?; + if u64::try_from(success.len()).unwrap_or(u64::MAX) + > grant.request().budget.max_settlement_bytes + { + return self.refused_candidate(grant, "settlement-budget-exceeded", ""); + } + self.candidate( + grant, + ExternalActionSettlementKindV1::Succeeded, + success, + observed_basis, + ) + } + + /// Produces an explicit ambiguous settlement during reconciliation. + pub fn outcome_unknown( + &self, + grant: &ExternalActionClaimGrantV1, + admitted: &AdmittedEdictExternalActionRequestV1, + external_evidence_digest: Hash, + ) -> Result { + self.validate_grant(grant, admitted)?; + if external_evidence_digest == [0; 32] { + return Err(BoundedWorkspaceObservationErrorV1::SchemaAdmissionFailed); + } + let result = encode_observation_settlement( + "outcomeUnknown", + grant.request().basis_digest, + external_evidence_digest, + &[], + Some("outcome-unknown"), + )?; + self.candidate( + grant, + ExternalActionSettlementKindV1::OutcomeUnknown, + result, + external_evidence_digest, + ) + } + + /// Validates the operation-specific schema and durably admits the settlement. + pub fn admit_settlement( + &self, + store: &mut impl WalStorePort, + coordinator: &mut ExternalActionCoordinatorV1, + context: ExternalActionTransactionContextV1, + admitted: &AdmittedEdictExternalActionRequestV1, + grant: ExternalActionClaimGrantV1, + candidate: ExternalActionSettlementCandidateV1, + ) -> Result { + self.validate_grant(&grant, admitted)?; + self.validate_candidate(&grant, admitted, &candidate)?; + Ok(admit_external_action_settlement( + store, + coordinator, + context, + grant, + candidate, + )?) + } + + fn validate_grant( + &self, + grant: &ExternalActionClaimGrantV1, + admitted: &AdmittedEdictExternalActionRequestV1, + ) -> Result<(), BoundedWorkspaceObservationErrorV1> { + let request = grant.request(); + if request != admitted.request() + || request.input_digest != input_identity(admitted.canonical_operation_input()) + || grant.claim().adapter_id != self.profile.adapter_id + { + return Err(BoundedWorkspaceObservationErrorV1::GrantMismatch); + } + if request.operation_id != self.profile.operation_id + || request.input_schema_digest != self.profile.input_schema_digest + || request.settlement_schema_digest != self.profile.settlement_schema_digest + || request.reconciliation_law_digest != self.profile.reconciliation_law_digest + || request.authority_scope_digest != self.profile.authority_scope_digest + { + return Err(BoundedWorkspaceObservationErrorV1::ProfileMismatch); + } + Ok(()) + } + + fn read_regular_file( + &self, + path: &str, + max_bytes: u64, + ) -> Result, BoundedWorkspaceObservationErrorV1> { + let components = Path::new(path).components().collect::>(); + let Some((file_name, directory_components)) = components.split_last() else { + return Err(BoundedWorkspaceObservationErrorV1::InvalidPath); + }; + let Component::Normal(file_name) = file_name else { + return Err(BoundedWorkspaceObservationErrorV1::InvalidPath); + }; + let mut directory = self + .root + .try_clone() + .map_err(|_| BoundedWorkspaceObservationErrorV1::Io)?; + for component in directory_components { + let Component::Normal(component) = component else { + return Err(BoundedWorkspaceObservationErrorV1::InvalidPath); + }; + let metadata = directory + .symlink_metadata(component) + .map_err(|_| BoundedWorkspaceObservationErrorV1::Io)?; + if metadata.file_type().is_symlink() { + return Err(BoundedWorkspaceObservationErrorV1::SymlinkRefused); + } + directory = directory + .open_dir_nofollow(component) + .map_err(|_| BoundedWorkspaceObservationErrorV1::Io)?; + } + let metadata = directory + .symlink_metadata(file_name) + .map_err(|_| BoundedWorkspaceObservationErrorV1::Io)?; + if metadata.file_type().is_symlink() { + return Err(BoundedWorkspaceObservationErrorV1::SymlinkRefused); + } + if !metadata.is_file() { + return Err(BoundedWorkspaceObservationErrorV1::NotRegularFile); + } + let mut options = OpenOptions::new(); + options.read(true).follow(FollowSymlinks::No).nonblock(true); + let mut file = directory + .open_with(file_name, &options) + .map_err(|_| BoundedWorkspaceObservationErrorV1::Io)?; + let metadata = file + .metadata() + .map_err(|_| BoundedWorkspaceObservationErrorV1::Io)?; + if !metadata.is_file() { + return Err(BoundedWorkspaceObservationErrorV1::NotRegularFile); + } + let mut bytes = Vec::new(); + file.by_ref() + .take(max_bytes.saturating_add(1)) + .read_to_end(&mut bytes) + .map_err(|_| BoundedWorkspaceObservationErrorV1::Io)?; + Ok(bytes) + } + + fn refused_candidate( + &self, + grant: &ExternalActionClaimGrantV1, + code: &str, + detail: &str, + ) -> Result { + self.refused_candidate_with_evidence(grant, code, refusal_evidence(code, detail)) + } + + fn refused_candidate_with_evidence( + &self, + grant: &ExternalActionClaimGrantV1, + code: &str, + evidence: Hash, + ) -> Result { + let result = encode_observation_settlement( + "rejected", + grant.request().basis_digest, + evidence, + &[], + Some(code), + )?; + if u64::try_from(result.len()).unwrap_or(u64::MAX) + > grant.request().budget.max_settlement_bytes + { + return Err(BoundedWorkspaceObservationErrorV1::SettlementBudgetExceeded); + } + self.candidate( + grant, + ExternalActionSettlementKindV1::Rejected, + result, + evidence, + ) + } + + fn failed_candidate( + &self, + grant: &ExternalActionClaimGrantV1, + code: &str, + detail: &str, + ) -> Result { + let evidence = refusal_evidence(code, detail); + let result = encode_observation_settlement( + "failed", + grant.request().basis_digest, + evidence, + &[], + Some(code), + )?; + self.candidate( + grant, + ExternalActionSettlementKindV1::Failed, + result, + evidence, + ) + } + + fn candidate( + &self, + grant: &ExternalActionClaimGrantV1, + kind: ExternalActionSettlementKindV1, + result: Vec, + external_evidence_digest: Hash, + ) -> Result { + if u64::try_from(result.len()).unwrap_or(u64::MAX) + > grant.request().budget.max_settlement_bytes + { + return Err(BoundedWorkspaceObservationErrorV1::SettlementBudgetExceeded); + } + let schema_admission_evidence_digest = + schema_admission_evidence(self.profile.settlement_schema_digest, &result); + Ok(ExternalActionSettlementCandidateV1::new( + grant.request().request_id(), + grant.claim().attempt_id, + self.profile.adapter_id, + kind, + self.profile.settlement_schema_digest, + grant.request().basis_digest, + result, + schema_admission_evidence_digest, + external_evidence_digest, + )) + } + + fn validate_candidate( + &self, + grant: &ExternalActionClaimGrantV1, + admitted: &AdmittedEdictExternalActionRequestV1, + candidate: &ExternalActionSettlementCandidateV1, + ) -> Result<(), BoundedWorkspaceObservationErrorV1> { + if candidate.request_id != grant.request().request_id() + || candidate.attempt_id != grant.claim().attempt_id + || candidate.adapter_id != self.profile.adapter_id + || candidate.settlement_schema_digest != self.profile.settlement_schema_digest + || candidate.basis_digest != grant.request().basis_digest + || candidate.declared_result_digest + != Hash::from(blake3::hash(&candidate.canonical_result_bytes)) + { + return Err(BoundedWorkspaceObservationErrorV1::SchemaAdmissionFailed); + } + let value = decode_canonical_cbor_v1(&candidate.canonical_result_bytes) + .map_err(|_| BoundedWorkspaceObservationErrorV1::SchemaAdmissionFailed)?; + let expected_paths = if candidate.kind == ExternalActionSettlementKindV1::Succeeded { + let paths = decode_observation_input(admitted.canonical_operation_input()) + .map_err(|_| BoundedWorkspaceObservationErrorV1::SchemaAdmissionFailed)?; + let expected = paths.iter().cloned().collect::>(); + if expected.is_empty() + || expected.len() != paths.len() + || expected.iter().any(|path| { + validate_relative_path(path).is_err() || !self.permitted_paths.contains(path) + }) + { + return Err(BoundedWorkspaceObservationErrorV1::SchemaAdmissionFailed); + } + Some(expected) + } else { + None + }; + validate_observation_settlement( + &value, + candidate.kind, + candidate.basis_digest, + candidate.external_evidence_digest, + expected_paths.as_ref(), + )?; + let expected_evidence = schema_admission_evidence( + candidate.settlement_schema_digest, + &candidate.canonical_result_bytes, + ); + if candidate.schema_admission_evidence_digest != expected_evidence { + return Err(BoundedWorkspaceObservationErrorV1::SchemaAdmissionFailed); + } + Ok(()) + } +} + +/// Encodes the operation-specific path request as canonical CBOR. +pub fn encode_bounded_workspace_observation_input_v1( + paths: impl IntoIterator, +) -> Result, BoundedWorkspaceObservationErrorV1> { + let paths = paths + .into_iter() + .map(CanonicalValueV1::Text) + .collect::>(); + encode_canonical_cbor_v1(&canonical_map([ + ( + "kind", + CanonicalValueV1::Text(OBSERVATION_INPUT_KIND.to_owned()), + ), + ("paths", CanonicalValueV1::Array(paths)), + ])) + .map_err(|_| BoundedWorkspaceObservationErrorV1::Canonical) +} + +/// Computes the basis committed by a sorted path/content snapshot. +pub fn bounded_workspace_observation_basis_v1<'a>( + entries: impl IntoIterator, +) -> Hash { + let mut entries = entries.into_iter().collect::>(); + entries.sort_by(|left, right| left.0.cmp(right.0)); + let mut hasher = blake3::Hasher::new(); + hasher.update(OBSERVATION_BASIS_DOMAIN); + hasher.update( + &u64::try_from(entries.len()) + .unwrap_or(u64::MAX) + .to_le_bytes(), + ); + for (path, bytes) in entries { + hash_len_prefixed(&mut hasher, path.as_bytes()); + hash_len_prefixed(&mut hasher, bytes); + } + hasher.finalize().into() +} + +/// Stable adapter/profile failures. +#[derive(Debug, Error, PartialEq, Eq)] +pub enum BoundedWorkspaceObservationErrorV1 { + /// The canonical input or settlement encoding is invalid. + #[error("bounded observation canonical value is invalid")] + Canonical, + /// The claim, compiler request, and adapter profile do not identify one request. + #[error("bounded observation grant does not match the admitted compiler request")] + GrantMismatch, + /// The compiler request is outside the runtime-owned adapter profile. + #[error("bounded observation request does not match adapter policy")] + ProfileMismatch, + /// An input path is empty, absolute, non-normalized, or traverses a parent. + #[error("bounded observation path is invalid")] + InvalidPath, + /// The requested relative path is outside the attenuated path set. + #[error("bounded observation path is unauthorized")] + UnauthorizedPath, + /// A requested path or any traversed component is a symlink. + #[error("bounded observation refuses symlinks")] + SymlinkRefused, + /// A requested path is not one regular file. + #[error("bounded observation requires regular files")] + NotRegularFile, + /// The observed bytes do not match the request's exact basis. + #[error("bounded observation basis is stale")] + StaleBasis, + /// The canonical settlement exceeds the compiler-delegated bound. + #[error("bounded observation settlement exceeds its byte budget")] + SettlementBudgetExceeded, + /// The candidate did not pass the operation-specific settlement validator. + #[error("bounded observation settlement schema admission failed")] + SchemaAdmissionFailed, + /// Capability-rooted filesystem access failed. + #[error("bounded observation filesystem access failed")] + Io, + /// The generic durable protocol refused the transition. + #[error(transparent)] + Protocol(#[from] ExternalActionProtocolErrorV1), +} + +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] +struct ResourceRefV1 { + coordinate: String, + digest: Hash, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +struct LocalRefV1 { + id: String, + alpha_name: String, + ty: String, +} + +#[derive(Clone, Copy, Debug)] +struct CoreEvaluationBudgetV1 { + steps: u64, + allocated_bytes: u64, + output_bytes: u64, +} + +struct BoundedExpressionEvaluatorV1<'a> { + application_input: &'a CanonicalValueV1, + budget: CoreEvaluationBudgetV1, + steps: u64, + allocated_bytes: u64, +} + +impl ResourceRefV1 { + fn review_digest(&self) -> String { + format!("sha256:{}", hex::encode(self.digest)) + } +} + +#[derive(Debug)] +struct ObservedFileV1 { + path: String, + bytes: Vec, +} + +fn expect_map( + value: &CanonicalValueV1, +) -> Result<&[(CanonicalValueV1, CanonicalValueV1)], EdictExternalActionAdmissionErrorV1> { + match value { + CanonicalValueV1::Map(entries) => Ok(entries), + _ => Err(EdictExternalActionAdmissionErrorV1::ArtifactShape), + } +} + +fn expect_array( + value: &CanonicalValueV1, +) -> Result<&[CanonicalValueV1], EdictExternalActionAdmissionErrorV1> { + match value { + CanonicalValueV1::Array(entries) => Ok(entries), + _ => Err(EdictExternalActionAdmissionErrorV1::ArtifactShape), + } +} + +fn expect_bytes(value: &CanonicalValueV1) -> Result<&[u8], EdictExternalActionAdmissionErrorV1> { + match value { + CanonicalValueV1::Bytes(bytes) => Ok(bytes), + _ => Err(EdictExternalActionAdmissionErrorV1::InvalidRuntimeValue), + } +} + +fn expect_hash(value: &CanonicalValueV1) -> Result { + expect_bytes(value)? + .try_into() + .map_err(|_| EdictExternalActionAdmissionErrorV1::InvalidRuntimeValue) +} + +fn expect_u64(value: &CanonicalValueV1) -> Result { + let CanonicalValueV1::Integer(value) = value else { + return Err(EdictExternalActionAdmissionErrorV1::InvalidRuntimeValue); + }; + u64::try_from(*value).map_err(|_| EdictExternalActionAdmissionErrorV1::InvalidRuntimeValue) +} + +fn expect_u32(value: &CanonicalValueV1) -> Result { + u32::try_from(expect_u64(value)?) + .map_err(|_| EdictExternalActionAdmissionErrorV1::InvalidRuntimeValue) +} + +fn map_field<'a>( + entries: &'a [(CanonicalValueV1, CanonicalValueV1)], + field: &str, +) -> Option<&'a CanonicalValueV1> { + entries.iter().find_map(|(key, value)| match key { + CanonicalValueV1::Text(key) if key == field => Some(value), + _ => None, + }) +} + +fn require_field<'a>( + entries: &'a [(CanonicalValueV1, CanonicalValueV1)], + field: &str, +) -> Result<&'a CanonicalValueV1, EdictExternalActionAdmissionErrorV1> { + map_field(entries, field).ok_or(EdictExternalActionAdmissionErrorV1::ArtifactShape) +} + +fn require_nonempty_text<'a>( + entries: &'a [(CanonicalValueV1, CanonicalValueV1)], + field: &str, +) -> Result<&'a str, EdictExternalActionAdmissionErrorV1> { + match require_field(entries, field)? { + CanonicalValueV1::Text(value) if !value.is_empty() => Ok(value), + _ => Err(EdictExternalActionAdmissionErrorV1::ArtifactShape), + } +} + +fn require_text_field( + entries: &[(CanonicalValueV1, CanonicalValueV1)], + field: &str, + expected: &str, +) -> Result<(), EdictExternalActionAdmissionErrorV1> { + if require_nonempty_text(entries, field)? == expected { + Ok(()) + } else { + Err(EdictExternalActionAdmissionErrorV1::ArtifactShape) + } +} + +fn parse_resource( + value: &CanonicalValueV1, +) -> Result { + let resource = expect_map(value)?; + let coordinate = require_nonempty_text(resource, "id")?.to_owned(); + let digest = expect_array(require_field(resource, "digest")?) + .map_err(|_| EdictExternalActionAdmissionErrorV1::InvalidDigest)?; + let [CanonicalValueV1::Text(algorithm), CanonicalValueV1::Bytes(bytes)] = digest else { + return Err(EdictExternalActionAdmissionErrorV1::InvalidDigest); + }; + if algorithm != "sha256" { + return Err(EdictExternalActionAdmissionErrorV1::InvalidDigest); + } + let digest = bytes + .as_slice() + .try_into() + .map_err(|_| EdictExternalActionAdmissionErrorV1::InvalidDigest)?; + Ok(ResourceRefV1 { coordinate, digest }) +} + +fn parse_local_ref( + value: &CanonicalValueV1, +) -> Result { + let reference = expect_map(value)?; + Ok(LocalRefV1 { + id: require_nonempty_text(reference, "id")?.to_owned(), + alpha_name: require_nonempty_text(reference, "alphaName")?.to_owned(), + ty: require_nonempty_text(reference, "type")?.to_owned(), + }) +} + +fn verify_target_derivation( + core: &[(CanonicalValueV1, CanonicalValueV1)], + intent_name: &str, + target_intent: &[(CanonicalValueV1, CanonicalValueV1)], + target_request: &[(CanonicalValueV1, CanonicalValueV1)], + target_lawpacks: &[ResourceRefV1], + target_capabilities: &[ResourceRefV1], +) -> Result<(), EdictExternalActionAdmissionErrorV1> { + let core_intents = expect_map(require_field(core, "intents")?)?; + let core_intent = expect_map( + map_field(core_intents, intent_name) + .ok_or(EdictExternalActionAdmissionErrorV1::TargetDerivationMismatch)?, + )?; + if require_field(core_intent, "inputConstraints")? + != require_field(target_intent, "inputConstraints")? + || require_field(core_intent, "coreEvaluationBudget")? + != require_field(target_intent, "coreEvaluationBudget")? + || require_field(core_intent, "basis")? != require_field(target_intent, "basis")? + { + return Err(EdictExternalActionAdmissionErrorV1::TargetDerivationMismatch); + } + + let core_body = expect_map(require_field(core_intent, "body")?)?; + let core_nodes = expect_array(require_field(core_body, "nodes")?)?; + let [core_request_value] = core_nodes else { + return Err(EdictExternalActionAdmissionErrorV1::TargetDerivationMismatch); + }; + let core_request = expect_map(core_request_value)?; + require_text_field(core_request, "kind", "externalActionRequest") + .map_err(|_| EdictExternalActionAdmissionErrorV1::TargetDerivationMismatch)?; + let core_locals = expect_array(require_field(core_body, "locals")?)?; + let [core_argument, core_binding] = core_locals else { + return Err(EdictExternalActionAdmissionErrorV1::TargetDerivationMismatch); + }; + if parse_local_ref(core_argument)?.id != "arg.0" + || core_binding != require_field(target_request, "binding")? + || require_field(core_body, "result")? != require_field(target_intent, "result")? + || require_nonempty_text(target_request, "id")? != format!("{intent_name}.request.0") + { + return Err(EdictExternalActionAdmissionErrorV1::TargetDerivationMismatch); + } + for field in [ + "binding", + "operation", + "inputType", + "settlementType", + "inputSchema", + "settlementSchema", + "input", + "authorityScope", + "basis", + "budget", + "reconciliationLaw", + "state", + "settlementAdmission", + ] { + if require_field(core_request, field)? != require_field(target_request, field)? { + return Err(EdictExternalActionAdmissionErrorV1::TargetDerivationMismatch); + } + } + + let core_lawpacks = core_import_resources(core, "lawpack")?; + let core_capabilities = core_import_resources(core, "capability")?; + if canonical_resource_set(core_lawpacks)? != canonical_resource_set(target_lawpacks.to_vec())? + || canonical_resource_set(core_capabilities)? + != canonical_resource_set(target_capabilities.to_vec())? + { + return Err(EdictExternalActionAdmissionErrorV1::TargetDerivationMismatch); + } + Ok(()) +} + +fn core_import_resources( + core: &[(CanonicalValueV1, CanonicalValueV1)], + expected_kind: &str, +) -> Result, EdictExternalActionAdmissionErrorV1> { + let imports = expect_array(require_field(core, "imports")?)?; + let mut resources = Vec::new(); + for import in imports { + let import = expect_map(import)?; + let kind = require_nonempty_text(import, "kind")?; + if kind == expected_kind { + resources.push(parse_resource(require_field(import, "ref")?)?); + } + } + Ok(resources) +} + +fn canonical_resource_set( + mut resources: Vec, +) -> Result, EdictExternalActionAdmissionErrorV1> { + resources.sort(); + if resources.windows(2).any(|pair| pair[0] == pair[1]) { + return Err(EdictExternalActionAdmissionErrorV1::TargetDerivationMismatch); + } + Ok(resources) +} + +fn parse_evaluation_budget( + value: &CanonicalValueV1, +) -> Result { + let budget = expect_map(value)?; + let parsed = CoreEvaluationBudgetV1 { + steps: expect_u64(require_field(budget, "maxSteps")?)?, + allocated_bytes: expect_u64(require_field(budget, "maxAllocatedBytes")?)?, + output_bytes: expect_u64(require_field(budget, "maxOutputBytes")?)?, + }; + if parsed.steps == 0 + || parsed.allocated_bytes == 0 + || parsed.output_bytes == 0 + || parsed.steps > MAX_CORE_EVALUATION_STEPS_V1 + || parsed.allocated_bytes > MAX_CORE_EVALUATION_ALLOCATED_BYTES_V1 + || parsed.output_bytes > MAX_CORE_EVALUATION_OUTPUT_BYTES_V1 + { + return Err(EdictExternalActionAdmissionErrorV1::EvaluationBudgetExceeded); + } + Ok(parsed) +} + +impl<'a> BoundedExpressionEvaluatorV1<'a> { + fn new( + application_input: &'a CanonicalValueV1, + budget: CoreEvaluationBudgetV1, + ) -> Result { + let input_bytes = retained_value_bytes(application_input); + if input_bytes > budget.allocated_bytes { + return Err(EdictExternalActionAdmissionErrorV1::EvaluationBudgetExceeded); + } + Ok(Self { + application_input, + budget, + steps: 0, + allocated_bytes: 0, + }) + } + + fn evaluate_root( + &mut self, + expression: &CanonicalValueV1, + ) -> Result { + let value = self.evaluate(expression)?; + if retained_value_bytes(&value) > self.budget.output_bytes { + return Err(EdictExternalActionAdmissionErrorV1::EvaluationBudgetExceeded); + } + Ok(value) + } + + fn evaluate( + &mut self, + expression: &CanonicalValueV1, + ) -> Result { + self.steps = self.steps.saturating_add(1); + if self.steps > self.budget.steps { + return Err(EdictExternalActionAdmissionErrorV1::EvaluationBudgetExceeded); + } + let expression = expect_map(expression)?; + match require_nonempty_text(expression, "kind")? { + "local" => { + let reference = parse_local_ref(require_field(expression, "ref")?)?; + if reference.id != "arg.0" { + return Err(EdictExternalActionAdmissionErrorV1::UnsupportedExpression); + } + self.allocate_value(self.application_input)?; + Ok(self.application_input.clone()) + } + "field" => { + let base = self.evaluate(require_field(expression, "base")?)?; + let base = expect_map(&base)?; + let field = require_nonempty_text(expression, "field")?; + let value = map_field(base, field) + .ok_or(EdictExternalActionAdmissionErrorV1::InvalidRuntimeValue)?; + self.allocate_value(value)?; + Ok(value.clone()) + } + "const" => { + let value = evaluate_core_value(require_field(expression, "value")?)?; + self.allocate_value(&value)?; + Ok(value) + } + "record" => { + let fields = expect_map(require_field(expression, "fields")?)?; + let mut evaluated = Vec::with_capacity(fields.len()); + for (name, value) in fields { + let CanonicalValueV1::Text(name) = name else { + return Err(EdictExternalActionAdmissionErrorV1::ArtifactShape); + }; + self.allocate_bytes( + u64::try_from(name.len()) + .unwrap_or(u64::MAX) + .saturating_add(16), + )?; + evaluated.push((CanonicalValueV1::Text(name.clone()), self.evaluate(value)?)); + } + Ok(CanonicalValueV1::Map(evaluated)) + } + _ => Err(EdictExternalActionAdmissionErrorV1::UnsupportedExpression), + } + } + + fn allocate_value( + &mut self, + value: &CanonicalValueV1, + ) -> Result<(), EdictExternalActionAdmissionErrorV1> { + self.allocate_bytes(retained_value_bytes(value)) + } + + fn allocate_bytes(&mut self, bytes: u64) -> Result<(), EdictExternalActionAdmissionErrorV1> { + self.allocated_bytes = self.allocated_bytes.saturating_add(bytes); + if self.allocated_bytes > self.budget.allocated_bytes { + return Err(EdictExternalActionAdmissionErrorV1::EvaluationBudgetExceeded); + } + Ok(()) + } +} + +fn retained_value_bytes(value: &CanonicalValueV1) -> u64 { + let payload = match value { + CanonicalValueV1::Null | CanonicalValueV1::Bool(_) | CanonicalValueV1::Integer(_) => 16, + CanonicalValueV1::Bytes(bytes) => u64::try_from(bytes.len()).unwrap_or(u64::MAX), + CanonicalValueV1::Text(text) => u64::try_from(text.len()).unwrap_or(u64::MAX), + CanonicalValueV1::Array(values) => values + .iter() + .fold(0_u64, |total, value| { + total.saturating_add(retained_value_bytes(value)) + }) + .saturating_add( + u64::try_from(values.len()) + .unwrap_or(u64::MAX) + .saturating_mul(8), + ), + CanonicalValueV1::Map(entries) => entries + .iter() + .fold(0_u64, |total, (key, value)| { + total + .saturating_add(retained_value_bytes(key)) + .saturating_add(retained_value_bytes(value)) + }) + .saturating_add( + u64::try_from(entries.len()) + .unwrap_or(u64::MAX) + .saturating_mul(16), + ), + }; + payload.saturating_add(16) +} + +fn evaluate_core_value( + value: &CanonicalValueV1, +) -> Result { + let value = expect_map(value)?; + match require_nonempty_text(value, "kind")? { + "null" => Ok(CanonicalValueV1::Null), + "bool" => match require_field(value, "value")? { + CanonicalValueV1::Bool(value) => Ok(CanonicalValueV1::Bool(*value)), + _ => Err(EdictExternalActionAdmissionErrorV1::InvalidRuntimeValue), + }, + "int" => match require_field(value, "value")? { + CanonicalValueV1::Integer(value) => Ok(CanonicalValueV1::Integer(*value)), + _ => Err(EdictExternalActionAdmissionErrorV1::InvalidRuntimeValue), + }, + "string" => match require_field(value, "value")? { + CanonicalValueV1::Text(value) => Ok(CanonicalValueV1::Text(value.clone())), + _ => Err(EdictExternalActionAdmissionErrorV1::InvalidRuntimeValue), + }, + "bytes" => match require_field(value, "value")? { + CanonicalValueV1::Bytes(value) => Ok(CanonicalValueV1::Bytes(value.clone())), + _ => Err(EdictExternalActionAdmissionErrorV1::InvalidRuntimeValue), + }, + _ => Err(EdictExternalActionAdmissionErrorV1::InvalidRuntimeValue), + } +} + +fn bytes_type_max(value: &str) -> Result { + let value = value + .strip_prefix("Bytes')) + .ok_or(EdictExternalActionAdmissionErrorV1::ArtifactShape)?; + value + .parse() + .map_err(|_| EdictExternalActionAdmissionErrorV1::ArtifactShape) +} + +fn resource_identity(resource: &ResourceRefV1) -> Hash { + let mut hasher = blake3::Hasher::new(); + hasher.update(RESOURCE_ID_DOMAIN); + hash_len_prefixed(&mut hasher, resource.coordinate.as_bytes()); + hasher.update(&resource.digest); + hasher.finalize().into() +} + +fn target_operation_identity(target_profile: &ResourceRefV1, operation: &ResourceRefV1) -> Hash { + let mut hasher = blake3::Hasher::new(); + hasher.update(TARGET_OPERATION_ID_DOMAIN); + hasher.update(&resource_identity(target_profile)); + hasher.update(&resource_identity(operation)); + hasher.finalize().into() +} + +fn input_identity(bytes: &[u8]) -> Hash { + let mut hasher = blake3::Hasher::new(); + hasher.update(INPUT_DIGEST_DOMAIN); + hash_len_prefixed(&mut hasher, bytes); + hasher.finalize().into() +} + +fn decode_observation_input( + bytes: &[u8], +) -> Result, BoundedWorkspaceObservationErrorV1> { + let value = decode_canonical_cbor_v1(bytes) + .map_err(|_| BoundedWorkspaceObservationErrorV1::Canonical)?; + let CanonicalValueV1::Map(entries) = value else { + return Err(BoundedWorkspaceObservationErrorV1::Canonical); + }; + if entries.len() != 2 { + return Err(BoundedWorkspaceObservationErrorV1::Canonical); + } + let Some(CanonicalValueV1::Text(kind)) = map_field(&entries, "kind") else { + return Err(BoundedWorkspaceObservationErrorV1::Canonical); + }; + if kind != OBSERVATION_INPUT_KIND { + return Err(BoundedWorkspaceObservationErrorV1::Canonical); + } + let Some(CanonicalValueV1::Array(paths)) = map_field(&entries, "paths") else { + return Err(BoundedWorkspaceObservationErrorV1::Canonical); + }; + paths + .iter() + .map(|path| match path { + CanonicalValueV1::Text(path) => Ok(path.clone()), + _ => Err(BoundedWorkspaceObservationErrorV1::Canonical), + }) + .collect() +} + +fn validate_relative_path(path: &str) -> Result<(), BoundedWorkspaceObservationErrorV1> { + if path.is_empty() + || path.starts_with('/') + || path.ends_with('/') + || path.contains("//") + || path.contains('\\') + || path + .split('/') + .any(|component| component.is_empty() || matches!(component, "." | "..")) + { + return Err(BoundedWorkspaceObservationErrorV1::InvalidPath); + } + let mut component_count = 0_usize; + for component in Path::new(path).components() { + match component { + Component::Normal(component) if component.to_str().is_some() => { + component_count = component_count.saturating_add(1); + } + _ => return Err(BoundedWorkspaceObservationErrorV1::InvalidPath), + } + } + if component_count == 0 { + Err(BoundedWorkspaceObservationErrorV1::InvalidPath) + } else { + Ok(()) + } +} + +fn encode_observation_settlement( + posture: &str, + basis: Hash, + evidence: Hash, + files: &[ObservedFileV1], + obstruction: Option<&str>, +) -> Result, BoundedWorkspaceObservationErrorV1> { + let files = files + .iter() + .map(|file| { + canonical_map([ + ("path", CanonicalValueV1::Text(file.path.clone())), + ("bytes", CanonicalValueV1::Bytes(file.bytes.clone())), + ( + "digest", + CanonicalValueV1::Bytes(blake3::hash(&file.bytes).as_bytes().to_vec()), + ), + ]) + }) + .collect(); + encode_canonical_cbor_v1(&canonical_map([ + ( + "kind", + CanonicalValueV1::Text(OBSERVATION_SETTLEMENT_KIND.to_owned()), + ), + ("posture", CanonicalValueV1::Text(posture.to_owned())), + ("basis", CanonicalValueV1::Bytes(basis.to_vec())), + ("evidence", CanonicalValueV1::Bytes(evidence.to_vec())), + ("files", CanonicalValueV1::Array(files)), + ( + "obstruction", + obstruction.map_or(CanonicalValueV1::Null, |value| { + CanonicalValueV1::Text(value.to_owned()) + }), + ), + ])) + .map_err(|_| BoundedWorkspaceObservationErrorV1::Canonical) +} + +fn minimum_terminal_settlement_bytes_v1( + basis: Hash, +) -> Result { + [ + ("rejected", "settlement-budget-exceeded"), + ("rejected", "malformed-input"), + ("failed", "io-failure"), + ("outcomeUnknown", "outcome-unknown"), + ] + .into_iter() + .map(|(posture, obstruction)| { + encode_observation_settlement(posture, basis, [0xff; 32], &[], Some(obstruction)) + .map(|bytes| u64::try_from(bytes.len()).unwrap_or(u64::MAX)) + .map_err(|_| EdictExternalActionAdmissionErrorV1::InvalidRuntimeValue) + }) + .try_fold(0_u64, |maximum, length| { + length.map(|length| maximum.max(length)) + }) +} + +fn validate_observation_settlement( + value: &CanonicalValueV1, + kind: ExternalActionSettlementKindV1, + basis: Hash, + evidence: Hash, + expected_paths: Option<&BTreeSet>, +) -> Result<(), BoundedWorkspaceObservationErrorV1> { + let CanonicalValueV1::Map(entries) = value else { + return Err(BoundedWorkspaceObservationErrorV1::SchemaAdmissionFailed); + }; + if entries.len() != 6 + || !matches!( + map_field(entries, "kind"), + Some(CanonicalValueV1::Text(value)) if value == OBSERVATION_SETTLEMENT_KIND + ) + || !matches!( + map_field(entries, "basis"), + Some(CanonicalValueV1::Bytes(value)) if value.as_slice() == basis + ) + || !matches!( + map_field(entries, "evidence"), + Some(CanonicalValueV1::Bytes(value)) if value.as_slice() == evidence + ) + { + return Err(BoundedWorkspaceObservationErrorV1::SchemaAdmissionFailed); + } + let expected_posture = match kind { + ExternalActionSettlementKindV1::Succeeded => "succeeded", + ExternalActionSettlementKindV1::Rejected => "rejected", + ExternalActionSettlementKindV1::Failed => "failed", + ExternalActionSettlementKindV1::OutcomeUnknown => "outcomeUnknown", + }; + if !matches!( + map_field(entries, "posture"), + Some(CanonicalValueV1::Text(value)) if value == expected_posture + ) { + return Err(BoundedWorkspaceObservationErrorV1::SchemaAdmissionFailed); + } + let Some(CanonicalValueV1::Array(files)) = map_field(entries, "files") else { + return Err(BoundedWorkspaceObservationErrorV1::SchemaAdmissionFailed); + }; + if evidence == [0; 32] { + return Err(BoundedWorkspaceObservationErrorV1::SchemaAdmissionFailed); + } + if kind == ExternalActionSettlementKindV1::Succeeded { + if files.is_empty() + || !matches!( + map_field(entries, "obstruction"), + Some(CanonicalValueV1::Null) + ) + { + return Err(BoundedWorkspaceObservationErrorV1::SchemaAdmissionFailed); + } + let mut observed = Vec::with_capacity(files.len()); + let mut previous_path: Option<&str> = None; + for file in files { + let CanonicalValueV1::Map(file) = file else { + return Err(BoundedWorkspaceObservationErrorV1::SchemaAdmissionFailed); + }; + if file.len() != 3 { + return Err(BoundedWorkspaceObservationErrorV1::SchemaAdmissionFailed); + } + let ( + Some(CanonicalValueV1::Text(path)), + Some(CanonicalValueV1::Bytes(bytes)), + Some(CanonicalValueV1::Bytes(digest)), + ) = ( + map_field(file, "path"), + map_field(file, "bytes"), + map_field(file, "digest"), + ) + else { + return Err(BoundedWorkspaceObservationErrorV1::SchemaAdmissionFailed); + }; + if validate_relative_path(path).is_err() + || previous_path.is_some_and(|previous| previous >= path.as_str()) + || digest.as_slice() != blake3::hash(bytes).as_bytes() + { + return Err(BoundedWorkspaceObservationErrorV1::SchemaAdmissionFailed); + } + previous_path = Some(path); + observed.push((path.as_str(), bytes.as_slice())); + } + if bounded_workspace_observation_basis_v1(observed) != basis || evidence != basis { + return Err(BoundedWorkspaceObservationErrorV1::SchemaAdmissionFailed); + } + let Some(expected_paths) = expected_paths else { + return Err(BoundedWorkspaceObservationErrorV1::SchemaAdmissionFailed); + }; + if files.len() != expected_paths.len() + || files.iter().zip(expected_paths).any(|(file, expected)| { + !matches!( + file, + CanonicalValueV1::Map(entries) + if matches!( + map_field(entries, "path"), + Some(CanonicalValueV1::Text(path)) if path == expected + ) + ) + }) + { + return Err(BoundedWorkspaceObservationErrorV1::SchemaAdmissionFailed); + } + } else if !files.is_empty() + || !matches!( + map_field(entries, "obstruction"), + Some(CanonicalValueV1::Text(value)) if !value.is_empty() + ) + || expected_paths.is_some() + { + return Err(BoundedWorkspaceObservationErrorV1::SchemaAdmissionFailed); + } + Ok(()) +} + +fn canonical_map<'a>( + entries: impl IntoIterator, +) -> CanonicalValueV1 { + CanonicalValueV1::Map( + entries + .into_iter() + .map(|(key, value)| (CanonicalValueV1::Text(key.to_owned()), value)) + .collect(), + ) +} + +fn schema_admission_evidence(schema: Hash, bytes: &[u8]) -> Hash { + let mut hasher = blake3::Hasher::new(); + hasher.update(OBSERVATION_SCHEMA_EVIDENCE_DOMAIN); + hasher.update(&schema); + hash_len_prefixed(&mut hasher, bytes); + hasher.finalize().into() +} + +fn refusal_evidence(code: &str, detail: &str) -> Hash { + let mut hasher = blake3::Hasher::new(); + hasher.update(OBSERVATION_REFUSAL_EVIDENCE_DOMAIN); + hash_len_prefixed(&mut hasher, code.as_bytes()); + hash_len_prefixed(&mut hasher, detail.as_bytes()); + hasher.finalize().into() +} + +fn hash_len_prefixed(hasher: &mut blake3::Hasher, bytes: &[u8]) { + hasher.update(&u64::try_from(bytes.len()).unwrap_or(u64::MAX).to_le_bytes()); + hasher.update(bytes); +} diff --git a/crates/warp-core/src/lib.rs b/crates/warp-core/src/lib.rs index cc76da61..143eec82 100644 --- a/crates/warp-core/src/lib.rs +++ b/crates/warp-core/src/lib.rs @@ -70,6 +70,8 @@ mod edict_target_ir; mod engine_impl; pub mod evidence; pub mod external_action; +#[cfg(not(target_arch = "wasm32"))] +pub mod external_action_adapter; mod footprint; /// Footprint enforcement guard for parallel execution. /// diff --git a/crates/warp-core/tests/bounded_workspace_observation_tests.rs b/crates/warp-core/tests/bounded_workspace_observation_tests.rs new file mode 100644 index 00000000..b63c825a --- /dev/null +++ b/crates/warp-core/tests/bounded_workspace_observation_tests.rs @@ -0,0 +1,1452 @@ +// SPDX-License-Identifier: Apache-2.0 +// © James Ross Ω FLYING•ROBOTS +//! RED contract for compiler-authored bounded workspace observation. + +#![allow(clippy::panic)] + +use std::fs; +use std::path::{Path, PathBuf}; +use std::sync::atomic::{AtomicU64, Ordering}; + +use echo_edict_canonical::{ + decode_canonical_cbor_v1, digest_canonical_value_v1, encode_canonical_cbor_v1, CanonicalValueV1, +}; +use warp_core::causal_wal::{ + InMemoryWalStore, Lsn, PayloadCodecId, PayloadSchemaId, WalDurabilityMode, WalSegmentId, + WalStorePort, WalTransactionId, WriterEpochId, WriterEpochRequest, +}; +use warp_core::external_action::{ + claim_external_action, record_external_action_request, ExternalActionAdapterIdV1, + ExternalActionAdapterRegistryV1, ExternalActionClaimGrantV1, ExternalActionCoordinatorV1, + ExternalActionProtocolErrorV1, ExternalActionSettlementCandidateV1, + ExternalActionSettlementKindV1, ExternalActionTransactionContextV1, + RecoveredExternalActionPostureV1, +}; +use warp_core::external_action_adapter::{ + admit_edict_external_action_request_v1, bounded_workspace_observation_basis_v1, + encode_bounded_workspace_observation_input_v1, AdmittedEdictExternalActionRequestV1, + BoundedWorkspaceObservationAdapterV1, BoundedWorkspaceObservationErrorV1, + BoundedWorkspaceObservationProfileV1, EdictExternalActionAdmissionErrorV1, +}; +use warp_core::{Hash, WorldlineId}; + +const CORE_BYTES: &[u8] = include_bytes!("fixtures/external_action/observe-workspace.core.cbor"); +const TARGET_IR_BYTES: &[u8] = + include_bytes!("fixtures/external_action/observe-workspace.target-ir.cbor"); +const CORE_DIGEST: &str = include_str!("fixtures/external_action/observe-workspace.core.sha256"); +const TARGET_IR_DIGEST: &str = + include_str!("fixtures/external_action/observe-workspace.target-ir.sha256"); + +fn digest(label: &str) -> Hash { + blake3::hash(label.as_bytes()).into() +} + +fn must_ok(result: Result) -> T { + match result { + Ok(value) => value, + Err(error) => panic!("expected Ok(..), got {error:?}"), + } +} + +fn must_some(value: Option) -> T { + match value { + Some(value) => value, + None => panic!("expected Some(..), got None"), + } +} + +fn text(value: &str) -> CanonicalValueV1 { + CanonicalValueV1::Text(value.to_owned()) +} + +fn map(entries: impl IntoIterator) -> CanonicalValueV1 { + CanonicalValueV1::Map( + entries + .into_iter() + .map(|(key, value)| (text(key), value)) + .collect(), + ) +} + +fn map_field_mut<'a>(value: &'a mut CanonicalValueV1, field: &str) -> &'a mut CanonicalValueV1 { + let CanonicalValueV1::Map(entries) = value else { + panic!("expected map containing {field}"); + }; + must_some(entries.iter_mut().find_map(|(key, value)| match key { + CanonicalValueV1::Text(key) if key == field => Some(value), + _ => None, + })) +} + +fn target_ir_value() -> CanonicalValueV1 { + must_ok(decode_canonical_cbor_v1(TARGET_IR_BYTES)) +} + +fn core_value() -> CanonicalValueV1 { + must_ok(decode_canonical_cbor_v1(CORE_BYTES)) +} + +fn encoded(value: &CanonicalValueV1) -> Vec { + must_ok(encode_canonical_cbor_v1(value)) +} + +fn intent_mut<'a>(artifact: &'a mut CanonicalValueV1, name: &str) -> &'a mut CanonicalValueV1 { + map_field_mut(map_field_mut(artifact, "intents"), name) +} + +fn target_request_mut(artifact: &mut CanonicalValueV1) -> &mut CanonicalValueV1 { + let requests = map_field_mut(intent_mut(artifact, "observe"), "externalActionRequests"); + let CanonicalValueV1::Array(requests) = requests else { + panic!("expected external request array"); + }; + must_some(requests.first_mut()) +} + +fn bind_target_to_core(target: &mut CanonicalValueV1, core: &CanonicalValueV1) { + let reviewed = must_ok(digest_canonical_value_v1("edict.core.module/v1", core)); + let reviewed = must_some(reviewed.strip_prefix("sha256:")); + let reviewed = must_ok(hex::decode(reviewed)); + let source = map_field_mut(map_field_mut(target, "semanticClosure"), "sourceCore"); + let digest = map_field_mut(source, "digest"); + let CanonicalValueV1::Array(digest) = digest else { + panic!("expected reviewed digest"); + }; + let Some(CanonicalValueV1::Bytes(bytes)) = digest.get_mut(1) else { + panic!("expected reviewed digest bytes"); + }; + *bytes = reviewed; +} + +fn schema_admission_evidence(schema: Hash, bytes: &[u8]) -> Hash { + let mut hasher = blake3::Hasher::new(); + hasher.update(b"echo.bounded-observation.schema-evidence/v1"); + hasher.update(&schema); + hasher.update(&u64::try_from(bytes.len()).unwrap_or(u64::MAX).to_le_bytes()); + hasher.update(bytes); + hasher.finalize().into() +} + +fn rewrite_first_local_id(value: &mut CanonicalValueV1, replacement: &str) -> bool { + match value { + CanonicalValueV1::Map(entries) => { + let is_local = entries.iter().any(|(key, value)| { + matches!( + (key, value), + (CanonicalValueV1::Text(key), CanonicalValueV1::Text(value)) + if key == "kind" && value == "local" + ) + }); + if is_local { + let Some(CanonicalValueV1::Map(reference)) = + entries.iter_mut().find_map(|(key, value)| { + matches!(key, CanonicalValueV1::Text(key) if key == "ref").then_some(value) + }) + else { + panic!("local expression must contain a reference"); + }; + let Some(CanonicalValueV1::Text(id)) = + reference.iter_mut().find_map(|(key, value)| { + matches!(key, CanonicalValueV1::Text(key) if key == "id").then_some(value) + }) + else { + panic!("local reference must contain an id"); + }; + replacement.clone_into(id); + true + } else { + entries + .iter_mut() + .any(|(_, value)| rewrite_first_local_id(value, replacement)) + } + } + CanonicalValueV1::Array(values) => values + .iter_mut() + .any(|value| rewrite_first_local_id(value, replacement)), + _ => false, + } +} + +fn mutate_resource_digest(resources: &mut CanonicalValueV1, coordinate: &str, byte: u8) { + let CanonicalValueV1::Array(resources) = resources else { + panic!("expected resource array"); + }; + let resource = must_some(resources.iter_mut().find(|resource| { + let CanonicalValueV1::Map(entries) = resource else { + return false; + }; + entries.iter().any(|(key, value)| { + matches!( + (key, value), + (CanonicalValueV1::Text(key), CanonicalValueV1::Text(value)) + if key == "id" && value == coordinate + ) + }) + })); + let digest = map_field_mut(resource, "digest"); + let CanonicalValueV1::Array(digest) = digest else { + panic!("expected reviewed digest"); + }; + let Some(CanonicalValueV1::Bytes(bytes)) = digest.get_mut(1) else { + panic!("expected digest bytes"); + }; + *bytes = vec![byte; 32]; +} + +fn application_input( + operation_input: Vec, + scope: Hash, + basis: Hash, + max_settlement_bytes: u64, +) -> Vec { + must_ok(encode_canonical_cbor_v1(&map([ + ("payload", CanonicalValueV1::Bytes(operation_input)), + ("scope", CanonicalValueV1::Bytes(scope.to_vec())), + ("basis", CanonicalValueV1::Bytes(basis.to_vec())), + ( + "maxSettlementBytes", + CanonicalValueV1::Integer(i128::from(max_settlement_bytes)), + ), + ("maxAttempts", CanonicalValueV1::Integer(1)), + ]))) +} + +fn admitted_request( + worldline_byte: u8, + paths: impl IntoIterator, + scope: Hash, + basis: Hash, + max_settlement_bytes: u64, +) -> AdmittedEdictExternalActionRequestV1 { + let operation_input = must_ok(encode_bounded_workspace_observation_input_v1(paths)); + must_ok(admit_edict_external_action_request_v1( + WorldlineId::from_bytes([worldline_byte; 32]), + CORE_BYTES, + TARGET_IR_BYTES, + "observe", + &application_input(operation_input, scope, basis, max_settlement_bytes), + )) +} + +fn profile( + admitted: &AdmittedEdictExternalActionRequestV1, + adapter_label: &str, +) -> BoundedWorkspaceObservationProfileV1 { + let request = admitted.request(); + BoundedWorkspaceObservationProfileV1 { + operation_id: request.operation_id, + input_schema_digest: request.input_schema_digest, + settlement_schema_digest: request.settlement_schema_digest, + reconciliation_law_digest: request.reconciliation_law_digest, + authority_scope_digest: request.authority_scope_digest, + adapter_id: ExternalActionAdapterIdV1::from_hash(digest(adapter_label)), + } +} + +fn epoch_id() -> WriterEpochId { + WriterEpochId::from_hash(digest("bounded-observation:epoch")) +} + +fn store() -> InMemoryWalStore { + let mut store = InMemoryWalStore::new(); + must_ok(store.acquire_writer_epoch(WriterEpochRequest { + epoch_id: epoch_id(), + storage_fencing_token: digest("bounded-observation:fencing"), + process_identity: digest("bounded-observation:process"), + host_identity: digest("bounded-observation:host"), + started_at_lsn: Lsn::from_raw(0), + previous_epoch_id: None, + previous_epoch_final_commit_digest: None, + lease_or_lock_evidence: digest("bounded-observation:lease"), + })); + store +} + +fn context(label: &str) -> ExternalActionTransactionContextV1 { + ExternalActionTransactionContextV1 { + writer_epoch: epoch_id(), + segment_id: WalSegmentId::from_raw(1), + transaction_id: WalTransactionId::from_hash(digest(label)), + durability_mode: WalDurabilityMode::Buffered, + payload_codec_id: PayloadCodecId::from_hash(digest("bounded-observation:codec")), + payload_schema_id: PayloadSchemaId::from_hash(digest("bounded-observation:schema")), + payload_schema_version: 1, + canonical_encoding_version: 1, + digest_domain: digest("bounded-observation:wal-domain"), + } +} + +fn claim( + store: &mut InMemoryWalStore, + coordinator: &mut ExternalActionCoordinatorV1, + admitted: &AdmittedEdictExternalActionRequestV1, + adapter: &BoundedWorkspaceObservationAdapterV1, + label: &str, +) -> ExternalActionClaimGrantV1 { + let request = admitted.request(); + let recorded = must_ok(record_external_action_request( + store, + coordinator, + context(&format!("{label}:request")), + request, + )); + let registry = ExternalActionAdapterRegistryV1::new([adapter.adapter_binding()]); + let authorization = must_ok(registry.authorize(&request, adapter.adapter_binding().adapter_id)); + must_ok(claim_external_action( + store, + coordinator, + context(&format!("{label}:claim")), + recorded, + authorization, + request.basis_digest, + 0, + digest(&format!("{label}:lease")), + )) +} + +static TEMP_COUNTER: AtomicU64 = AtomicU64::new(0); + +struct TempRoot(PathBuf); + +impl TempRoot { + fn new(label: &str) -> Self { + let counter = TEMP_COUNTER.fetch_add(1, Ordering::Relaxed); + let root = std::env::temp_dir().join(format!( + "echo-bounded-observation-{}-{counter}-{label}", + std::process::id() + )); + if root.exists() { + must_ok(fs::remove_dir_all(&root)); + } + must_ok(fs::create_dir_all(&root)); + Self(root) + } + + fn path(&self) -> &Path { + &self.0 + } + + fn write(&self, relative: &str, bytes: &[u8]) { + let path = self.0.join(relative); + if let Some(parent) = path.parent() { + must_ok(fs::create_dir_all(parent)); + } + must_ok(fs::write(path, bytes)); + } +} + +impl Drop for TempRoot { + fn drop(&mut self) { + let _ = fs::remove_dir_all(&self.0); + } +} + +fn adapter( + root: &TempRoot, + admitted: &AdmittedEdictExternalActionRequestV1, + permitted_paths: impl IntoIterator, +) -> BoundedWorkspaceObservationAdapterV1 { + must_ok(BoundedWorkspaceObservationAdapterV1::open( + root.path(), + permitted_paths, + profile(admitted, "bounded-observation:adapter"), + )) +} + +#[test] +fn exact_compiler_artifacts_admit_one_noncallable_request() { + let scope = digest("scope:exact"); + let basis = digest("basis:exact"); + let operation_input = must_ok(encode_bounded_workspace_observation_input_v1([ + "src/lib.rs".to_owned(), + ])); + let admitted = must_ok(admit_edict_external_action_request_v1( + WorldlineId::from_bytes([7; 32]), + CORE_BYTES, + TARGET_IR_BYTES, + "observe", + &application_input(operation_input.clone(), scope, basis, 65_536), + )); + + assert_eq!(admitted.source_core_digest(), CORE_DIGEST.trim()); + assert_eq!(admitted.target_ir_digest(), TARGET_IR_DIGEST.trim()); + assert_eq!( + admitted.operation_coordinate(), + "workspace.snapshot.observe@1" + ); + assert_eq!(admitted.canonical_operation_input(), operation_input); + assert_eq!(admitted.request().authority_scope_digest, scope); + assert_eq!(admitted.request().basis_digest, basis); + assert_eq!(admitted.request().budget.max_settlement_bytes, 65_536); + assert_eq!(admitted.request().budget.max_attempts, 1); +} + +#[test] +fn malformed_or_substituted_compiler_artifacts_fail_closed() { + let input = application_input(vec![], digest("scope"), digest("basis"), 1024); + let mut malformed_core = CORE_BYTES.to_vec(); + malformed_core.push(0); + assert!(matches!( + admit_edict_external_action_request_v1( + WorldlineId::from_bytes([1; 32]), + &malformed_core, + TARGET_IR_BYTES, + "observe", + &input, + ), + Err(EdictExternalActionAdmissionErrorV1::Canonical(_)) + )); + + assert_eq!( + admit_edict_external_action_request_v1( + WorldlineId::from_bytes([1; 32]), + CORE_BYTES, + TARGET_IR_BYTES, + "missing", + &input, + ), + Err(EdictExternalActionAdmissionErrorV1::MissingIntent) + ); + + let mut wrong_domain = target_ir_value(); + *map_field_mut(&mut wrong_domain, "domain") = text("other.span-ir/v1"); + assert_eq!( + admit_edict_external_action_request_v1( + WorldlineId::from_bytes([1; 32]), + CORE_BYTES, + &encoded(&wrong_domain), + "observe", + &input, + ), + Err(EdictExternalActionAdmissionErrorV1::ArtifactShape) + ); + + let mut wrong_source_coordinate = target_ir_value(); + *map_field_mut(&mut wrong_source_coordinate, "sourceCoreCoordinate") = text("other.source@1"); + let closure = map_field_mut(&mut wrong_source_coordinate, "semanticClosure"); + *map_field_mut(map_field_mut(closure, "sourceCore"), "id") = text("other.source@1"); + assert_eq!( + admit_edict_external_action_request_v1( + WorldlineId::from_bytes([1; 32]), + CORE_BYTES, + &encoded(&wrong_source_coordinate), + "observe", + &input, + ), + Err(EdictExternalActionAdmissionErrorV1::CoreDigestMismatch) + ); + + let mut wrong_operation_profile = target_ir_value(); + let intent = map_field_mut( + map_field_mut(&mut wrong_operation_profile, "intents"), + "observe", + ); + *map_field_mut(intent, "operationProfile") = text("other.profile/v1"); + assert_eq!( + admit_edict_external_action_request_v1( + WorldlineId::from_bytes([1; 32]), + CORE_BYTES, + &encoded(&wrong_operation_profile), + "observe", + &input, + ), + Err(EdictExternalActionAdmissionErrorV1::ArtifactShape) + ); + + let mut substituted_capability = target_ir_value(); + let semantic_closure = map_field_mut(&mut substituted_capability, "semanticClosure"); + mutate_resource_digest( + map_field_mut(semantic_closure, "capabilities"), + "workspace.snapshot.observe@1", + 0x77, + ); + assert_eq!( + admit_edict_external_action_request_v1( + WorldlineId::from_bytes([1; 32]), + CORE_BYTES, + &encoded(&substituted_capability), + "observe", + &input, + ), + Err(EdictExternalActionAdmissionErrorV1::CapabilityClosureMismatch) + ); + + let mut callable = target_ir_value(); + let steps = map_field_mut( + map_field_mut(map_field_mut(&mut callable, "intents"), "observe"), + "steps", + ); + *steps = CanonicalValueV1::Array(vec![CanonicalValueV1::Null]); + assert_eq!( + admit_edict_external_action_request_v1( + WorldlineId::from_bytes([1; 32]), + CORE_BYTES, + &encoded(&callable), + "observe", + &input, + ), + Err(EdictExternalActionAdmissionErrorV1::CallableStepsPresent) + ); + + let mut hidden_local = target_ir_value(); + let requests = map_field_mut( + map_field_mut(map_field_mut(&mut hidden_local, "intents"), "observe"), + "externalActionRequests", + ); + let CanonicalValueV1::Array(requests) = requests else { + panic!("expected external request array"); + }; + assert!(rewrite_first_local_id( + map_field_mut(&mut requests[0], "input"), + "local.0" + )); + assert_eq!( + admit_edict_external_action_request_v1( + WorldlineId::from_bytes([1; 32]), + CORE_BYTES, + &encoded(&hidden_local), + "observe", + &input, + ), + Err(EdictExternalActionAdmissionErrorV1::TargetDerivationMismatch) + ); + + let mut wrong_result = target_ir_value(); + let result = map_field_mut( + map_field_mut(map_field_mut(&mut wrong_result, "intents"), "observe"), + "result", + ); + let result_reference = map_field_mut(result, "ref"); + *map_field_mut(result_reference, "id") = text("other.result"); + assert_eq!( + admit_edict_external_action_request_v1( + WorldlineId::from_bytes([1; 32]), + CORE_BYTES, + &encoded(&wrong_result), + "observe", + &input, + ), + Err(EdictExternalActionAdmissionErrorV1::ArtifactShape) + ); + + let mut wrong_basis = target_ir_value(); + let intent = map_field_mut(map_field_mut(&mut wrong_basis, "intents"), "observe"); + *map_field_mut(intent, "basis") = CanonicalValueV1::Null; + assert_eq!( + admit_edict_external_action_request_v1( + WorldlineId::from_bytes([1; 32]), + CORE_BYTES, + &encoded(&wrong_basis), + "observe", + &input, + ), + Err(EdictExternalActionAdmissionErrorV1::ArtifactShape) + ); +} + +#[test] +fn target_profile_digest_is_part_of_the_runtime_operation_identity() { + let input = application_input(vec![], digest("scope"), digest("basis"), 1024); + let baseline = must_ok(admit_edict_external_action_request_v1( + WorldlineId::from_bytes([2; 32]), + CORE_BYTES, + TARGET_IR_BYTES, + "observe", + &input, + )); + let mut substituted_profile = target_ir_value(); + let profile = map_field_mut(&mut substituted_profile, "targetProfile"); + let digest = map_field_mut(profile, "digest"); + let CanonicalValueV1::Array(digest) = digest else { + panic!("expected reviewed target profile digest"); + }; + let Some(CanonicalValueV1::Bytes(bytes)) = digest.get_mut(1) else { + panic!("expected target profile digest bytes"); + }; + *bytes = vec![0x55; 32]; + let substituted = must_ok(admit_edict_external_action_request_v1( + WorldlineId::from_bytes([2; 32]), + CORE_BYTES, + &encoded(&substituted_profile), + "observe", + &input, + )); + assert_ne!( + baseline.request().operation_id, + substituted.request().operation_id + ); +} + +#[test] +fn target_request_must_be_derived_from_the_supplied_core() { + let mut target = target_ir_value(); + *map_field_mut(target_request_mut(&mut target), "input") = map([ + ("kind", text("const")), + ( + "value", + map([ + ("kind", text("bytes")), + ("value", CanonicalValueV1::Bytes(vec![0x44])), + ]), + ), + ]); + let input = application_input( + vec![0x44], + digest("scope:target-derivation"), + digest("basis:target-derivation"), + 65_536, + ); + assert!( + admit_edict_external_action_request_v1( + WorldlineId::from_bytes([5; 32]), + CORE_BYTES, + &encoded(&target), + "observe", + &input, + ) + .is_err(), + "Target IR request fields must be corroborated against Core" + ); +} + +#[test] +fn expression_evaluation_obeys_declared_and_host_budgets() { + for (index, (field, limit)) in [ + ("maxSteps", 1_i128), + ("maxAllocatedBytes", 1_i128), + ("maxOutputBytes", 1_i128), + ("maxSteps", i128::from(u64::MAX)), + ] + .into_iter() + .enumerate() + { + let mut core = core_value(); + *map_field_mut( + map_field_mut(intent_mut(&mut core, "observe"), "coreEvaluationBudget"), + field, + ) = CanonicalValueV1::Integer(limit); + let mut target = target_ir_value(); + *map_field_mut( + map_field_mut(intent_mut(&mut target, "observe"), "coreEvaluationBudget"), + field, + ) = CanonicalValueV1::Integer(limit); + bind_target_to_core(&mut target, &core); + let operation_input = must_ok(encode_bounded_workspace_observation_input_v1([ + "metered.txt".to_owned(), + ])); + assert!( + admit_edict_external_action_request_v1( + WorldlineId::from_bytes([u8::try_from(index + 10).unwrap_or(u8::MAX); 32]), + &encoded(&core), + &encoded(&target), + "observe", + &application_input( + operation_input, + digest("scope:metered"), + digest("basis:metered"), + 65_536, + ), + ) + .is_err(), + "{field}={limit} must fail closed" + ); + } +} + +#[test] +fn request_budget_must_fit_a_terminal_settlement() { + let operation_input = must_ok(encode_bounded_workspace_observation_input_v1([ + "terminal.txt".to_owned(), + ])); + assert!( + admit_edict_external_action_request_v1( + WorldlineId::from_bytes([20; 32]), + CORE_BYTES, + TARGET_IR_BYTES, + "observe", + &application_input( + operation_input, + digest("scope:terminal"), + digest("basis:terminal"), + 1, + ), + ) + .is_err(), + "a request must not be admitted when no terminal envelope can fit" + ); +} + +#[test] +fn malformed_operation_input_settles_as_rejected() { + let root = TempRoot::new("malformed-input"); + let admitted = must_ok(admit_edict_external_action_request_v1( + WorldlineId::from_bytes([21; 32]), + CORE_BYTES, + TARGET_IR_BYTES, + "observe", + &application_input( + vec![0xff], + digest("scope:malformed-input"), + digest("basis:malformed-input"), + 65_536, + ), + )); + let adapter = adapter(&root, &admitted, ["unused.txt".to_owned()]); + let mut store = store(); + let mut coordinator = must_ok(ExternalActionCoordinatorV1::recover(&store)); + let grant = claim( + &mut store, + &mut coordinator, + &admitted, + &adapter, + "malformed-input", + ); + assert_eq!( + must_ok(adapter.observe(&grant, &admitted)).kind, + ExternalActionSettlementKindV1::Rejected + ); +} + +#[test] +fn settlement_admission_revalidates_the_adapter_profile() { + let root = TempRoot::new("settlement-profile"); + root.write("profile.txt", b"profile"); + let basis = bounded_workspace_observation_basis_v1([("profile.txt", b"profile".as_slice())]); + let admitted = admitted_request( + 22, + ["profile.txt".to_owned()], + digest("scope:settlement-profile"), + basis, + 65_536, + ); + let adapter = adapter(&root, &admitted, ["profile.txt".to_owned()]); + let mut mismatched_profile = profile(&admitted, "bounded-observation:adapter"); + mismatched_profile.operation_id = + warp_core::external_action::ExternalActionOperationIdV1::from_hash(digest( + "other-operation", + )); + let mismatched = must_ok(BoundedWorkspaceObservationAdapterV1::open( + root.path(), + ["profile.txt".to_owned()], + mismatched_profile, + )); + let mut store = store(); + let mut coordinator = must_ok(ExternalActionCoordinatorV1::recover(&store)); + let grant = claim( + &mut store, + &mut coordinator, + &admitted, + &adapter, + "settlement-profile", + ); + let candidate = must_ok(adapter.observe(&grant, &admitted)); + assert_eq!( + mismatched.admit_settlement( + &mut store, + &mut coordinator, + context("settlement-profile:settlement"), + &admitted, + grant, + candidate, + ), + Err(BoundedWorkspaceObservationErrorV1::ProfileMismatch) + ); +} + +#[test] +fn successful_settlement_paths_must_equal_the_requested_aperture() { + let root = TempRoot::new("settlement-aperture"); + let secret = b"secret"; + let basis = bounded_workspace_observation_basis_v1([("secret.txt", secret.as_slice())]); + let admitted = admitted_request( + 23, + ["allowed.txt".to_owned()], + digest("scope:settlement-aperture"), + basis, + 65_536, + ); + let adapter = adapter( + &root, + &admitted, + ["allowed.txt".to_owned(), "secret.txt".to_owned()], + ); + let mut store = store(); + let mut coordinator = must_ok(ExternalActionCoordinatorV1::recover(&store)); + let grant = claim( + &mut store, + &mut coordinator, + &admitted, + &adapter, + "settlement-aperture", + ); + let settlement = encoded(&map([ + ("kind", text("boundedWorkspaceObservationSettlement")), + ("posture", text("succeeded")), + ("basis", CanonicalValueV1::Bytes(basis.to_vec())), + ("evidence", CanonicalValueV1::Bytes(basis.to_vec())), + ( + "files", + CanonicalValueV1::Array(vec![map([ + ("path", text("secret.txt")), + ("bytes", CanonicalValueV1::Bytes(secret.to_vec())), + ( + "digest", + CanonicalValueV1::Bytes(blake3::hash(secret).as_bytes().to_vec()), + ), + ])]), + ), + ("obstruction", CanonicalValueV1::Null), + ])); + let request = admitted.request(); + let candidate = ExternalActionSettlementCandidateV1::new( + request.request_id(), + grant.claim().attempt_id, + adapter.adapter_binding().adapter_id, + ExternalActionSettlementKindV1::Succeeded, + request.settlement_schema_digest, + basis, + settlement.clone(), + schema_admission_evidence(request.settlement_schema_digest, &settlement), + basis, + ); + assert_eq!( + adapter.admit_settlement( + &mut store, + &mut coordinator, + context("settlement-aperture:settlement"), + &admitted, + grant, + candidate, + ), + Err(BoundedWorkspaceObservationErrorV1::SchemaAdmissionFailed) + ); +} + +#[test] +fn interior_current_directory_segments_are_not_canonical_paths() { + let root = TempRoot::new("dot-segment"); + let admitted = admitted_request( + 24, + ["dir/file.txt".to_owned()], + digest("scope:dot-segment"), + digest("basis:dot-segment"), + 65_536, + ); + assert!(matches!( + BoundedWorkspaceObservationAdapterV1::open( + root.path(), + ["dir/./file.txt".to_owned()], + profile(&admitted, "bounded-observation:dot-segment"), + ), + Err(BoundedWorkspaceObservationErrorV1::InvalidPath) + )); +} + +#[cfg(unix)] +#[test] +fn special_files_are_rejected_before_a_read_attempt() { + use std::os::unix::net::UnixListener; + + let root = TempRoot::new("special-file"); + let _listener = must_ok(UnixListener::bind(root.path().join("socket"))); + let admitted = admitted_request( + 25, + ["socket".to_owned()], + digest("scope:special-file"), + digest("basis:special-file"), + 65_536, + ); + let adapter = adapter(&root, &admitted, ["socket".to_owned()]); + let mut store = store(); + let mut coordinator = must_ok(ExternalActionCoordinatorV1::recover(&store)); + let grant = claim( + &mut store, + &mut coordinator, + &admitted, + &adapter, + "special-file", + ); + assert_eq!( + must_ok(adapter.observe(&grant, &admitted)).kind, + ExternalActionSettlementKindV1::Rejected + ); +} + +#[test] +fn request_and_claim_are_durable_before_the_adapter_can_read() { + let root = TempRoot::new("ordered"); + let bytes = b"durable before effect"; + let basis = bounded_workspace_observation_basis_v1([("notes/evidence.txt", bytes.as_slice())]); + let admitted = admitted_request( + 2, + ["notes/evidence.txt".to_owned()], + digest("scope:ordered"), + basis, + 65_536, + ); + let adapter = adapter(&root, &admitted, ["notes/evidence.txt".to_owned()]); + let mut store = store(); + let mut coordinator = must_ok(ExternalActionCoordinatorV1::recover(&store)); + let grant = claim(&mut store, &mut coordinator, &admitted, &adapter, "ordered"); + + root.write("notes/evidence.txt", bytes); + let candidate = must_ok(adapter.observe(&grant, &admitted)); + assert_eq!(candidate.kind, ExternalActionSettlementKindV1::Succeeded); + let settled = must_ok(adapter.admit_settlement( + &mut store, + &mut coordinator, + context("ordered:settlement"), + &admitted, + grant, + candidate, + )); + assert_eq!( + settled.settlement().kind, + ExternalActionSettlementKindV1::Succeeded + ); + assert_ne!(settled.settlement_commit_digest(), [0; 32]); +} + +#[test] +fn absolute_parent_escaped_and_unauthorized_paths_settle_as_rejected() { + for (index, path) in [ + "/etc/passwd", + "../outside", + "nested/../../outside", + "other.txt", + ] + .into_iter() + .enumerate() + { + let root = TempRoot::new(&format!("path-{index}")); + root.write("allowed.txt", b"allowed"); + let admitted = admitted_request( + must_ok(u8::try_from(index + 10)), + [path.to_owned()], + digest(&format!("scope:path-{index}")), + digest(&format!("basis:path-{index}")), + 65_536, + ); + let adapter = adapter(&root, &admitted, ["allowed.txt".to_owned()]); + let mut store = store(); + let mut coordinator = must_ok(ExternalActionCoordinatorV1::recover(&store)); + let grant = claim( + &mut store, + &mut coordinator, + &admitted, + &adapter, + &format!("path-{index}"), + ); + let candidate = must_ok(adapter.observe(&grant, &admitted)); + assert_eq!(candidate.kind, ExternalActionSettlementKindV1::Rejected); + must_ok(adapter.admit_settlement( + &mut store, + &mut coordinator, + context(&format!("path-{index}:settlement")), + &admitted, + grant, + candidate, + )); + } +} + +#[test] +fn duplicate_paths_settle_as_rejected() { + let root = TempRoot::new("duplicate-path"); + root.write("allowed.txt", b"allowed"); + let admitted = admitted_request( + 19, + ["allowed.txt".to_owned(), "allowed.txt".to_owned()], + digest("scope:duplicate-path"), + digest("basis:duplicate-path"), + 65_536, + ); + let adapter = adapter(&root, &admitted, ["allowed.txt".to_owned()]); + let mut store = store(); + let mut coordinator = must_ok(ExternalActionCoordinatorV1::recover(&store)); + let grant = claim( + &mut store, + &mut coordinator, + &admitted, + &adapter, + "duplicate-path", + ); + assert_eq!( + must_ok(adapter.observe(&grant, &admitted)).kind, + ExternalActionSettlementKindV1::Rejected + ); +} + +#[cfg(unix)] +#[test] +fn symlink_components_are_rejected_without_reading_the_target() { + use std::os::unix::fs::symlink; + + let root = TempRoot::new("symlink"); + root.write("outside.txt", b"must remain unread"); + must_ok(symlink( + root.path().join("outside.txt"), + root.path().join("link.txt"), + )); + let admitted = admitted_request( + 20, + ["link.txt".to_owned()], + digest("scope:symlink"), + digest("basis:symlink"), + 65_536, + ); + let adapter = adapter(&root, &admitted, ["link.txt".to_owned()]); + let mut store = store(); + let mut coordinator = must_ok(ExternalActionCoordinatorV1::recover(&store)); + let grant = claim(&mut store, &mut coordinator, &admitted, &adapter, "symlink"); + let candidate = must_ok(adapter.observe(&grant, &admitted)); + assert_eq!(candidate.kind, ExternalActionSettlementKindV1::Rejected); +} + +#[test] +fn exact_settlement_boundary_succeeds_and_one_byte_less_rejects() { + let root = TempRoot::new("budget"); + let bytes = vec![0x5a; 4096]; + root.write("blob.bin", &bytes); + let basis = bounded_workspace_observation_basis_v1([("blob.bin", bytes.as_slice())]); + let generous = admitted_request( + 30, + ["blob.bin".to_owned()], + digest("scope:budget"), + basis, + 65_536, + ); + let generous_adapter = adapter(&root, &generous, ["blob.bin".to_owned()]); + let mut generous_store = store(); + let mut generous_coordinator = must_ok(ExternalActionCoordinatorV1::recover(&generous_store)); + let generous_grant = claim( + &mut generous_store, + &mut generous_coordinator, + &generous, + &generous_adapter, + "budget:measure", + ); + let measured = must_ok(generous_adapter.observe(&generous_grant, &generous)); + let exact_budget = must_ok(u64::try_from(measured.canonical_result_bytes.len())); + + let exact = admitted_request( + 31, + ["blob.bin".to_owned()], + digest("scope:budget"), + basis, + exact_budget, + ); + let exact_adapter = adapter(&root, &exact, ["blob.bin".to_owned()]); + let mut exact_store = store(); + let mut exact_coordinator = must_ok(ExternalActionCoordinatorV1::recover(&exact_store)); + let exact_grant = claim( + &mut exact_store, + &mut exact_coordinator, + &exact, + &exact_adapter, + "budget:exact", + ); + assert_eq!( + must_ok(exact_adapter.observe(&exact_grant, &exact)).kind, + ExternalActionSettlementKindV1::Succeeded + ); + + let below = admitted_request( + 32, + ["blob.bin".to_owned()], + digest("scope:budget"), + basis, + exact_budget - 1, + ); + let below_adapter = adapter(&root, &below, ["blob.bin".to_owned()]); + let mut below_store = store(); + let mut below_coordinator = must_ok(ExternalActionCoordinatorV1::recover(&below_store)); + let below_grant = claim( + &mut below_store, + &mut below_coordinator, + &below, + &below_adapter, + "budget:below", + ); + assert_eq!( + must_ok(below_adapter.observe(&below_grant, &below)).kind, + ExternalActionSettlementKindV1::Rejected + ); +} + +#[test] +fn aggregate_file_bytes_cannot_exceed_the_request_budget() { + let root = TempRoot::new("aggregate-budget"); + let left = vec![0x4c; 3_000]; + let right = vec![0x52; 3_000]; + root.write("left.bin", &left); + root.write("right.bin", &right); + let basis = bounded_workspace_observation_basis_v1([ + ("left.bin", left.as_slice()), + ("right.bin", right.as_slice()), + ]); + let admitted = admitted_request( + 33, + ["left.bin".to_owned(), "right.bin".to_owned()], + digest("scope:aggregate-budget"), + basis, + 5_000, + ); + let adapter = adapter( + &root, + &admitted, + ["left.bin".to_owned(), "right.bin".to_owned()], + ); + let mut store = store(); + let mut coordinator = must_ok(ExternalActionCoordinatorV1::recover(&store)); + let grant = claim( + &mut store, + &mut coordinator, + &admitted, + &adapter, + "aggregate-budget", + ); + assert_eq!( + must_ok(adapter.observe(&grant, &admitted)).kind, + ExternalActionSettlementKindV1::Rejected + ); +} + +#[test] +fn stale_basis_settles_as_rejected() { + let root = TempRoot::new("stale"); + root.write("state.txt", b"current"); + let admitted = admitted_request( + 40, + ["state.txt".to_owned()], + digest("scope:stale"), + bounded_workspace_observation_basis_v1([("state.txt", b"prior".as_slice())]), + 65_536, + ); + let adapter = adapter(&root, &admitted, ["state.txt".to_owned()]); + let mut store = store(); + let mut coordinator = must_ok(ExternalActionCoordinatorV1::recover(&store)); + let grant = claim(&mut store, &mut coordinator, &admitted, &adapter, "stale"); + assert_eq!( + must_ok(adapter.observe(&grant, &admitted)).kind, + ExternalActionSettlementKindV1::Rejected + ); +} + +#[test] +fn definite_io_failure_settles_and_recovers_as_failed() { + let root = TempRoot::new("failed"); + let admitted = admitted_request( + 41, + ["missing.txt".to_owned()], + digest("scope:failed"), + digest("basis:failed"), + 65_536, + ); + let adapter = adapter(&root, &admitted, ["missing.txt".to_owned()]); + let mut store = store(); + let mut coordinator = must_ok(ExternalActionCoordinatorV1::recover(&store)); + let grant = claim(&mut store, &mut coordinator, &admitted, &adapter, "failed"); + let candidate = must_ok(adapter.observe(&grant, &admitted)); + assert_eq!(candidate.kind, ExternalActionSettlementKindV1::Failed); + must_ok(adapter.admit_settlement( + &mut store, + &mut coordinator, + context("failed:settlement"), + &admitted, + grant, + candidate, + )); + let recovered = must_ok(ExternalActionCoordinatorV1::recover(&store)); + assert_eq!( + must_some( + recovered + .observed_index() + .get(admitted.request().request_id()) + ) + .posture, + RecoveredExternalActionPostureV1::Settled(ExternalActionSettlementKindV1::Failed) + ); +} + +#[test] +fn malformed_settlement_cannot_bypass_the_profile_validator() { + let root = TempRoot::new("malformed"); + root.write("valid.txt", b"valid"); + let basis = bounded_workspace_observation_basis_v1([("valid.txt", b"valid".as_slice())]); + let admitted = admitted_request( + 50, + ["valid.txt".to_owned()], + digest("scope:malformed"), + basis, + 65_536, + ); + let adapter = adapter(&root, &admitted, ["valid.txt".to_owned()]); + let mut store = store(); + let mut coordinator = must_ok(ExternalActionCoordinatorV1::recover(&store)); + let grant = claim( + &mut store, + &mut coordinator, + &admitted, + &adapter, + "malformed", + ); + let mut candidate = must_ok(adapter.observe(&grant, &admitted)); + candidate.canonical_result_bytes.push(0); + candidate.declared_result_digest = blake3::hash(&candidate.canonical_result_bytes).into(); + assert_eq!( + adapter.admit_settlement( + &mut store, + &mut coordinator, + context("malformed:settlement"), + &admitted, + grant, + candidate, + ), + Err(BoundedWorkspaceObservationErrorV1::SchemaAdmissionFailed) + ); +} + +#[test] +fn noncanonical_success_file_order_cannot_bypass_the_profile_validator() { + let root = TempRoot::new("settlement-order"); + root.write("a.txt", b"a"); + root.write("b.txt", b"b"); + let basis = bounded_workspace_observation_basis_v1([ + ("a.txt", b"a".as_slice()), + ("b.txt", b"b".as_slice()), + ]); + let admitted = admitted_request( + 51, + ["a.txt".to_owned(), "b.txt".to_owned()], + digest("scope:settlement-order"), + basis, + 65_536, + ); + let adapter = adapter(&root, &admitted, ["a.txt".to_owned(), "b.txt".to_owned()]); + let mut store = store(); + let mut coordinator = must_ok(ExternalActionCoordinatorV1::recover(&store)); + let grant = claim( + &mut store, + &mut coordinator, + &admitted, + &adapter, + "settlement-order", + ); + let mut candidate = must_ok(adapter.observe(&grant, &admitted)); + let mut settlement = must_ok(decode_canonical_cbor_v1(&candidate.canonical_result_bytes)); + let CanonicalValueV1::Array(files) = map_field_mut(&mut settlement, "files") else { + panic!("expected settlement files"); + }; + files.reverse(); + candidate.canonical_result_bytes = encoded(&settlement); + candidate.declared_result_digest = blake3::hash(&candidate.canonical_result_bytes).into(); + assert_eq!( + adapter.admit_settlement( + &mut store, + &mut coordinator, + context("settlement-order:settlement"), + &admitted, + grant, + candidate, + ), + Err(BoundedWorkspaceObservationErrorV1::SchemaAdmissionFailed) + ); +} + +#[test] +fn requested_claimed_unknown_and_settled_postures_recover() { + let root = TempRoot::new("recovery"); + let bytes = b"recoverable"; + root.write("recover.txt", bytes); + let admitted = admitted_request( + 60, + ["recover.txt".to_owned()], + digest("scope:recovery"), + bounded_workspace_observation_basis_v1([("recover.txt", bytes.as_slice())]), + 65_536, + ); + let adapter = adapter(&root, &admitted, ["recover.txt".to_owned()]); + let mut store = store(); + let mut coordinator = must_ok(ExternalActionCoordinatorV1::recover(&store)); + let request = admitted.request(); + let recorded = must_ok(record_external_action_request( + &mut store, + &mut coordinator, + context("recovery:request"), + request, + )); + let recovered_requested = must_ok(ExternalActionCoordinatorV1::recover(&store)); + assert_eq!( + must_some( + recovered_requested + .observed_index() + .get(request.request_id()) + ) + .posture, + RecoveredExternalActionPostureV1::Requested + ); + + let authorization = must_ok( + ExternalActionAdapterRegistryV1::new([adapter.adapter_binding()]) + .authorize(&request, adapter.adapter_binding().adapter_id), + ); + let grant = must_ok(claim_external_action( + &mut store, + &mut coordinator, + context("recovery:claim"), + recorded, + authorization, + request.basis_digest, + 0, + digest("recovery:lease"), + )); + let recovered_claimed = must_ok(ExternalActionCoordinatorV1::recover(&store)); + assert_eq!( + must_some(recovered_claimed.observed_index().get(request.request_id())).posture, + RecoveredExternalActionPostureV1::Claimed + ); + + assert_eq!( + adapter.outcome_unknown(&grant, &admitted, [0; 32]), + Err(BoundedWorkspaceObservationErrorV1::SchemaAdmissionFailed) + ); + let candidate = + must_ok(adapter.outcome_unknown(&grant, &admitted, digest("recovery:ambiguous"))); + must_ok(adapter.admit_settlement( + &mut store, + &mut coordinator, + context("recovery:settlement"), + &admitted, + grant, + candidate, + )); + let recovered_settled = must_ok(ExternalActionCoordinatorV1::recover(&store)); + assert_eq!( + must_some(recovered_settled.observed_index().get(request.request_id())).posture, + RecoveredExternalActionPostureV1::Settled(ExternalActionSettlementKindV1::OutcomeUnknown) + ); +} + +#[test] +fn settled_replay_uses_wal_bytes_after_the_source_disappears() { + let root = TempRoot::new("replay"); + let bytes = b"retained"; + root.write("retained.txt", bytes); + let admitted = admitted_request( + 70, + ["retained.txt".to_owned()], + digest("scope:replay"), + bounded_workspace_observation_basis_v1([("retained.txt", bytes.as_slice())]), + 65_536, + ); + let adapter = adapter(&root, &admitted, ["retained.txt".to_owned()]); + let mut store = store(); + let mut coordinator = must_ok(ExternalActionCoordinatorV1::recover(&store)); + let grant = claim(&mut store, &mut coordinator, &admitted, &adapter, "replay"); + let candidate = must_ok(adapter.observe(&grant, &admitted)); + let expected_bytes = candidate.canonical_result_bytes.clone(); + must_ok(adapter.admit_settlement( + &mut store, + &mut coordinator, + context("replay:settlement"), + &admitted, + grant, + candidate, + )); + + must_ok(fs::remove_file(root.path().join("retained.txt"))); + let recovered = must_ok(ExternalActionCoordinatorV1::recover(&store)); + let replay = must_ok(recovered.admitted_settlement(admitted.request().request_id())); + assert_eq!(replay.settlement().canonical_result_bytes, expected_bytes); +} + +#[test] +fn fixed_seed_property_corpus_is_deterministic() { + const SEED: u64 = 0x51a7_7e11_cafe_babe; + let mut state = SEED; + for case in 0_u8..32 { + state ^= state << 13; + state ^= state >> 7; + state ^= state << 17; + let length = must_ok(usize::try_from((state % 257) + 1)); + let bytes = vec![case; length]; + let path = format!("property/{case:02}.bin"); + let root = TempRoot::new(&format!("property-{case}")); + root.write(&path, &bytes); + let basis = bounded_workspace_observation_basis_v1([(path.as_str(), bytes.as_slice())]); + let admitted = admitted_request( + case.saturating_add(80), + [path.clone()], + digest(&format!("scope:property-{case}")), + basis, + 65_536, + ); + let adapter = adapter(&root, &admitted, [path]); + let mut store = store(); + let mut coordinator = must_ok(ExternalActionCoordinatorV1::recover(&store)); + let grant = claim( + &mut store, + &mut coordinator, + &admitted, + &adapter, + &format!("property-{case}"), + ); + let first = must_ok(adapter.observe(&grant, &admitted)); + let second = must_ok(adapter.observe(&grant, &admitted)); + assert_eq!(first, second, "fixed seed {SEED:#x}, case {case}"); + } +} + +#[test] +fn bounded_stress_settles_many_requests_without_identity_collision() { + const REQUEST_COUNT: u8 = 64; + let root = TempRoot::new("stress"); + root.write("stress.txt", b"bounded"); + let basis = bounded_workspace_observation_basis_v1([("stress.txt", b"bounded".as_slice())]); + let mut store = store(); + let mut coordinator = must_ok(ExternalActionCoordinatorV1::recover(&store)); + + for index in 0..REQUEST_COUNT { + let admitted = admitted_request( + index.saturating_add(128), + ["stress.txt".to_owned()], + digest("scope:stress"), + basis, + 65_536, + ); + let adapter = adapter(&root, &admitted, ["stress.txt".to_owned()]); + let grant = claim( + &mut store, + &mut coordinator, + &admitted, + &adapter, + &format!("stress-{index}"), + ); + let candidate = must_ok(adapter.observe(&grant, &admitted)); + must_ok(adapter.admit_settlement( + &mut store, + &mut coordinator, + context(&format!("stress-{index}:settlement")), + &admitted, + grant, + candidate, + )); + } + + let recovered = must_ok(ExternalActionCoordinatorV1::recover(&store)); + assert_eq!(recovered.observed_index().len(), usize::from(REQUEST_COUNT)); + assert!(matches!( + recovered.recorded_request( + admitted_request( + 1, + ["stress.txt".to_owned()], + digest("scope:stress"), + basis, + 65_536, + ) + .request() + .request_id() + ), + Err(ExternalActionProtocolErrorV1::MissingRequest) + )); +} diff --git a/crates/warp-core/tests/fixtures/external_action/SOURCE.md b/crates/warp-core/tests/fixtures/external_action/SOURCE.md new file mode 100644 index 00000000..dc8fc053 --- /dev/null +++ b/crates/warp-core/tests/fixtures/external_action/SOURCE.md @@ -0,0 +1,20 @@ + + + +# Compiler-Owned External-Request Fixture + +These bytes were copied without modification from Edict commit +`97ef5eaf21f114abae74108d564b4a0c0f0b5d59`: + +- `fixtures/lawpack/workspace-snapshot/observe-workspace.core.cbor` +- `fixtures/lawpack/workspace-snapshot/observe-workspace.target-ir.cbor` + +Edict owns regeneration through: + +```sh +cargo xtask lawpack-goldens --write +``` + +Echo treats the files as received compiler artifacts. It independently checks +canonical encoding, the reviewed source-Core digest, the exact capability +closure, request-only shape, runtime request fields, and target identity. diff --git a/crates/warp-core/tests/fixtures/external_action/observe-workspace.core.cbor b/crates/warp-core/tests/fixtures/external_action/observe-workspace.core.cbor new file mode 100644 index 00000000..046e0832 Binary files /dev/null and b/crates/warp-core/tests/fixtures/external_action/observe-workspace.core.cbor differ diff --git a/crates/warp-core/tests/fixtures/external_action/observe-workspace.core.sha256 b/crates/warp-core/tests/fixtures/external_action/observe-workspace.core.sha256 new file mode 100644 index 00000000..0d9d3c7d --- /dev/null +++ b/crates/warp-core/tests/fixtures/external_action/observe-workspace.core.sha256 @@ -0,0 +1 @@ +sha256:8c99118cefb996cda44a860fd8a04a136ac81bf98356c8b909a31b28da136598 diff --git a/crates/warp-core/tests/fixtures/external_action/observe-workspace.target-ir.cbor b/crates/warp-core/tests/fixtures/external_action/observe-workspace.target-ir.cbor new file mode 100644 index 00000000..103ff0d6 Binary files /dev/null and b/crates/warp-core/tests/fixtures/external_action/observe-workspace.target-ir.cbor differ diff --git a/crates/warp-core/tests/fixtures/external_action/observe-workspace.target-ir.sha256 b/crates/warp-core/tests/fixtures/external_action/observe-workspace.target-ir.sha256 new file mode 100644 index 00000000..cb22456c --- /dev/null +++ b/crates/warp-core/tests/fixtures/external_action/observe-workspace.target-ir.sha256 @@ -0,0 +1 @@ +sha256:29cce7912efa7d2923b0c5631339bbac680c042b837ee1f4666aca2d746d3bfe diff --git a/docs/README.md b/docs/README.md index db5e3516..388aa1a9 100644 --- a/docs/README.md +++ b/docs/README.md @@ -35,6 +35,7 @@ causal history. Git history is the archive; GitHub owns live work and status. - [Obstructions](topics/Obstructions.md) - [Causal anchors](topics/CausalAnchors.md) - [Contract inverse admission](topics/ContractInverseAdmission.md) +- [External actions](topics/ExternalActions.md) - [WAL](topics/WAL.md) - [Runtime constellation](topics/RuntimeConstellation.md) - [Security posture and threat models](topics/security/README.md) diff --git a/docs/adr/0026-durable-external-action-settlement.md b/docs/adr/0026-durable-external-action-settlement.md index be9694dd..7e5ec96a 100644 --- a/docs/adr/0026-durable-external-action-settlement.md +++ b/docs/adr/0026-durable-external-action-settlement.md @@ -103,8 +103,12 @@ and unknown outcome codes. The schema-admission evidence is a protocol binding, not a general schema engine. Each operation profile must define which validator produces that -evidence. The bounded workspace-observation profile will supply the first -concrete validator. +evidence. The bounded workspace-observation profile supplies the first concrete +validator. It independently accepts exact compiler-owned Core and Target IR, +binds the request to the exact target profile and capability closure, and +validates strictly ordered retained path/content bytes, aggregate bounds, +nonzero evidence, and complete basis evidence before generic settlement +admission. ### WAL ownership @@ -188,8 +192,15 @@ invoking adapter execution. - Crash ambiguity has an explicit causal representation. - Operation-specific idempotency and reconciliation laws remain mandatory; Echo does not claim general exactly-once external execution. -- The current protocol proves the durable lifecycle without implementing a - filesystem, process, network, Git, GitHub, timer, or model adapter. +- The first capability-rooted read-only workspace adapter is implemented. + Compiler admission independently reconstructs the request projection from + Core, enforces both declared and host-owned evaluation budgets, and requires + enough settlement capacity for every terminal posture. Successful + settlements must cover exactly the admitted path aperture. Settlement + admission revalidates the live registry grant, while nonblocking no-follow + opens refuse special-file substitution without stalling. + Process, network, Git, GitHub, timer, model, and mutation adapters remain + absent. ## Rejected Alternatives @@ -216,8 +227,11 @@ deterministic validation and a separately authorized adapter govern mutation. ## Evidence - `crates/warp-core/src/external_action.rs` +- `crates/warp-core/src/external_action_adapter.rs` - `crates/warp-core/src/causal_wal.rs` - `crates/warp-core/tests/external_action_protocol_tests.rs` +- `crates/warp-core/tests/bounded_workspace_observation_tests.rs` +- [External Actions](../topics/ExternalActions.md) - [WAL](../topics/WAL.md) - [Runtime Authority](../topics/RuntimeAuthority.md) - [Security And Authority Boundaries](../topics/security/AuthorityBoundaries.md) diff --git a/docs/architecture/application-contract-hosting.md b/docs/architecture/application-contract-hosting.md index 1d321544..0c893915 100644 --- a/docs/architecture/application-contract-hosting.md +++ b/docs/architecture/application-contract-hosting.md @@ -1142,3 +1142,16 @@ those commits. Arbitrary recovery reports are observation-only; they cannot reconstruct adapter work grants or resumable settlement facts. Checked local recovery validates the frontier commitment and storage snapshot before restoring those authorities. + +The first concrete adapter profile is bounded workspace observation. Echo +independently accepts exact compiler-owned Core and Target IR, requires one +non-callable request in its digest-locked capability closure, independently +corroborates the complete Target IR request against Core, enforces declared +evaluation budgets below Echo-owned ceilings, and derives the generic durable +request. The adapter then holds one capability-rooted directory and an explicit +relative-path set. It can read only after a claim commit, refuses traversal, +symlinks, and special files through nonblocking no-follow opens, requires the +successful settlement aperture to equal the admitted request, revalidates the +registry grant before settlement, and retains replay bytes before deterministic +resumption. This profile adds no application noun, native application callback, +provider import, shell, or ambient filesystem capability. diff --git a/docs/topics/ExternalActions.md b/docs/topics/ExternalActions.md new file mode 100644 index 00000000..81133f8f --- /dev/null +++ b/docs/topics/ExternalActions.md @@ -0,0 +1,128 @@ + + + +# External Actions + +External actions are typed boundary crossings governed by Edict decisions, +Echo history, and separately authorized host adapters. They are not callable +imports, native callbacks, or a second application execution lifecycle. + +## Compiler Admission + +`admit_edict_external_action_request_v1` accepts exact canonical Core, Target +IR, application input, intent name, and worldline identity. Admission: + +1. independently decodes canonical Edict bytes; +2. recomputes the reviewed Core and Target IR digests; +3. requires the Echo Target IR domain and digest-locked target profile; +4. requires the Target IR source-Core binding to match the supplied Core and + independently proves that the complete Target IR request is the exact + projection of the supplied Core request; +5. requires exactly one request, its exact result and basis bindings, and zero + callable target steps; +6. requires the request operation to occur exactly in the digest-locked + capability closure; +7. evaluates only argument-rooted local, field, record, and constant request + expressions under the compiler-declared Core budget and Echo ceilings of + 4,096 steps, 4 MiB retained allocation, and 1 MiB output; +8. validates runtime scope, basis, byte, and attempt bounds; and +9. derives the generic `ExternalActionRequestV1`, with the target profile and + operation identities both committed by its operation identity. + +No provider component executes on this route. The provider/lowerer/verifier +seam remains deterministic and capability-denied. + +The v1 request profile expects operation input bytes, 32-byte authority and +basis commitments, a retained-settlement bound, and exactly one attempt. A +call expression or callable Target IR step is outside this admission profile. +Admission also requires enough retained-settlement capacity to encode every +terminal posture; a request cannot select a budget that makes rejection, +failure, or ambiguity unrecordable. + +## First Adapter Profile + +`BoundedWorkspaceObservationAdapterV1` is the first operation-specific +adapter. Runtime configuration supplies: + +- one capability-rooted directory; +- an explicit set of permitted relative paths; +- the exact operation, input schema, settlement schema, reconciliation law, + authority scope, and adapter identities. + +Opening the adapter retains directory-relative authority. Observation rejects +empty, absolute, parent-traversing, non-normalized, backslash, escaped, +duplicate, unauthorized, and symlinked paths. Directory components and the +final file are opened without following symlinks. Pre-open and post-open +metadata must both identify a regular file, and the final no-follow open is +nonblocking so a substituted FIFO cannot stall adapter execution. The +request's retained-settlement bound limits aggregate file bytes during reads as +well as the final canonical settlement. + +`cap-std` and `cap-fs-ext` are direct `warp-core` dependencies because this +boundary needs an unforgeable directory capability plus component-wise +no-follow opens. Both packages were already pinned in the workspace lockfile; +the adapter does not introduce an ambient path or shell interface. + +## Ordering + +The adapter accepts only `ExternalActionClaimGrantV1`. That grant exists only +after Echo commits: + +```text +REQUESTED + -> CLAIMED + -> adapter observation + -> validated settlement candidate + -> SETTLED +``` + +Compiler admission and request recording do not read the configured files. +The operation input is re-bound to its recorded digest before adapter work. +The runtime-owned registry binds the exact operation, scope, and adapter. + +## Settlement + +A successful settlement retains canonical bytes for every sorted relative +path, each content digest, the complete snapshot basis, and external evidence. +The basis commits every path and byte sequence. A mismatch yields a typed +`Rejected` settlement rather than silently reading a different snapshot. + +Path policy, stale basis, and settlement-budget failures are typed +rejections. Definite host I/O failure is `Failed`. Reconciliation may admit +`OutcomeUnknown` with explicit nonzero evidence. + +Before generic WAL admission, the operation profile independently validates: + +- candidate request, attempt, adapter, schema, basis, and result digest; +- the current registry profile and authority grant against the admitted + request; +- exact canonical settlement shape; +- outcome/posture agreement; +- exact equality between requested and returned path apertures on success; +- nonempty, strictly sorted, unique, valid relative paths on success; +- per-file content digests and nonzero external evidence; +- complete snapshot-basis equality on success; +- external evidence binding; and +- operation-specific schema-admission evidence. + +Malformed or substituted candidates fail before the settlement transaction. + +## Recovery And Replay + +Recovery reconstructs `Requested`, `Claimed`, or the exact settled outcome +through the generic external-action coordinator. A recovered claim is a +reconciliation obligation, not permission to reread the workspace. + +Settled replay returns the canonical bytes retained in the WAL. It does not +open the capability directory again. Removing or mutating source files after +settlement therefore cannot change replay. + +## Evidence + +- `crates/warp-core/src/external_action_adapter.rs` +- `crates/warp-core/tests/bounded_workspace_observation_tests.rs` +- `crates/warp-core/src/external_action.rs` +- `crates/warp-core/tests/external_action_protocol_tests.rs` +- [ADR 0026](../adr/0026-durable-external-action-settlement.md) +- [Runtime Authority](RuntimeAuthority.md) +- [WAL](WAL.md) diff --git a/docs/topics/README.md b/docs/topics/README.md index 5a2d8bc6..04493eed 100644 --- a/docs/topics/README.md +++ b/docs/topics/README.md @@ -12,6 +12,7 @@ ADR. - [Causal anchors](CausalAnchors.md) - [Contract inverse admission](ContractInverseAdmission.md) +- [External actions](ExternalActions.md) - [Generated rule authorship](GeneratedRules.md) - [Obstructions](Obstructions.md) - [Runtime authority](RuntimeAuthority.md) diff --git a/docs/topics/RuntimeAuthority.md b/docs/topics/RuntimeAuthority.md index 6c47c6bc..98990590 100644 --- a/docs/topics/RuntimeAuthority.md +++ b/docs/topics/RuntimeAuthority.md @@ -58,6 +58,11 @@ evidence posture. - Replay consumes the admitted settlement bytes and never invokes the adapter. - `OutcomeUnknown` is a first-class terminal observation, not an alias for failure. +- The bounded workspace-observation profile independently admits exact Edict + Core and Echo-profile Target IR, binds target profile plus portable operation + into the runtime operation identity, then exposes only an explicit + relative-path set through one capability-rooted, no-follow adapter. Compiler + admission and request recording perform no filesystem read. ## Host Boundary @@ -71,6 +76,7 @@ adapters. - [Registry/provider/host boundary](../adr/0015-registry-provider-host-boundary.md) - [Durable external-action settlement](../adr/0026-durable-external-action-settlement.md) +- [External actions](ExternalActions.md) - `docs/architecture/application-contract-hosting.md` - `crates/warp-core/src/trusted_runtime_host.rs` - `crates/warp-core/src/engine_impl.rs` diff --git a/docs/topics/WAL.md b/docs/topics/WAL.md index 1d84ca36..ee8dce68 100644 --- a/docs/topics/WAL.md +++ b/docs/topics/WAL.md @@ -186,6 +186,14 @@ facts. Raw WAL builders and commit flushes lack its opaque capability, and the coordinator derives LSN and predecessor coordinates from the validated local continuation. +The first operation profile independently admits exact compiler-produced Edict +Core and Target IR before deriving the generic request. Its capability-rooted +workspace observer receives only a post-claim work grant and an explicit +relative-path set. Successful settlement retains complete path/content bytes +and a basis commitment; operation-specific schema validation runs before the +generic settlement transaction. Replay consumes those retained bytes without +opening the workspace again. + ## Boundaries The WAL belongs to the trusted runtime host. Application-facing code can submit diff --git a/schemas/edict-provider/generated/v1/evidence/provenance.provider-generation.json b/schemas/edict-provider/generated/v1/evidence/provenance.provider-generation.json index 632d8807..c4072d2d 100644 --- a/schemas/edict-provider/generated/v1/evidence/provenance.provider-generation.json +++ b/schemas/edict-provider/generated/v1/evidence/provenance.provider-generation.json @@ -1 +1 @@ -{"apiVersion":"wesley.generation-provenance-manifest/v1","contractVersions":{"generatorAbi":"wesley.extension-generator/v1","inputSchema":"wesley.extension-generation-input/v1","provenanceSchema":"wesley.generation-provenance-manifest/v1"},"emittedArtifacts":[{"coordinate":"echo.dpo-authority-facts@1","digest":"sha256:e2a6a539a7156296c18ff1bc02f80dcabf62b42bb4314d0737527869e43986d6"},{"coordinate":"echo.dpo-lawpack-authority-facts@1","digest":"sha256:ce1af559551f8b8242c4b290b620c5a28323b9e43f8bc6bb161a191001b6f4b9"},{"coordinate":"echo.dpo-lawpack@1","digest":"sha256:8c570362671a0b1cd1a992d8210e4a90ffd153ad06dcdf1e0cb36033cc971e2c"},{"coordinate":"echo.dpo.registration/v1","digest":"sha256:4ef3aaad0d2131ba7129a3e2ae178d10c44f7e9b062af2c1fe211de781462f34"},{"coordinate":"echo.dpo@1","digest":"sha256:1b105d1b1f6cdf5fecdef98b7adeb238525047d43581fe9fd8c44fd213e1788e"},{"coordinate":"echo.provider-artifacts.cddl@1","digest":"sha256:730ce72f6658fe8cc9302221c32ad5992492e190674ddd69c1b164749d2bb669"}],"generationInputDigest":"sha256:57f5f67172544014af9c92e658301b9fcec69fd77fba9d251b258b59e3591380","generator":{"coordinate":"echo-wesley-gen.provider-artifact-generator@1","digest":"sha256:434467463d311ae5df7b3f778b75cb19113ec31ec2b82e572316c2f788f35346","version":"0.1.0"},"settingsDigest":"sha256:0f708e76898de6fdb8186352e81d0f5c445adf94bb6c7de9204952d9fe913d4a","sourceArtifacts":[{"coordinate":"echo.semantic-schema@1","digest":"sha256:322d9eb5eb8273108eaa602f2eb58ba591efcf829fecb10bb705717efae18852"},{"coordinate":"edict.provider-contract-pack.cddl@1","digest":"sha256:ff5405708185e0bcf33dac263000fcb772c79e21145ad69da7cad0692e1a9552"},{"coordinate":"edict.provider-contract-pack.manifest@1","digest":"sha256:e31d0452bf9d8ba84eaa1ce8c09fa1c25f740decd0ab08cd9c2fde533bdef1fa"}]} \ No newline at end of file +{"apiVersion":"wesley.generation-provenance-manifest/v1","contractVersions":{"generatorAbi":"wesley.extension-generator/v1","inputSchema":"wesley.extension-generation-input/v1","provenanceSchema":"wesley.generation-provenance-manifest/v1"},"emittedArtifacts":[{"coordinate":"echo.dpo-authority-facts@1","digest":"sha256:e2a6a539a7156296c18ff1bc02f80dcabf62b42bb4314d0737527869e43986d6"},{"coordinate":"echo.dpo-lawpack-authority-facts@1","digest":"sha256:ce1af559551f8b8242c4b290b620c5a28323b9e43f8bc6bb161a191001b6f4b9"},{"coordinate":"echo.dpo-lawpack@1","digest":"sha256:8c570362671a0b1cd1a992d8210e4a90ffd153ad06dcdf1e0cb36033cc971e2c"},{"coordinate":"echo.dpo.registration/v1","digest":"sha256:4ef3aaad0d2131ba7129a3e2ae178d10c44f7e9b062af2c1fe211de781462f34"},{"coordinate":"echo.dpo@1","digest":"sha256:1b105d1b1f6cdf5fecdef98b7adeb238525047d43581fe9fd8c44fd213e1788e"},{"coordinate":"echo.provider-artifacts.cddl@1","digest":"sha256:730ce72f6658fe8cc9302221c32ad5992492e190674ddd69c1b164749d2bb669"}],"generationInputDigest":"sha256:57f5f67172544014af9c92e658301b9fcec69fd77fba9d251b258b59e3591380","generator":{"coordinate":"echo-wesley-gen.provider-artifact-generator@1","digest":"sha256:48e583f8c29af45bb89d0e4f883b4fc478f7ec65f887612b865ebd4e1895b0a8","version":"0.1.0"},"settingsDigest":"sha256:0f708e76898de6fdb8186352e81d0f5c445adf94bb6c7de9204952d9fe913d4a","sourceArtifacts":[{"coordinate":"echo.semantic-schema@1","digest":"sha256:322d9eb5eb8273108eaa602f2eb58ba591efcf829fecb10bb705717efae18852"},{"coordinate":"edict.provider-contract-pack.cddl@1","digest":"sha256:ff5405708185e0bcf33dac263000fcb772c79e21145ad69da7cad0692e1a9552"},{"coordinate":"edict.provider-contract-pack.manifest@1","digest":"sha256:e31d0452bf9d8ba84eaa1ce8c09fa1c25f740decd0ab08cd9c2fde533bdef1fa"}]} \ No newline at end of file diff --git a/schemas/edict-provider/generated/v1/evidence/review.provider-generation.json b/schemas/edict-provider/generated/v1/evidence/review.provider-generation.json index 0ba69ca3..1179388c 100644 --- a/schemas/edict-provider/generated/v1/evidence/review.provider-generation.json +++ b/schemas/edict-provider/generated/v1/evidence/review.provider-generation.json @@ -1 +1 @@ -{"apiVersion":"wesley.generation-review/v1","authoritative":false,"emittedArtifacts":[{"coordinate":"echo.dpo-authority-facts@1","digest":"sha256:e2a6a539a7156296c18ff1bc02f80dcabf62b42bb4314d0737527869e43986d6"},{"coordinate":"echo.dpo-lawpack-authority-facts@1","digest":"sha256:ce1af559551f8b8242c4b290b620c5a28323b9e43f8bc6bb161a191001b6f4b9"},{"coordinate":"echo.dpo-lawpack@1","digest":"sha256:8c570362671a0b1cd1a992d8210e4a90ffd153ad06dcdf1e0cb36033cc971e2c"},{"coordinate":"echo.dpo.registration/v1","digest":"sha256:4ef3aaad0d2131ba7129a3e2ae178d10c44f7e9b062af2c1fe211de781462f34"},{"coordinate":"echo.dpo@1","digest":"sha256:1b105d1b1f6cdf5fecdef98b7adeb238525047d43581fe9fd8c44fd213e1788e"},{"coordinate":"echo.provider-artifacts.cddl@1","digest":"sha256:730ce72f6658fe8cc9302221c32ad5992492e190674ddd69c1b164749d2bb669"}],"generationInputDigest":"sha256:57f5f67172544014af9c92e658301b9fcec69fd77fba9d251b258b59e3591380","generator":{"coordinate":"echo-wesley-gen.provider-artifact-generator@1","digest":"sha256:434467463d311ae5df7b3f778b75cb19113ec31ec2b82e572316c2f788f35346","version":"0.1.0"},"projectionRoles":["authority-facts.echo-dpo","authority-facts.echo-lawpack","generated-artifact-profile.echo-dpo-registration","lawpack.echo-dpo","schema.echo-provider-artifacts","target-profile.echo-dpo"],"provenanceManifestDigest":"sha256:998426658fbcbd44bff4f28d0d4c82df7aaf05bdbcdb736db9cf481a1948ec82","sourceArtifacts":[{"coordinate":"echo.semantic-schema@1","digest":"sha256:322d9eb5eb8273108eaa602f2eb58ba591efcf829fecb10bb705717efae18852"},{"coordinate":"edict.provider-contract-pack.cddl@1","digest":"sha256:ff5405708185e0bcf33dac263000fcb772c79e21145ad69da7cad0692e1a9552"},{"coordinate":"edict.provider-contract-pack.manifest@1","digest":"sha256:e31d0452bf9d8ba84eaa1ce8c09fa1c25f740decd0ab08cd9c2fde533bdef1fa"}]} \ No newline at end of file +{"apiVersion":"wesley.generation-review/v1","authoritative":false,"emittedArtifacts":[{"coordinate":"echo.dpo-authority-facts@1","digest":"sha256:e2a6a539a7156296c18ff1bc02f80dcabf62b42bb4314d0737527869e43986d6"},{"coordinate":"echo.dpo-lawpack-authority-facts@1","digest":"sha256:ce1af559551f8b8242c4b290b620c5a28323b9e43f8bc6bb161a191001b6f4b9"},{"coordinate":"echo.dpo-lawpack@1","digest":"sha256:8c570362671a0b1cd1a992d8210e4a90ffd153ad06dcdf1e0cb36033cc971e2c"},{"coordinate":"echo.dpo.registration/v1","digest":"sha256:4ef3aaad0d2131ba7129a3e2ae178d10c44f7e9b062af2c1fe211de781462f34"},{"coordinate":"echo.dpo@1","digest":"sha256:1b105d1b1f6cdf5fecdef98b7adeb238525047d43581fe9fd8c44fd213e1788e"},{"coordinate":"echo.provider-artifacts.cddl@1","digest":"sha256:730ce72f6658fe8cc9302221c32ad5992492e190674ddd69c1b164749d2bb669"}],"generationInputDigest":"sha256:57f5f67172544014af9c92e658301b9fcec69fd77fba9d251b258b59e3591380","generator":{"coordinate":"echo-wesley-gen.provider-artifact-generator@1","digest":"sha256:48e583f8c29af45bb89d0e4f883b4fc478f7ec65f887612b865ebd4e1895b0a8","version":"0.1.0"},"projectionRoles":["authority-facts.echo-dpo","authority-facts.echo-lawpack","generated-artifact-profile.echo-dpo-registration","lawpack.echo-dpo","schema.echo-provider-artifacts","target-profile.echo-dpo"],"provenanceManifestDigest":"sha256:45670aa10682d15f8349865c7e94d3938b8229f83dab91cb4b7f4865b035d656","sourceArtifacts":[{"coordinate":"echo.semantic-schema@1","digest":"sha256:322d9eb5eb8273108eaa602f2eb58ba591efcf829fecb10bb705717efae18852"},{"coordinate":"edict.provider-contract-pack.cddl@1","digest":"sha256:ff5405708185e0bcf33dac263000fcb772c79e21145ad69da7cad0692e1a9552"},{"coordinate":"edict.provider-contract-pack.manifest@1","digest":"sha256:e31d0452bf9d8ba84eaa1ce8c09fa1c25f740decd0ab08cd9c2fde533bdef1fa"}]} \ No newline at end of file diff --git a/schemas/edict-provider/package/v1/generated/evidence/provenance.provider-generation.json b/schemas/edict-provider/package/v1/generated/evidence/provenance.provider-generation.json index 632d8807..c4072d2d 100644 --- a/schemas/edict-provider/package/v1/generated/evidence/provenance.provider-generation.json +++ b/schemas/edict-provider/package/v1/generated/evidence/provenance.provider-generation.json @@ -1 +1 @@ -{"apiVersion":"wesley.generation-provenance-manifest/v1","contractVersions":{"generatorAbi":"wesley.extension-generator/v1","inputSchema":"wesley.extension-generation-input/v1","provenanceSchema":"wesley.generation-provenance-manifest/v1"},"emittedArtifacts":[{"coordinate":"echo.dpo-authority-facts@1","digest":"sha256:e2a6a539a7156296c18ff1bc02f80dcabf62b42bb4314d0737527869e43986d6"},{"coordinate":"echo.dpo-lawpack-authority-facts@1","digest":"sha256:ce1af559551f8b8242c4b290b620c5a28323b9e43f8bc6bb161a191001b6f4b9"},{"coordinate":"echo.dpo-lawpack@1","digest":"sha256:8c570362671a0b1cd1a992d8210e4a90ffd153ad06dcdf1e0cb36033cc971e2c"},{"coordinate":"echo.dpo.registration/v1","digest":"sha256:4ef3aaad0d2131ba7129a3e2ae178d10c44f7e9b062af2c1fe211de781462f34"},{"coordinate":"echo.dpo@1","digest":"sha256:1b105d1b1f6cdf5fecdef98b7adeb238525047d43581fe9fd8c44fd213e1788e"},{"coordinate":"echo.provider-artifacts.cddl@1","digest":"sha256:730ce72f6658fe8cc9302221c32ad5992492e190674ddd69c1b164749d2bb669"}],"generationInputDigest":"sha256:57f5f67172544014af9c92e658301b9fcec69fd77fba9d251b258b59e3591380","generator":{"coordinate":"echo-wesley-gen.provider-artifact-generator@1","digest":"sha256:434467463d311ae5df7b3f778b75cb19113ec31ec2b82e572316c2f788f35346","version":"0.1.0"},"settingsDigest":"sha256:0f708e76898de6fdb8186352e81d0f5c445adf94bb6c7de9204952d9fe913d4a","sourceArtifacts":[{"coordinate":"echo.semantic-schema@1","digest":"sha256:322d9eb5eb8273108eaa602f2eb58ba591efcf829fecb10bb705717efae18852"},{"coordinate":"edict.provider-contract-pack.cddl@1","digest":"sha256:ff5405708185e0bcf33dac263000fcb772c79e21145ad69da7cad0692e1a9552"},{"coordinate":"edict.provider-contract-pack.manifest@1","digest":"sha256:e31d0452bf9d8ba84eaa1ce8c09fa1c25f740decd0ab08cd9c2fde533bdef1fa"}]} \ No newline at end of file +{"apiVersion":"wesley.generation-provenance-manifest/v1","contractVersions":{"generatorAbi":"wesley.extension-generator/v1","inputSchema":"wesley.extension-generation-input/v1","provenanceSchema":"wesley.generation-provenance-manifest/v1"},"emittedArtifacts":[{"coordinate":"echo.dpo-authority-facts@1","digest":"sha256:e2a6a539a7156296c18ff1bc02f80dcabf62b42bb4314d0737527869e43986d6"},{"coordinate":"echo.dpo-lawpack-authority-facts@1","digest":"sha256:ce1af559551f8b8242c4b290b620c5a28323b9e43f8bc6bb161a191001b6f4b9"},{"coordinate":"echo.dpo-lawpack@1","digest":"sha256:8c570362671a0b1cd1a992d8210e4a90ffd153ad06dcdf1e0cb36033cc971e2c"},{"coordinate":"echo.dpo.registration/v1","digest":"sha256:4ef3aaad0d2131ba7129a3e2ae178d10c44f7e9b062af2c1fe211de781462f34"},{"coordinate":"echo.dpo@1","digest":"sha256:1b105d1b1f6cdf5fecdef98b7adeb238525047d43581fe9fd8c44fd213e1788e"},{"coordinate":"echo.provider-artifacts.cddl@1","digest":"sha256:730ce72f6658fe8cc9302221c32ad5992492e190674ddd69c1b164749d2bb669"}],"generationInputDigest":"sha256:57f5f67172544014af9c92e658301b9fcec69fd77fba9d251b258b59e3591380","generator":{"coordinate":"echo-wesley-gen.provider-artifact-generator@1","digest":"sha256:48e583f8c29af45bb89d0e4f883b4fc478f7ec65f887612b865ebd4e1895b0a8","version":"0.1.0"},"settingsDigest":"sha256:0f708e76898de6fdb8186352e81d0f5c445adf94bb6c7de9204952d9fe913d4a","sourceArtifacts":[{"coordinate":"echo.semantic-schema@1","digest":"sha256:322d9eb5eb8273108eaa602f2eb58ba591efcf829fecb10bb705717efae18852"},{"coordinate":"edict.provider-contract-pack.cddl@1","digest":"sha256:ff5405708185e0bcf33dac263000fcb772c79e21145ad69da7cad0692e1a9552"},{"coordinate":"edict.provider-contract-pack.manifest@1","digest":"sha256:e31d0452bf9d8ba84eaa1ce8c09fa1c25f740decd0ab08cd9c2fde533bdef1fa"}]} \ No newline at end of file diff --git a/schemas/edict-provider/package/v1/generated/evidence/review.provider-generation.json b/schemas/edict-provider/package/v1/generated/evidence/review.provider-generation.json index 0ba69ca3..1179388c 100644 --- a/schemas/edict-provider/package/v1/generated/evidence/review.provider-generation.json +++ b/schemas/edict-provider/package/v1/generated/evidence/review.provider-generation.json @@ -1 +1 @@ -{"apiVersion":"wesley.generation-review/v1","authoritative":false,"emittedArtifacts":[{"coordinate":"echo.dpo-authority-facts@1","digest":"sha256:e2a6a539a7156296c18ff1bc02f80dcabf62b42bb4314d0737527869e43986d6"},{"coordinate":"echo.dpo-lawpack-authority-facts@1","digest":"sha256:ce1af559551f8b8242c4b290b620c5a28323b9e43f8bc6bb161a191001b6f4b9"},{"coordinate":"echo.dpo-lawpack@1","digest":"sha256:8c570362671a0b1cd1a992d8210e4a90ffd153ad06dcdf1e0cb36033cc971e2c"},{"coordinate":"echo.dpo.registration/v1","digest":"sha256:4ef3aaad0d2131ba7129a3e2ae178d10c44f7e9b062af2c1fe211de781462f34"},{"coordinate":"echo.dpo@1","digest":"sha256:1b105d1b1f6cdf5fecdef98b7adeb238525047d43581fe9fd8c44fd213e1788e"},{"coordinate":"echo.provider-artifacts.cddl@1","digest":"sha256:730ce72f6658fe8cc9302221c32ad5992492e190674ddd69c1b164749d2bb669"}],"generationInputDigest":"sha256:57f5f67172544014af9c92e658301b9fcec69fd77fba9d251b258b59e3591380","generator":{"coordinate":"echo-wesley-gen.provider-artifact-generator@1","digest":"sha256:434467463d311ae5df7b3f778b75cb19113ec31ec2b82e572316c2f788f35346","version":"0.1.0"},"projectionRoles":["authority-facts.echo-dpo","authority-facts.echo-lawpack","generated-artifact-profile.echo-dpo-registration","lawpack.echo-dpo","schema.echo-provider-artifacts","target-profile.echo-dpo"],"provenanceManifestDigest":"sha256:998426658fbcbd44bff4f28d0d4c82df7aaf05bdbcdb736db9cf481a1948ec82","sourceArtifacts":[{"coordinate":"echo.semantic-schema@1","digest":"sha256:322d9eb5eb8273108eaa602f2eb58ba591efcf829fecb10bb705717efae18852"},{"coordinate":"edict.provider-contract-pack.cddl@1","digest":"sha256:ff5405708185e0bcf33dac263000fcb772c79e21145ad69da7cad0692e1a9552"},{"coordinate":"edict.provider-contract-pack.manifest@1","digest":"sha256:e31d0452bf9d8ba84eaa1ce8c09fa1c25f740decd0ab08cd9c2fde533bdef1fa"}]} \ No newline at end of file +{"apiVersion":"wesley.generation-review/v1","authoritative":false,"emittedArtifacts":[{"coordinate":"echo.dpo-authority-facts@1","digest":"sha256:e2a6a539a7156296c18ff1bc02f80dcabf62b42bb4314d0737527869e43986d6"},{"coordinate":"echo.dpo-lawpack-authority-facts@1","digest":"sha256:ce1af559551f8b8242c4b290b620c5a28323b9e43f8bc6bb161a191001b6f4b9"},{"coordinate":"echo.dpo-lawpack@1","digest":"sha256:8c570362671a0b1cd1a992d8210e4a90ffd153ad06dcdf1e0cb36033cc971e2c"},{"coordinate":"echo.dpo.registration/v1","digest":"sha256:4ef3aaad0d2131ba7129a3e2ae178d10c44f7e9b062af2c1fe211de781462f34"},{"coordinate":"echo.dpo@1","digest":"sha256:1b105d1b1f6cdf5fecdef98b7adeb238525047d43581fe9fd8c44fd213e1788e"},{"coordinate":"echo.provider-artifacts.cddl@1","digest":"sha256:730ce72f6658fe8cc9302221c32ad5992492e190674ddd69c1b164749d2bb669"}],"generationInputDigest":"sha256:57f5f67172544014af9c92e658301b9fcec69fd77fba9d251b258b59e3591380","generator":{"coordinate":"echo-wesley-gen.provider-artifact-generator@1","digest":"sha256:48e583f8c29af45bb89d0e4f883b4fc478f7ec65f887612b865ebd4e1895b0a8","version":"0.1.0"},"projectionRoles":["authority-facts.echo-dpo","authority-facts.echo-lawpack","generated-artifact-profile.echo-dpo-registration","lawpack.echo-dpo","schema.echo-provider-artifacts","target-profile.echo-dpo"],"provenanceManifestDigest":"sha256:45670aa10682d15f8349865c7e94d3938b8229f83dab91cb4b7f4865b035d656","sourceArtifacts":[{"coordinate":"echo.semantic-schema@1","digest":"sha256:322d9eb5eb8273108eaa602f2eb58ba591efcf829fecb10bb705717efae18852"},{"coordinate":"edict.provider-contract-pack.cddl@1","digest":"sha256:ff5405708185e0bcf33dac263000fcb772c79e21145ad69da7cad0692e1a9552"},{"coordinate":"edict.provider-contract-pack.manifest@1","digest":"sha256:e31d0452bf9d8ba84eaa1ce8c09fa1c25f740decd0ab08cd9c2fde533bdef1fa"}]} \ No newline at end of file diff --git a/schemas/edict-provider/package/v1/provider-manifest.echo.json b/schemas/edict-provider/package/v1/provider-manifest.echo.json index 729f3463..d8227f27 100644 --- a/schemas/edict-provider/package/v1/provider-manifest.echo.json +++ b/schemas/edict-provider/package/v1/provider-manifest.echo.json @@ -3,7 +3,7 @@ "providerAbi": "edict:target-provider@1.0.0", "provider": { "coordinate": "echo.edict-provider@1", - "digest": "sha256:fe1a1f1c05e88bb3caeadb2d77fb17a906a4819674c50d75b2dcaca0fb6058ec" + "digest": "sha256:6685b7c629ae6955515d69158feb1d7db06af2193de7e5d13e1095101670b977" }, "artifacts": [ { @@ -21,7 +21,7 @@ }, "generator": { "coordinate": "echo-wesley-gen.provider-artifact-generator@1", - "digest": "sha256:434467463d311ae5df7b3f778b75cb19113ec31ec2b82e572316c2f788f35346" + "digest": "sha256:48e583f8c29af45bb89d0e4f883b4fc478f7ec65f887612b865ebd4e1895b0a8" } } }, @@ -40,7 +40,7 @@ }, "generator": { "coordinate": "echo-wesley-gen.provider-artifact-generator@1", - "digest": "sha256:434467463d311ae5df7b3f778b75cb19113ec31ec2b82e572316c2f788f35346" + "digest": "sha256:48e583f8c29af45bb89d0e4f883b4fc478f7ec65f887612b865ebd4e1895b0a8" } } }, @@ -59,7 +59,7 @@ }, "generator": { "coordinate": "echo-wesley-gen.provider-artifact-generator@1", - "digest": "sha256:434467463d311ae5df7b3f778b75cb19113ec31ec2b82e572316c2f788f35346" + "digest": "sha256:48e583f8c29af45bb89d0e4f883b4fc478f7ec65f887612b865ebd4e1895b0a8" } } }, @@ -78,7 +78,7 @@ }, "generator": { "coordinate": "echo-wesley-gen.provider-artifact-generator@1", - "digest": "sha256:434467463d311ae5df7b3f778b75cb19113ec31ec2b82e572316c2f788f35346" + "digest": "sha256:48e583f8c29af45bb89d0e4f883b4fc478f7ec65f887612b865ebd4e1895b0a8" } } }, @@ -102,7 +102,7 @@ "artifactKind": "generationProvenance", "resource": { "coordinate": "echo.edict-provider-generation-provenance@1", - "digest": "sha256:720076fdd6462ab5500e944408199a141d07eca8e4e73d542e01e7fcdc715ac4" + "digest": "sha256:42f43a9bd874fe7808f3edce7491899ec21c991d33526e5802e6a3ad1edd2020" }, "source": { "kind": "generated", @@ -112,7 +112,7 @@ }, "generator": { "coordinate": "echo-wesley-gen.provider-artifact-generator@1", - "digest": "sha256:434467463d311ae5df7b3f778b75cb19113ec31ec2b82e572316c2f788f35346" + "digest": "sha256:48e583f8c29af45bb89d0e4f883b4fc478f7ec65f887612b865ebd4e1895b0a8" } } }, @@ -121,7 +121,7 @@ "artifactKind": "reviewArtifact", "resource": { "coordinate": "echo.edict-provider-generation-review@1", - "digest": "sha256:592af76dab9f354a4afb663365990e8a14bf07c1721cdc2242319ad457ee5e2b" + "digest": "sha256:1459a631c3ca083de2ccfa3e73eded405b8cdf9b969a0ec6519c1e0484e34877" }, "source": { "kind": "generated", @@ -131,7 +131,7 @@ }, "generator": { "coordinate": "echo-wesley-gen.provider-artifact-generator@1", - "digest": "sha256:434467463d311ae5df7b3f778b75cb19113ec31ec2b82e572316c2f788f35346" + "digest": "sha256:48e583f8c29af45bb89d0e4f883b4fc478f7ec65f887612b865ebd4e1895b0a8" } } }, @@ -150,7 +150,7 @@ }, "generator": { "coordinate": "echo-wesley-gen.provider-artifact-generator@1", - "digest": "sha256:434467463d311ae5df7b3f778b75cb19113ec31ec2b82e572316c2f788f35346" + "digest": "sha256:48e583f8c29af45bb89d0e4f883b4fc478f7ec65f887612b865ebd4e1895b0a8" } } }, @@ -169,7 +169,7 @@ }, "generator": { "coordinate": "echo-wesley-gen.provider-artifact-generator@1", - "digest": "sha256:434467463d311ae5df7b3f778b75cb19113ec31ec2b82e572316c2f788f35346" + "digest": "sha256:48e583f8c29af45bb89d0e4f883b4fc478f7ec65f887612b865ebd4e1895b0a8" } } }, diff --git a/tests/edict-provider-host-v1/Cargo.lock b/tests/edict-provider-host-v1/Cargo.lock index c88c8b29..1370e206 100644 --- a/tests/edict-provider-host-v1/Cargo.lock +++ b/tests/edict-provider-host-v1/Cargo.lock @@ -26,6 +26,12 @@ version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" +[[package]] +name = "ambient-authority" +version = "0.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9d4ee0d472d1cd2e28c97dfa124b3d8d992e10eb0a035f33f5d12e3a177ba3b" + [[package]] name = "anyhow" version = "1.0.103" @@ -137,6 +143,48 @@ version = "1.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" +[[package]] +name = "cap-fs-ext" +version = "4.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d78e5a3368ae89b7cb68186411452b4b9fac8b41be9c19bf3f47c2d2c8e36e6b" +dependencies = [ + "cap-primitives", + "cap-std", + "io-lifetimes 3.0.1", + "windows-sys 0.61.2", +] + +[[package]] +name = "cap-primitives" +version = "4.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdadbd7c002d3a484b35243669abdae85a0ebaded5a61117169dc3400f9a7ff0" +dependencies = [ + "ambient-authority", + "fs-set-times", + "io-extras", + "io-lifetimes 3.0.1", + "ipnet", + "maybe-owned", + "rustix", + "rustix-linux-procfs", + "windows-sys 0.61.2", + "winx", +] + +[[package]] +name = "cap-std" +version = "4.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7281235d6e96d3544ca18bba9049be92f4190f8d923e3caef1b5f66cfa752608" +dependencies = [ + "cap-primitives", + "io-extras", + "io-lifetimes 3.0.1", + "rustix", +] + [[package]] name = "cc" version = "1.2.67" @@ -538,7 +586,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys", + "windows-sys 0.61.2", ] [[package]] @@ -571,6 +619,17 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" +[[package]] +name = "fs-set-times" +version = "0.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94e7099f6313ecacbe1256e8ff9d617b75d1bcb16a6fddef94866d225a01a14a" +dependencies = [ + "io-lifetimes 2.0.4", + "rustix", + "windows-sys 0.59.0", +] + [[package]] name = "futures" version = "0.3.32" @@ -724,6 +783,34 @@ dependencies = [ "serde_core", ] +[[package]] +name = "io-extras" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20fd6de4ccfcc187e38bc21cfa543cb5a302cb86a8b114eb7f0bf0dc9f8ac00f" +dependencies = [ + "io-lifetimes 3.0.1", + "windows-sys 0.60.2", +] + +[[package]] +name = "io-lifetimes" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06432fb54d3be7964ecd3649233cddf80db2832f47fec34c01f65b3d9d774983" + +[[package]] +name = "io-lifetimes" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f0fb0570afe1fed943c5c3d4102d5358592d8625fda6a0007fdbe65a92fba96" + +[[package]] +name = "ipnet" +version = "2.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" + [[package]] name = "itertools" version = "0.14.0" @@ -775,6 +862,12 @@ version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dae608c151f68243f2b000364e1f7b186d9c29845f7d2d85bd31b9ad77ad552b" +[[package]] +name = "maybe-owned" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4facc753ae494aeb6e3c22f839b158aebd4f9270f55cd3c79906c45476c47ab4" + [[package]] name = "memchr" version = "2.8.3" @@ -967,7 +1060,17 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustix-linux-procfs" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fc84bf7e9aa16c4f2c758f27412dc9841341e16aa682d9c7ac308fe3ee12056" +dependencies = [ + "once_cell", + "rustix", ] [[package]] @@ -1210,6 +1313,8 @@ dependencies = [ "blake3", "bytemuck", "bytes", + "cap-fs-ext", + "cap-std", "echo-edict-canonical", "echo-registry-api", "echo-runtime-schema", @@ -1301,7 +1406,7 @@ dependencies = [ "wasmtime-internal-jit-icache-coherence", "wasmtime-internal-unwinder", "wasmtime-internal-versioned-export-macros", - "windows-sys", + "windows-sys 0.61.2", ] [[package]] @@ -1406,7 +1511,7 @@ dependencies = [ "rustix", "wasmtime-environ", "wasmtime-internal-versioned-export-macros", - "windows-sys", + "windows-sys 0.61.2", ] [[package]] @@ -1428,7 +1533,7 @@ dependencies = [ "cfg-if", "libc", "wasmtime-internal-core", - "windows-sys", + "windows-sys 0.61.2", ] [[package]] @@ -1496,7 +1601,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys", + "windows-sys 0.61.2", ] [[package]] @@ -1505,6 +1610,24 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.5", +] + [[package]] name = "windows-sys" version = "0.61.2" @@ -1514,6 +1637,145 @@ dependencies = [ "windows-link", ] +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + +[[package]] +name = "winx" +version = "0.36.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f3fd376f71958b862e7afb20cfe5a22830e1963462f3a17f49d82a6c1d1f42d" +dependencies = [ + "bitflags", + "windows-sys 0.59.0", +] + [[package]] name = "wit-parser" version = "0.251.0"