Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
13 changes: 13 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/).
dangling link rather than a relative path, and the observation witness failed
before it began. It worked only because every caller had passed absolute
paths until CI existed.
- Settlement attempt, basis, external-evidence, and schema-admission identities
in the observation report, which `patch-host` already projected and
`effect-host` did not. Without them a basis retained over the wrong bytes was
unobservable to the witness.
- An observed-basis binding case for the observation witness. A succeeded
observation cannot establish it: the adapter refuses unless the basis it
derives over what it read equals the requested basis, so the two agree there
by construction. The stale-basis refusal is the one settlement family where
the retained evidence is independently informative, and a separate successful
observation of those exact bytes derives the same basis by a second route.
The case also pins that the refusal does not echo the requested basis, and
that the basis varies with the observed bytes on a fixed path, so neither
comparison can be satisfied by a constant.

### Removed

Expand Down
6 changes: 5 additions & 1 deletion docs/roadmap.md
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,11 @@ root. Post-claim aperture substitution cannot recover the durable claim, while
path, symlink, basis, terminal-size, compiler-artifact, and runtime-request
violations all fail closed at their owning boundary. Compiler artifacts are
re-admitted at every phase, so a post-request substitution returns a typed
obstruction without appending to the existing WAL.
obstruction without appending to the existing WAL. The settlement's retained
attempt, basis, external-evidence, and schema-admission identities are
projected and bound to the bytes that were actually read: a stale-basis refusal
retains a basis over what was found, and a separate successful observation of
those same bytes derives the same value by an independent route.

### Basis-bound write

Expand Down
11 changes: 11 additions & 0 deletions effect-host/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -516,6 +516,17 @@ fn report(
.ok_or_else(|| "settlement commit digest absent".to_owned())?;
Ok::<_, String>(json!({
"kind": settlement_kind(settlement.kind),
"attemptId": hex(&settlement.attempt_id.as_hash()),
// The basis the request declared, and the evidence the adapter
// derived from what it actually read. A succeeded observation
// is refused unless these agree, so they are equal there by
// construction; a stale-basis refusal is where the evidence
// independently describes the observed bytes.
"basisDigest": hex(&settlement.basis_digest),
"externalEvidenceDigest": hex(&settlement.external_evidence_digest),
"schemaAdmissionEvidenceDigest": hex(
&settlement.schema_admission_evidence_digest,
),
"commitDigest": hex(&commit_digest),
"resultDigest": hex(&settlement.result_digest),
"canonicalResultByteCount": settlement.canonical_result_bytes.len(),
Expand Down
62 changes: 61 additions & 1 deletion tests/effect-runtime.sh
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,19 @@ assert_posture() {
assert_identity "$report_file"
}

# A settled observation must retain the identities that bind it to what was
# actually read. patch-host projects these; without them a basis retained over
# the wrong bytes is invisible to this witness.
assert_settlement_evidence() {
report_file=$1
jq -e '
(.settlement.attemptId | test("^[0-9a-f]{64}$"))
and (.settlement.basisDigest | test("^[0-9a-f]{64}$"))
and (.settlement.externalEvidenceDigest | test("^[0-9a-f]{64}$"))
and (.settlement.schemaAdmissionEvidenceDigest | test("^[0-9a-f]{64}$"))
' "$report_file" >/dev/null
}

complete_success_case() {
case_name=$1
worldline_byte=$2
Expand Down Expand Up @@ -168,6 +181,7 @@ complete_success_case() {
and (.settlement.commitDigest | test("^[0-9a-f]{64}$"))
and (.settlement.resultDigest | test("^[0-9a-f]{64}$"))' \
"$case_root/settlement-report.json" >/dev/null
assert_settlement_evidence "$case_root/settlement-report.json"

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n -C 6 \
  'uncert|outcomeUnknown|assert_settlement_evidence|externalEvidenceDigest|basisDigest' \
  tests/effect-runtime.sh

Repository: flyingrobots/hello-echo

Length of output: 5105


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== files =="
git ls-files | sed -n '1,200p'

echo "== effect-runtime outline =="
wc -l tests/effect-runtime.sh
sed -n '1,80p;180,470p;500,575p' tests/effect-runtime.sh

echo "== repository references to uncertainty/assert settlement evidence/evidence ids =="
rg -n --hidden --iglob '!*.lock' --iglob '!node_modules/**' \
  'uncertainty|outcomeUnknown|assert_settlement_evidence|externalEvidenceDigest|basisDigest|schemaAdmissionEvidenceDigest|retained.observed|settlement-report.json|unknown-report.json' .

Repository: flyingrobots/hello-echo

Length of output: 29247


Bind uncertainty assertions to retained observed evidence.

unknown-report.json only checks settlement/posture/commit-kind fields and empty files. Apply the same evidence contract there: check the four settlement digest fields and link externalEvidenceDigest to an independently derived observed value so a stale/ambiguous actual observation does not satisfy the witness by replaying claimed bytes.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/effect-runtime.sh` at line 184, Extend the unknown-report assertions
alongside assert_settlement_evidence to validate all four settlement digest
fields, not only settlement/posture/commit-kind and empty-file conditions.
Derive the observed external evidence independently and require
externalEvidenceDigest to match that value, preventing claimed or stale bytes
from satisfying the witness.

}

# Golden path. The target file is absent while request and claim are durably
Expand Down Expand Up @@ -496,6 +510,7 @@ assert_rejected_case() {
and .settlement.observation.refusal == $refusal
and .settlement.observation.files == []' \
"$case_root/settlement-report.json" >/dev/null
assert_settlement_evidence "$case_root/settlement-report.json"
}

# Known failures: unauthorized, parent-escaped, and symlink paths obstruct
Expand All @@ -505,6 +520,51 @@ assert_rejected_case parent-escape 45 ../secret.txt allowed.txt invalid-path abs
assert_rejected_case symlink 46 link.txt link.txt symlink-refused symlink
assert_rejected_case stale-basis 70 stale.txt stale.txt stale-basis stale

# The retained observation evidence must describe the bytes that were actually
# read, and not merely agree with itself.
#
# A succeeded settlement cannot demonstrate that. The adapter derives a basis
# over what it read and refuses unless that equals the requested basis, so on
# the success path the retained basis and evidence are equal by construction
# and comparing them proves nothing. The stale-basis refusal is the one
# settlement family where the retained evidence is independently informative:
# it carries a basis over what was actually found, which by construction is not
# what the request declared.
#
# Echo derives that basis by domain-separated hashing over path and bytes.
# Recomputing it here would rebuild producer logic in the consumer, which is
# the boundary this witness exists to hold. Instead the host is asked for the
# same value by a second route: a successful observation that declares those
# exact bytes as its own expectation derives a basis over the same
# (path, bytes) pair, and that must equal the evidence the refusal retained.
#
# `assert_rejected_case stale-basis` writes these bytes to `stale.txt`.
stale_actual_value='changed after request'
complete_success_case stale-basis-actual 81 "$stale_actual_value" stale.txt 65536

stale_settlement="$effect_root/stale-basis/settlement-report.json"
stale_evidence=$(jq -r '.settlement.externalEvidenceDigest' "$stale_settlement")
stale_request_basis=$(jq -r '.settlement.basisDigest' "$stale_settlement")
stale_actual_basis=$(
jq -r '.settlement.basisDigest' \
"$effect_root/stale-basis-actual/settlement-report.json"
)

# One basis over (stale.txt, "changed after request"), reached two ways.
test "$stale_evidence" = "$stale_actual_basis"

# The refusal must not satisfy that by echoing the basis the request declared.
test "$stale_evidence" != "$stale_request_basis"

# And the basis must vary with the observed bytes, so the equality above cannot
# be satisfied by a constant. Same path, different content, different basis.
complete_success_case stale-basis-other 82 'a wholly different content' stale.txt 65536
stale_other_basis=$(
jq -r '.settlement.basisDigest' \
"$effect_root/stale-basis-other/settlement-report.json"
)
test "$stale_other_basis" != "$stale_actual_basis"

# Boundary: first observe the exact encoding size for this path and value, then
# prove that exact bound passes while one byte less produces a typed rejection.
complete_success_case boundary-probe 47 boundary exact.txt 65536
Expand Down Expand Up @@ -667,4 +727,4 @@ then
fi

printf '%s\n' \
"Hello Effect suite passed: 1 ordered golden, 1 relative artifact path, 1 retry, 1 conflict, 1 aperture substitution, 1 replay, 1 fresh world, 1 rootless unknown, 4 refusals, 1 boundary probe, 2 boundaries, 2 artifact refusals, 1 request refusal, 3 fixed-seed property, 8 stress"
"Hello Effect suite passed: 1 ordered golden, 1 relative artifact path, 1 retry, 1 conflict, 1 aperture substitution, 1 replay, 1 fresh world, 1 rootless unknown, 4 refusals, 1 observed-basis binding reached by 2 routes, 1 boundary probe, 2 boundaries, 2 artifact refusals, 1 request refusal, 3 fixed-seed property, 8 stress"
Loading