From 9a194da7c6129c6791d9d5f4a059a187554cf6fd Mon Sep 17 00:00:00 2001 From: RobLe3 Date: Fri, 28 Aug 2026 17:52:50 +0200 Subject: [PATCH] Define public route readiness before eligibility --- CHANGELOG.md | 5 ++++ .../directory-state-semantics-v1.json | 14 ++++++++++- .../architecture/directory-state-semantics.md | 7 +++++- pre1/feature-baseline-v1.json | 25 +++++++++++++++++-- spec/v1.9/conformance-test-suite.md | 6 +++-- spec/v1.9/iicp-dir.md | 5 ++++ spec/v1.9/release-integrity-manifest.json | 12 ++++----- tools/test_directory_state_semantics.py | 9 +++++++ 8 files changed, 71 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 403312b..d9322eb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,11 @@ ## Unreleased +- Clarifies the existing external-tunnel admission invariant and registers + `DIR-REG-10`: allocation of a public tunnel URL is not route-readiness + evidence, so registration and discovery eligibility wait for the provider + listener and public `/iicp/health` probe. + ## v1.10.17 — 2026-08-25 Pre-normative preservation and traceability candidate. It does not ratify a diff --git a/docs/architecture/directory-state-semantics-v1.json b/docs/architecture/directory-state-semantics-v1.json index 090d3ad..dce3221 100644 --- a/docs/architecture/directory-state-semantics-v1.json +++ b/docs/architecture/directory-state-semantics-v1.json @@ -1,5 +1,5 @@ { - "decision_version": "1.0.0", + "decision_version": "1.1.0", "status": "accepted-semantic-boundary", "wire_change": false, "default_discovery_change": false, @@ -50,6 +50,18 @@ "dispatch": "eligible_if_all_other_gates_pass" } }, + { + "name": "external_tunnel_created_not_serving", + "expected": { + "identity": "valid_or_pending", + "advertisement": "not_current", + "reachability": "unverified", + "availability": "unavailable", + "dispatch": "ineligible", + "registration_token_issued": false, + "default_discovery_contains": false + } + }, { "name": "heartbeat_available_false", "expected": { diff --git a/docs/architecture/directory-state-semantics.md b/docs/architecture/directory-state-semantics.md index c16fda6..125889b 100644 --- a/docs/architecture/directory-state-semantics.md +++ b/docs/architecture/directory-state-semantics.md @@ -1,7 +1,7 @@ # Architecture decision: directory state and dispatch eligibility **Status:** Accepted semantic boundary; current connected-directory behavior retained -**Recorded:** 2026-08-14 +**Recorded:** 2026-08-14; clarified 2026-08-28 **Machine-readable contract:** [`directory-state-semantics-v1.json`](directory-state-semantics-v1.json) **Related work:** IICP #39, #63, #160 and #163 @@ -33,6 +33,9 @@ discovery contract does not change: - Registration establishes a current advertisement only after identity, authorization, endpoint-safety and liveness checks pass. +- Allocation or observation of an `external_tunnel` URL establishes only a + candidate locator. The provider listener and the public `/iicp/health` route + must be serving before registration can make that locator eligible. - An authenticated heartbeat refreshes self-reported reachability and runtime state. `available: false` keeps the node unavailable and ineligible. - More than 90 seconds without a valid heartbeat makes reachability stale, @@ -86,6 +89,8 @@ expired, revoked, superseded or policy-ineligible evidence. but default discovery omits it. - **Superseded:** an accepted newer advertisement replaces the old one. The old locator cannot be selected even if it still responds. +- **Tunnel starting:** identity may be valid and a public URL may exist, but a + failed admission probe leaves the route unregistered and dispatch-ineligible. - **Revoked:** identity or advertisement revocation makes dispatch ineligible regardless of reachability. - **Policy-ineligible:** route and service may be healthy, but an unmet policy, diff --git a/pre1/feature-baseline-v1.json b/pre1/feature-baseline-v1.json index 947900e..4dbbcd7 100644 --- a/pre1/feature-baseline-v1.json +++ b/pre1/feature-baseline-v1.json @@ -1,6 +1,6 @@ { "schema": "iicp.pre1-feature-baseline.v1", - "updated_at": "2026-08-26", + "updated_at": "2026-08-28", "status": "SPECIFICATION_RECONCILIATION", "non_authorizing": true, "stable_designation_authorized": false, @@ -103,6 +103,27 @@ "boundary": "PHP and Rust are tested in isolated authority lanes.", "contradiction_status": "CLEAR" }, + { + "id": "provider-route-readiness", + "title": "Verified provider route readiness before dispatch eligibility", + "classification": "REQUIRED_STABLE", + "authority_refs": [ + "spec/v1.9/iicp-dir.md", + "docs/architecture/directory-state-semantics.md" + ], + "fixture_refs": [ + "docs/architecture/directory-state-semantics-v1.json" + ], + "implementations": [ + "RobLe3/iicp-directory-php", + "RobLe3/iicp-directory-rust", + "RobLe3/iicp-client-python", + "RobLe3/iicp-client-typescript", + "RobLe3/iicp-client-rust" + ], + "boundary": "Allocated endpoints, running processes and usable routes are distinct. Dynamic public routes become eligible only after the provider listener and public health path are verified.", + "contradiction_status": "CLEAR" + }, { "id": "public-route-free-discovery", "title": "Route-free public directory projection", @@ -562,5 +583,5 @@ "contradiction_status": "CLEAR" } ], - "capability_ids_sha256": "sha256:51cb23ce24f7fefd8c7ff1831d835be7d8ce180d69f750efd71051aae9cdb333" + "capability_ids_sha256": "sha256:7a198e074b7d43db022b57768e06ee428b566ef065f14df84f704235850c5071" } diff --git a/spec/v1.9/conformance-test-suite.md b/spec/v1.9/conformance-test-suite.md index f9d3647..0616ef0 100644 --- a/spec/v1.9/conformance-test-suite.md +++ b/spec/v1.9/conformance-test-suite.md @@ -1,7 +1,7 @@ # IICP Conformance Test Suite -**Version**: 4.51.0 -**Date**: 2026-08-20 +**Version**: 4.52.0 +**Date**: 2026-08-28 **Status**: draft **Issue**: #22 **Authority**: Protocol Steward + Integration Validator @@ -61,6 +61,7 @@ docker compose up -d # brings up directory + adapter + database | `DIR-REG-07` | `node_token` stored as bcrypt hash, never plaintext | Code review: `NodeRegistry` uses `Hash::make()` before insert | code review only | | `DIR-REG-08` | `POST /v1/register` with unrecognised `capabilities[].quantization` value (e.g. `"fp64"`) → 201 | Directory MUST NOT reject unrecognised advisory field values; per iicp-core.md §2.1 v1.2.4 | — (directory unit test: `RegisterTest::test_accepts_unrecognised_quantization_value`) | | `DIR-REG-09` | `POST /v1/register` with unrecognised `capabilities[].inference_engine` value (e.g. `"tensorrt"`) → 201 | Directory MUST NOT reject unrecognised advisory field values; per iicp-core.md §2.1 v1.2.4 | — (directory unit test: `RegisterTest::test_accepts_unrecognised_inference_engine_value`) | +| `DIR-REG-10` | REGISTER declares `transport_method=external_tunnel` but public `/iicp/health` is not serving | Reject without issuing credentials or publishing the route; accept only after the same route passes the admission probe | PHP: `RegisterTest::test_external_tunnel_is_not_eligible_before_public_health_is_serving` + `test_external_tunnel_registers_after_public_health_is_serving`; Rust: `validate::tests::rt04_unknown_nat_does_not_bypass_probe` external-tunnel assertion | ### 3.2 Heartbeat (MUST) @@ -999,6 +1000,7 @@ canonical fixture and schema are | Version | Date | Change | |---------|------|--------| +| 4.52.0 | 2026-08-28 | DIR-REG-10 makes the existing external-tunnel liveness rule directly testable: allocating a public URL does not make a route eligible before the provider listener and public health path are serving. | | 4.51.0 | 2026-08-20 | REP-01 now covers slow successful execution; REP-08 separates audit integrity evidence from outcome reputation; REP-09 requires idempotent metrics-batch retries. | | 4.50.0 | 2026-08-02 | §3.3l registers DIR-LIFECYCLE-01..06 for registration, authenticated heartbeat, token-authenticated refresh, stale-token rejection and deregistration. Runner state is ephemeral and result bundles remain content-free. Existing public and dispatch fixtures remain immutable. | | 4.49.0 | 2026-08-02 | §3.3k registers the loopback-only DIR-DISPATCH-01..05 ticket-safety profile and reconciles the suite header and single changelog authority. Existing 4.45 result manifests remain immutable and verifiable. | diff --git a/spec/v1.9/iicp-dir.md b/spec/v1.9/iicp-dir.md index efc2778..87bc160 100644 --- a/spec/v1.9/iicp-dir.md +++ b/spec/v1.9/iicp-dir.md @@ -254,6 +254,11 @@ endpoint like any other public endpoint: it MUST still run the normal liveness and routability checks, and it MUST NOT depend on vendor-specific tunnel APIs or vendor state. +Allocation or log observation of an external-tunnel URL is not liveness +evidence. A provider MUST start its local listener before the Directory's +admission probe can succeed, and a Directory MUST NOT issue credentials or make +the route eligible while the public `/iicp/health` check is unsuccessful. + Accountless or ephemeral external tunnels are an onboarding fallback, not a durable production reachability guarantee. Provider SDKs SHOULD apply local creation back-pressure before creating such tunnels so multiple node services on diff --git a/spec/v1.9/release-integrity-manifest.json b/spec/v1.9/release-integrity-manifest.json index b4e0f99..b094bd9 100644 --- a/spec/v1.9/release-integrity-manifest.json +++ b/spec/v1.9/release-integrity-manifest.json @@ -1,7 +1,7 @@ { "files": { ".github/workflows/profile-fixtures.yml": "3c12b12ef4f2042daf6e04ba9aadd51ec74e97ca22e663d01fcf3ac4b152678b", - "CHANGELOG.md": "dab4628993d5aa586802a72ae961d900ee937ea26825290f2bc501341c5bf72a", + "CHANGELOG.md": "f085aa5ad4b3bade6b92ee967b7c1fee29a00d0d7ac5ff2a4b41da7be53ddb7c", "CONTINUATION.md": "2d019aba2d7d084987506c356be52efc7e15d2b5afc14b5747bd4f89d591209f", "CONTRIBUTING.md": "4b4f82a12e8422e1105591781dc69e222734315fcb6ff78b074a20c2e52df7cb", "GOVERNANCE.md": "aaf2eaf1f31347ffdd8e9b9ec060618b3cbcac6f35fc773352cf0a3fd7475dcd", @@ -31,8 +31,8 @@ "conformance-runner/tests/test_runner.py": "f42a0758494607a60594fd63bd0dbf648bc9df6c07319f923d2b6f22ed25caf2", "docs/ECOSYSTEM_VERSION_TRUTH.md": "8db6c18d0aad1d7edfcb6c8ac547180dad41de19a1fd24ed103dca423b085688", "docs/agent-bootstrap.md": "84e234870fd8ae1e70bf413fbe150b612985e0b4fbdfd862924b297d6ac9529a", - "docs/architecture/directory-state-semantics-v1.json": "586c242f2ddee13b81def73743f1dd47658359e8d35c09d99327e8f7dabd38e5", - "docs/architecture/directory-state-semantics.md": "bfc9dc41a00d7200de50236fe66a66380d470c26622015459d6fb37015afcde8", + "docs/architecture/directory-state-semantics-v1.json": "45c5328611249b5346924e8603803b08db26db0af1fafba15d0a1774454db030", + "docs/architecture/directory-state-semantics.md": "cd36bbd4cf0119d558247eaf3685c5971722506d37bdf820f972b94eef5f73dc", "docs/architecture/effective-service-capability-semantics.md": "8afeb2a854d0e6b58a2a8f52945b0155a2175a83c8fa7742cb2e330d7f2e161d", "docs/architecture/effective-service-capability-v1.json": "d193413abf445fd9ff82998c545f52ac905eadafb809dd32cf8fc0680a64fa13", "docs/architecture/environmental-independence-and-extension-architecture.md": "69d1029f42b6dc00e35f388b32cd233eba4925b14a794c7daeb14321822a7e27", @@ -153,14 +153,14 @@ "spec/v1.9/README.md": "cc772c76899fac209798fe78404ff1c05222a0a68d0f2d2cecd11c566951d217", "spec/v1.9/VERSION": "d2d98e986183e9995b2f59dcb713b69d886f478477ed8ec6fad703adbd7f3e26", "spec/v1.9/conformance-badges.md": "1837103fa0f19f825413869a1d2415b3122afca09b0b58511ddef5dc5b1df05d", - "spec/v1.9/conformance-test-suite.md": "f8ca723b0b0b43c77cd513e139a87ffb052632f9d9de464297df30d461f826fb", + "spec/v1.9/conformance-test-suite.md": "5defa3ba4114d4c8173db3afe6e7645c77b52a1dd1cd8ba4b03952ab927f0b9a", "spec/v1.9/iicp-billing-extension.md": "e4a29fc1af8762988b340c060e0b327df1172a6af3ae7524c81c171e4dd30018", "spec/v1.9/iicp-cbor-wire.md": "2b21cfbf9de195529c6113b9717db243f1aef71a24f5daeab1d896c8c644b3b8", "spec/v1.9/iicp-confidentiality.md": "af4df2a42a37b840ec907ad6de3f00bf02fb3bc6530080c0f43e90abab769229", "spec/v1.9/iicp-cooperative-inference.md": "4f35a608f3c2a7e60398dc36f365abdccbb5da89f521ea6aeb73ba6886c85a6c", "spec/v1.9/iicp-core.md": "9731eb9556af07d8c775dd14bba0cc24aa705934801570fdbeaa5d426648bc97", "spec/v1.9/iicp-deployment-provenance.md": "ee7f67221bbfceaf2a096037b488836699adbfb5ab9a18f17d10734dc4b5f03f", - "spec/v1.9/iicp-dir.md": "7be7931378b36d388e3f8b1d429e1136f8ee9b86c99538cd5a9763606dd7123d", + "spec/v1.9/iicp-dir.md": "3b626dea03ed7d4906cca12e766e9077a464eae319f461b0439deff41f8799ef", "spec/v1.9/iicp-extensions.md": "c1e0443c2e365abe54c7503dcdeb59a1efdc25d48faafee4429e4aed13f87ce9", "spec/v1.9/iicp-federated-directory.md": "ebb7c237111da0c3e5baa376426ea1b3720b33fe76bc894fda3fcbba5355f3f6", "spec/v1.9/iicp-framing.md": "803a0a20620df53c8bbcc7534d05e8ddd391d0d3d55752541bc1478764e914cf", @@ -226,7 +226,7 @@ "tools/test_clean_room_interoperability_record.py": "2f2cc274dfb5e9503b0d24d91bc5ee2e1878301c464ad3ff65d9afaa267a847b", "tools/test_compatibility_environment.py": "c5cfb6c4bda7002b500f5b727c4ebd6bcf96052aef5ae8fefd902daf0ed324c8", "tools/test_conformance_version_truth.py": "5fd46440bd25ec6a38033aac4948265fab3d2edebb18e2d9f25af248a4df82f3", - "tools/test_directory_state_semantics.py": "2580f57d9c76a26c88b3fa4887d61e9635afa5231fd9a2a1995d50f1aaaa4d07", + "tools/test_directory_state_semantics.py": "c7d726572fa2242f3d97307bd9987dea293343bb356c0ed575362eb22c12ddf8", "tools/test_e050_client_credential_lifecycle.py": "b3a5dd3259f40e9c362a5494d58e5be431a633d403a53f4a408aa5bddd58f115", "tools/test_ecosystem_version_truth.py": "2dc94abf0d7b2cd45e13cee2fbaf58c8157e08820db883e1b00f7354475260c0", "tools/test_effective_capability_wire_contract.py": "123a7f2de8b72eb39988a79a878ec21941c20be8fe95bb39abb7a8262fe9693d", diff --git a/tools/test_directory_state_semantics.py b/tools/test_directory_state_semantics.py index 5436c92..5a40dd4 100644 --- a/tools/test_directory_state_semantics.py +++ b/tools/test_directory_state_semantics.py @@ -55,6 +55,15 @@ def test_endpoint_rotation_is_fail_closed(self) -> None: self.assertFalse(rotation["unverified_new_route_published"]) self.assertIn("after_new_route_validation", rotation["old_advertisement"]) + def test_external_tunnel_url_is_not_route_readiness_evidence(self) -> None: + starting = self.scenarios["external_tunnel_created_not_serving"] + self.assertEqual("unverified", starting["reachability"]) + self.assertEqual("ineligible", starting["dispatch"]) + self.assertFalse(starting["registration_token_issued"]) + self.assertFalse(starting["default_discovery_contains"]) + self.assertIn("external_tunnel", self.directory_spec) + self.assertIn("normal liveness", self.directory_spec) + def test_signed_state_does_not_imply_current_freshness(self) -> None: delayed = self.scenarios["delayed_federation_sync"] self.assertTrue(delayed["signature_may_remain_valid"])