Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,13 @@

### Added

- Bounded workspace claims can now settle as `OutcomeUnknown` after directory
authority disappears. A rootless reconciliation handle retains only the
exact runtime-owned profile, revalidates the durable grant's exact claim
commit and compiler-admitted request, and constructs the schema-bound
settlement inside Echo. It cannot observe files or construct success, while
zero evidence and substituted profiles, grants, or requests fail before
another WAL commit.
- Settled external-action candidates can now be reconciled idempotently after
acknowledgement loss without a WAL store, transition context, or claim
grant. An exact retained candidate returns the original admitted settlement
Expand Down
5 changes: 4 additions & 1 deletion crates/warp-core/src/external_action.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1318,7 +1318,10 @@ pub fn admit_external_action_settlement(
let recovered_claim = recovered
.claim
.ok_or(ExternalActionProtocolErrorV1::MissingClaim)?;
if recovered.request != claim_grant.request || recovered_claim != claim_grant.claim {
if recovered.request != claim_grant.request
|| recovered_claim != claim_grant.claim
|| recovered.claim_commit_digest != Some(claim_grant.claim_commit_digest)
{
return Err(ExternalActionProtocolErrorV1::SettlementClaimMismatch);
}
if recovered.settlement.is_some() {
Expand Down
315 changes: 211 additions & 104 deletions crates/warp-core/src/external_action_adapter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -328,22 +328,24 @@ pub struct BoundedWorkspaceObservationAdapterV1 {
profile: BoundedWorkspaceObservationProfileV1,
}

/// Rootless settlement authority retained for post-claim reconciliation.
///
/// This handle carries no directory capability and cannot perform an
/// observation. It can only admit the bounded profile's schema-valid
/// `OutcomeUnknown` settlement for an exact durable claim.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct BoundedWorkspaceObservationReconcilerV1 {
profile: BoundedWorkspaceObservationProfileV1,
}

impl BoundedWorkspaceObservationAdapterV1 {
/// Opens the configured root once and retains only directory-relative authority.
pub fn open(
root: &Path,
permitted_paths: impl IntoIterator<Item = String>,
profile: BoundedWorkspaceObservationProfileV1,
) -> Result<Self, BoundedWorkspaceObservationErrorV1> {
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);
}
validate_observation_profile(profile)?;
let permitted_paths = permitted_paths.into_iter().collect::<BTreeSet<_>>();
for path in &permitted_paths {
validate_relative_path(path)?;
Expand All @@ -360,11 +362,7 @@ impl BoundedWorkspaceObservationAdapterV1 {
/// 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,
}
adapter_binding_for(self.profile)
}

/// Performs one bounded observation after request and claim durability.
Expand Down Expand Up @@ -463,22 +461,7 @@ impl BoundedWorkspaceObservationAdapterV1 {
external_evidence_digest: Hash,
) -> Result<ExternalActionSettlementCandidateV1, BoundedWorkspaceObservationErrorV1> {
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,
)
build_outcome_unknown_candidate(self.profile, grant, external_evidence_digest)
}

/// Validates the operation-specific schema and durably admits the settlement.
Expand Down Expand Up @@ -507,22 +490,7 @@ impl BoundedWorkspaceObservationAdapterV1 {
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(())
validate_observation_grant(self.profile, grant, admitted)
}

fn read_regular_file(
Expand Down Expand Up @@ -647,24 +615,7 @@ impl BoundedWorkspaceObservationAdapterV1 {
result: Vec<u8>,
external_evidence_digest: Hash,
) -> Result<ExternalActionSettlementCandidateV1, BoundedWorkspaceObservationErrorV1> {
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,
))
build_observation_candidate(self.profile, grant, kind, result, external_evidence_digest)
}

fn validate_candidate(
Expand All @@ -673,50 +624,206 @@ impl BoundedWorkspaceObservationAdapterV1 {
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)
validate_observation_candidate(
self.profile,
Some(&self.permitted_paths),
grant,
admitted,
candidate,
)
}
}

impl BoundedWorkspaceObservationReconcilerV1 {
/// Constructs a reconciliation handle without acquiring filesystem authority.
pub fn new(
profile: BoundedWorkspaceObservationProfileV1,
) -> Result<Self, BoundedWorkspaceObservationErrorV1> {
validate_observation_profile(profile)?;
Ok(Self { profile })
}

/// Returns the registry binding for the exact retained adapter identity.
#[must_use]
pub const fn adapter_binding(&self) -> ExternalActionAdapterBindingV1 {
adapter_binding_for(self.profile)
}

/// Durably admits explicit uncertainty without reopening the external world.
#[allow(clippy::too_many_arguments)]
pub fn admit_outcome_unknown(
&self,
store: &mut impl WalStorePort,
coordinator: &mut ExternalActionCoordinatorV1,
context: ExternalActionTransactionContextV1,
admitted: &AdmittedEdictExternalActionRequestV1,
grant: ExternalActionClaimGrantV1,
external_evidence_digest: Hash,
) -> Result<AdmittedExternalActionSettlementV1, BoundedWorkspaceObservationErrorV1> {
validate_observation_grant(self.profile, &grant, admitted)?;
Comment thread
flyingrobots marked this conversation as resolved.
let candidate =
build_outcome_unknown_candidate(self.profile, &grant, external_evidence_digest)?;
validate_observation_candidate(self.profile, None, &grant, admitted, &candidate)?;
Ok(admit_external_action_settlement(
store,
coordinator,
context,
grant,
candidate,
)?)
}
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

const fn adapter_binding_for(
profile: BoundedWorkspaceObservationProfileV1,
) -> ExternalActionAdapterBindingV1 {
ExternalActionAdapterBindingV1 {
adapter_id: profile.adapter_id,
operation_id: profile.operation_id,
authority_scope_digest: profile.authority_scope_digest,
}
}

fn build_outcome_unknown_candidate(
profile: BoundedWorkspaceObservationProfileV1,
grant: &ExternalActionClaimGrantV1,
external_evidence_digest: Hash,
) -> Result<ExternalActionSettlementCandidateV1, BoundedWorkspaceObservationErrorV1> {
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"),
)?;
build_observation_candidate(
profile,
grant,
ExternalActionSettlementKindV1::OutcomeUnknown,
result,
external_evidence_digest,
)
}

fn build_observation_candidate(
profile: BoundedWorkspaceObservationProfileV1,
grant: &ExternalActionClaimGrantV1,
kind: ExternalActionSettlementKindV1,
result: Vec<u8>,
external_evidence_digest: Hash,
) -> Result<ExternalActionSettlementCandidateV1, BoundedWorkspaceObservationErrorV1> {
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(profile.settlement_schema_digest, &result);
Ok(ExternalActionSettlementCandidateV1::new(
grant.request().request_id(),
grant.claim().attempt_id,
profile.adapter_id,
kind,
profile.settlement_schema_digest,
grant.request().basis_digest,
result,
schema_admission_evidence_digest,
external_evidence_digest,
))
}

fn validate_observation_profile(
profile: BoundedWorkspaceObservationProfileV1,
) -> Result<(), BoundedWorkspaceObservationErrorV1> {
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);
}
Ok(())
}

fn validate_observation_grant(
profile: BoundedWorkspaceObservationProfileV1,
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 != profile.adapter_id
{
return Err(BoundedWorkspaceObservationErrorV1::GrantMismatch);
}
if request.operation_id != profile.operation_id
|| request.input_schema_digest != profile.input_schema_digest
|| request.settlement_schema_digest != profile.settlement_schema_digest
|| request.reconciliation_law_digest != profile.reconciliation_law_digest
|| request.authority_scope_digest != profile.authority_scope_digest
{
return Err(BoundedWorkspaceObservationErrorV1::ProfileMismatch);
}
Ok(())
}

fn validate_observation_candidate(
profile: BoundedWorkspaceObservationProfileV1,
permitted_paths: Option<&BTreeSet<String>>,
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 != profile.adapter_id
|| candidate.settlement_schema_digest != 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 permitted_paths =
permitted_paths.ok_or(BoundedWorkspaceObservationErrorV1::SchemaAdmissionFailed)?;
let paths = decode_observation_input(admitted.canonical_operation_input())
.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::<BTreeSet<_>>();
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 {
let expected = paths.iter().cloned().collect::<BTreeSet<_>>();
if expected.is_empty()
|| expected.len() != paths.len()
|| expected.iter().any(|path| {
validate_relative_path(path).is_err() || !permitted_paths.contains(path)
})
{
return Err(BoundedWorkspaceObservationErrorV1::SchemaAdmissionFailed);
}
Ok(())
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.
Expand Down
Loading
Loading