Align Vergabe publication with existing Nextcloud DAV routes
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 2s

Assistant: codex
Assistant-Model: gpt-6.1-sol
Assistant-Session: 01a1abf1-7e95-7141-87c9-ee78332bf790
This commit is contained in:
tegwick 2026-11-06 06:08:46 +01:00
parent 473187c114
commit 84907a39f5
6 changed files with 158 additions and 30 deletions

View file

@ -0,0 +1,47 @@
{
"schema": "railiance.vergabe-pilot-provider-recovery.v1",
"run_id": "20261105T233543Z-89bfbe04dece",
"status": "failed",
"provider_publication_verified": false,
"decryption_verified": false,
"restore_verified": false,
"application_changed": false,
"stage": "owner_custody",
"owner_handoff_checks": {
"routing_marker_present": true,
"operator_tunnel_matches": true,
"no_ambient_token_variables": true,
"isolated_home_shape": true,
"isolated_home_private": true
},
"failure": "upload_lane_binding_changed",
"elapsed_seconds": 0.6,
"warden_exit_code": 11,
"receipt_file": "/home/worsch/.local/state/vergabe-pilot-recovery/20261105T233543Z-89bfbe04dece/provider-receipt-603935659e98.json",
"session_cleanup_confirmed": null,
"session_revocation_confirmed": null,
"session_storage_cleanup_confirmed": true,
"evidence_kind": "user_reported_terminal_output",
"reported_on": "2026-11-06",
"workplan": "RAPPS-WP-0014",
"task": "RAPPS-WP-0014-T03",
"source_revision": "473187c1141a87c8e05ca01a4cf3daae22a81b10",
"review": {
"login_and_handoff_passed": true,
"session_revoked_user_reported": true,
"owner_identity_and_two_custody_reads_succeeded_per_source_order": true,
"provider_calls_reached": false,
"obsolete_validator_accepted_only": "filesdrop",
"existing_owner_contract_routes": [
"files",
"filesdrop"
],
"actual_credential_bearing_url_not_requested_or_logged": true,
"actual_route_not_reported_in_original_failure": true,
"owner_sources": [
"railiance-platform/scripts/root_custody_finish.py:roundtrip",
"railiance-platform/scripts/openbao-reboot-snapshot.py"
],
"fix": "Accept only the two existing exact provider/share-token-bound route forms; retain provider, object, credential and no-overwrite scope."
}
}

View file

@ -14,6 +14,16 @@ offline test through the installed Warden envelope reproduces the inherited
alias refusal and passes with the corrected preparation. Retry the same command
below; the previous session was revoked. Provider scope and ciphertext are unchanged.
Second native attempt: [the founder return](../../evidence/2026-11-06-vergabe-upload-binding-failure-return.json)
passes all five handoff checks and the custody reads, then stops at
`upload_lane_binding_changed` before any provider call. The publisher had copied
the older migration helper's filesdrop-only assumption. Existing platform owner
recovery tools accept both `/public.php/dav/files/{share_token}` and the legacy
`/public.php/dav/filesdrop/{share_token}`. Revision 3 accepts exactly those two
forms on the approved provider with the same share token. Foreign origins,
different tokens, extra path segments, queries and fragments refuse. No raw
credential-bearing address was requested or logged. Retry the same command.
## Founder workstation command
```bash
@ -121,7 +131,7 @@ No second application pause or recapture is needed for this point.
## Validation
Fifteen publisher checks cover real age encrypt/decrypt and complete packet/media/
Seventeen publisher checks cover real age encrypt/decrypt and complete packet/media/
SQLite verification with mocked custody/provider; separate upload/readback
credentials; identical-object retry; different-object preservation; wrong escrow,
quota and tampered-download refusal; credential-safe silent failure; ambient
@ -129,5 +139,6 @@ token refusal; pinned owner helpers; foreign-origin denial and bounded readback.
The additional cases test all token aliases, API/effective-policy handling,
separate session outcomes and the installed Warden envelope using an offline
fake Bao client. They issue no native session and inspect no real helper or
credential. All 45 pilot helper checks pass. Native provider publication and
credential. Both existing URL forms pass the synthetic publication/readback/
decryption flow. All 47 pilot helper checks pass. Native provider publication and
decryption remain unverified until a successful founder receipt returns.

View file

@ -6,7 +6,7 @@
"run_id": "20261105T233543Z-89bfbe04dece",
"ciphertext_sha256": "4915288384e3ba8a87f1427b8e10106ee7241c04184adb97f439e57846bef76f",
"ciphertext_bytes": 182521,
"publisher_source_sha256": "8815272ed09ffce91e2a1551c646285d8aea844f318fc188c986d15f4093f807",
"publisher_source_sha256": "4340f386e448984b2b34d13d59f318ca4dda35b218a2b28cd034dcd00881244e",
"platform_helper_pins": {
"openbao-attended-exec.py": "53fb5fb38ad17c4b06d7969a4d8968d976c8ebc8f1abfeb9a191aa43dc54a5c3",
"migrate_nextcloud_backup_account.py": "d3285f403f3c947f75c3288d989152e59ab9a8f5542bcf901bd0a06b0f3e050c",
@ -53,18 +53,19 @@
"recovered_data": "Private provider-recovery-* directory inside the existing workstation kit, outside Git, retained for the isolated restore rehearsal",
"failure_handling": "Safe receipt with phase flags; different object bytes refuse without overwrite. Retry reads the deterministic object name. Warden exit and cleanup confirmation are recorded separately from data verification.",
"validation": {
"publisher_cases": 15,
"publisher_cases": 17,
"synthetic_only": true,
"actual_age_round_trip": true,
"provider_and_custody_mocked": true,
"native_provider_publication_executed": false,
"total_pilot_cases": 45,
"total_pilot_cases": 47,
"real_installed_warden_offline_fixture": true,
"synthetic_bao_only": true,
"native_login_used_by_tests": false
"native_login_used_by_tests": false,
"both_existing_routes_end_to_end_synthetic": true
},
"previous_publisher_source_sha256": "845294c574692333b35e627f5d3194e6334136f14f4f699e27a680f9cd4604db",
"revision": "r2-owner-handoff",
"previous_publisher_source_sha256": "8815272ed09ffce91e2a1551c646285d8aea844f318fc188c986d15f4093f807",
"revision": "r3-upload-route-contract",
"handoff_fix": {
"clear_all_token_aliases_before_login": [
"BAO_TOKEN",
@ -77,5 +78,19 @@
"effective_policies": "Union token and identity policies; root/agent boundary still refuse",
"nonzero_child_exit_does_not_imply_revocation_failure": true,
"same_provider_scope": true
},
"upload_url_contract": {
"origin": "https://nx4069.your-storageshare.de",
"accepted_paths": [
"/public.php/dav/files/{share_token}",
"/public.php/dav/filesdrop/{share_token}"
],
"same_share_token_required": true,
"extra_paths_query_fragments_foreign_origins_refused": true,
"owner_sources": [
"railiance-platform/scripts/root_custody_finish.py:roundtrip",
"railiance-platform/scripts/openbao-reboot-snapshot.py"
],
"new_provider_scope": false
}
}

View file

@ -97,21 +97,26 @@ class PublisherTests(unittest.TestCase):
return Fake()
def test_real_crypto_roundtrip_provider_readback_and_complete_packet_check(self):
provider = self.provider()
receipt = {"restore_verified": False}
publish.publish(self.directory, self.credentials, provider, receipt)
self.assertEqual(receipt["status"], "published_read_back_and_decrypted")
self.assertTrue(receipt["independent_readback_verified"])
self.assertTrue(receipt["sqlite_link_verified"])
self.assertTrue(receipt["packet_file_hashes_verified"])
self.assertFalse(receipt["database_restored"])
self.assertFalse(receipt["restore_verified"])
self.assertEqual(len(provider.put_calls), 1)
self.assertEqual(len(provider.get_calls), 2)
self.assertNotIn("synthetic private row", json.dumps(receipt))
recovered = Path(receipt["recovery_directory"])
self.assertEqual(recovered.stat().st_mode & 0o777, 0o700)
self.assertEqual((recovered / "point/database.dump").stat().st_mode & 0o777, 0o600)
for kind in ("files", "filesdrop"):
with self.subTest(kind=kind):
provider = self.provider()
receipt = {"restore_verified": False}
credentials = {**self.credentials, "upload_root": publish.HOST + "/public.php/dav/" + kind + "/synthetic",
"upload_route_kind": kind}
publish.publish(self.directory, credentials, provider, receipt)
self.assertEqual(receipt["status"], "published_read_back_and_decrypted")
self.assertEqual(receipt["upload_route_kind"], kind)
self.assertTrue(receipt["independent_readback_verified"])
self.assertTrue(receipt["sqlite_link_verified"])
self.assertTrue(receipt["packet_file_hashes_verified"])
self.assertFalse(receipt["database_restored"])
self.assertFalse(receipt["restore_verified"])
self.assertEqual(provider.put_calls, [credentials["upload_root"] + "/" + publish.PREFIX + "/" + publish.OBJECT])
self.assertEqual(len(provider.get_calls), 2)
self.assertNotIn("synthetic private row", json.dumps(receipt))
recovered = Path(receipt["recovery_directory"])
self.assertEqual(recovered.stat().st_mode & 0o777, 0o700)
self.assertEqual((recovered / "point/database.dump").stat().st_mode & 0o777, 0o600)
def test_identical_existing_object_is_recovered_without_upload(self):
provider = self.provider(existing=self.content, available=0)
@ -160,6 +165,26 @@ class PublisherTests(unittest.TestCase):
class BoundaryTests(unittest.TestCase):
def test_upload_binding_accepts_both_existing_owner_contract_routes(self):
token = "synthetic-share"
for kind in ("files", "filesdrop"):
url = publish.HOST + "/public.php/dav/" + kind + "/" + token
with self.subTest(kind=kind):
self.assertEqual(publish.upload_binding(token, url), (url, kind))
self.assertEqual(publish.upload_binding(token, url + "/"), (url, kind))
def test_upload_binding_refuses_foreign_hosts_tokens_and_extra_paths(self):
token = "synthetic-share"
for url in ["https://foreign.invalid/public.php/dav/files/" + token,
publish.HOST + "/public.php/dav/files/other-share",
publish.HOST + "/public.php/dav/files/" + token + "/unexpected",
publish.HOST + "/public.php/dav/files/" + token + "?query=1",
publish.HOST + "/public.php/dav/files/" + token + "#fragment"]:
with self.subTest(url=url), self.assertRaisesRegex(stores.Refused, "upload_lane_binding_changed"):
publish.upload_binding(token, url)
with self.assertRaisesRegex(stores.Refused, "upload_token_shape_invalid"):
publish.upload_binding("unsafe?query", publish.HOST + "/public.php/dav/files/unsafe?query")
def test_owner_environment_discards_all_token_aliases_before_fresh_login(self):
environment = {name: "synthetic" for name in publish.TOKEN_ENV}
environment.update(HOME="/synthetic", PATH="/usr/bin", BAO_ADDR="wrong", VAULT_ADDR="wrong")
@ -204,7 +229,7 @@ class BoundaryTests(unittest.TestCase):
if args[2] == publish.LANE:
return {"data": {"metadata": {"version": 1}, "data": {
"NC_WEBDAV_TOKEN": "synthetic-upload",
"NC_WEBDAV_URL": publish.HOST + "/public.php/dav/filesdrop/synthetic-upload",
"NC_WEBDAV_URL": publish.HOST + "/public.php/dav/files/synthetic-upload",
"AGE_PRIVATE_KEY": "synthetic-not-a-key"}}}
if args[2] == publish.OPERATOR:
return {"data": {"data": {"BACKUP_USERNAME": "Backup", "BACKUP_PASSWORD": "synthetic"}}}
@ -218,6 +243,7 @@ class BoundaryTests(unittest.TestCase):
"BAO_ADDR": publish.TUNNEL, "VAULT_ADDR": publish.TUNNEL}, clear=True):
result = publish.custody(SimpleNamespace(bao=read, data=lambda value: value))
self.assertEqual(result["kv_version"], 1)
self.assertEqual(result["upload_route_kind"], "files")
self.assertEqual([c[2] for c in calls], ["auth/token/lookup-self", publish.LANE, publish.OPERATOR])
def provider(self):

View file

@ -108,6 +108,17 @@ def require_owner_handoff():
require(checks[key], reason)
def upload_binding(token, url):
require(isinstance(token, str) and 0 < len(token) < 1024
and not any(c in token for c in "\r\n\t/ \\?#"), "upload_token_shape_invalid")
require(isinstance(url, str), "upload_lane_binding_changed")
root = url.rstrip("/")
for kind in ("filesdrop", "files"):
if root == HOST + "/public.php/dav/" + kind + "/" + token:
return root, kind
raise Refused("upload_lane_binding_changed")
def custody(helper):
# Reuse the owner's contained OIDC session, never a token environment/file
# handoff. Warden revokes and removes its private helper after the child.
@ -125,13 +136,10 @@ def custody(helper):
owner = read(["read", "-format=json", OPERATOR], "backup_account_custody_read_failed")["data"]["data"]
values = lane["data"]
token, url = values["NC_WEBDAV_TOKEN"], values["NC_WEBDAV_URL"]
require(isinstance(token, str) and token and len(token) < 1024
and not any(c in token for c in "\r\n/ ")
and url.rstrip("/") == HOST + "/public.php/dav/filesdrop/" + token,
"upload_lane_binding_changed")
upload_root, route_kind = upload_binding(token, url)
require(owner["BACKUP_USERNAME"] == "Backup" and bool(owner["BACKUP_PASSWORD"]),
"backup_account_required")
return {"upload_root": url.rstrip("/"), "upload_auth": (token, ""),
return {"upload_root": upload_root, "upload_route_kind": route_kind, "upload_auth": (token, ""),
"owner_auth": ("Backup", owner["BACKUP_PASSWORD"]),
"identity": values["AGE_PRIVATE_KEY"], "kv_version": lane["metadata"]["version"]}
@ -249,10 +257,13 @@ def packet_checks(plain, directory, expected_packet):
def publish(directory, credentials, provider, receipt):
receipt["stage"] = "local_ciphertext_verification"
encrypted = source(directory)
content = encrypted.read_bytes()
require(len(content) == CIPHER_BYTES and hashlib.sha256(content).hexdigest() == CIPHER_SHA256,
"source_ciphertext_changed")
receipt.update(stage="escrow_recipient_verification", lane_kv_version=credentials["kv_version"],
upload_route_kind=credentials.get("upload_route_kind", "unspecified"))
crypto_identity_matches(credentials["identity"])
receipt["escrow_recipient_verified"] = True
auth = credentials["owner_auth"]

View file

@ -196,7 +196,7 @@ id: RAPPS-WP-0014-T03
status: wait
needs_human: true
decision_id: d06307e5-2737-461a-a2f4-a12c3b492a9a
intervention_note: "Capture/restart and encrypted workstation copy pass. First provider attempt logged in but the publisher handoff guard refused before credential/provider access; Warden reported revocation. Environment preparation and lifecycle reporting fixed and tested through the installed Warden envelope offline. Retry the same publisher --execute command with fresh founder login. Provider evidence, isolated restore, access/rollback and operating ownership remain open."
intervention_note: "Capture/restart and encrypted workstation copy pass. Second provider attempt passes handoff and custody but stops before provider calls on a stale filesdrop-only URL check. Aligned with both existing platform recovery routes (files and filesdrop) with exact provider/share-token binding; 47 checks pass. Retry same publisher --execute command. Provider evidence, isolated restore, access/rollback and operating ownership remain open."
priority: high
assignee: the-custodian
blocking_reason: "Matched populated capture, restart and encrypted workstation copy pass with overview UI follow-up. Await provider publication/independent readback, escrow decryption, isolated restored workflow, access checks, compatible rollback and operating ownership."
@ -472,6 +472,24 @@ occurs in those tests. Retry the same reviewed provider command; the earlier
go persists, while fresh login is needed because that session was revoked. No
provider scope, role, policy, credential grant or live app change is introduced.
2026-11-06 second provider attempt: all handoff checks are true; owner identity
and both custody reads complete before `upload_lane_binding_changed` (0.6
seconds, Warden exit 11). No provider call or app change occurs. Warden again
reports session revocation. Receipt:
`docs/evidence/2026-11-06-vergabe-upload-binding-failure-return.json`.
The publisher's URL validator accepted only filesdrop, copied from the older
migration helper; `root_custody_finish.py:roundtrip` and the platform reboot
snapshot helper already accept the repaired files route as well. The actual
credential-bearing URL is not exposed by this receipt or requested from the
founder. Revision 3 matches those two exact owner-contract forms with the same
provider/share token, retaining the object, credentials, create-only and
independent readback scope. Receipt metadata records only a route-kind label
and KV version. Seventeen publisher / 47 total pilot checks pass, including
both forms through synthetic publication/readback/decryption and rejection of
foreign origin, mismatched token or extra paths/query/fragment. Retry the same
reviewed command; the earlier go remains valid. Native provider and restore
acceptance remain open.
## Validate synthetic sizing against the exact release and shared contention