diff --git a/.agents/skills/publishing-and-deployment/SKILL.md b/.agents/skills/publishing-and-deployment/SKILL.md index 16e970665..a3dc9ba23 100644 --- a/.agents/skills/publishing-and-deployment/SKILL.md +++ b/.agents/skills/publishing-and-deployment/SKILL.md @@ -35,7 +35,7 @@ Registry release inventory: 18 package-bearing contracts across 9 producers; 16 | `audio` | `fs-gg-audio` | `0.6.0` | `0.6.0` | `audio:0.6.0` | FS.GG.Audio.Core/.Host/.Engine/.Elmish/.WebBrowser public .fsi surfaces — AudioEffect vocabulary + IAudioBackend/IMixingBackend seam + mixing Engine + Audio.Cmd Elmish bridge + browser Web Audio host | | `net` | `fs-gg-net` | `0.6.0` | `0.6.0` | `net:0.6.0` | FS.GG.Net.Core/.WebSocket/.WebSocket.Server/.Protobuf/.Grpc/.Elmish public .fsi surfaces — ITransport/IMessageChannel seam + Sequential/Multiplexed correlation + serve/ServerEcho + WebSocket client/server transport + Google.Protobuf/protobuf-net codecs + gRPC lifecycle bridge + Elmish Cmd/Sub | | `github` | `coord-engine` | `0.113.0` | `0.112.0` | `github:0.112.0` | the `fsgg-coord-engine` CLI verb surface (claim/take/batch/who/widen/set-paths/say/landable/done/release/flush/…) + its exit-code contract, emitted from src/FS.GG.Coord.Core/Protocol.fs; shipped as the FS.GG.Coord.Cli dotnet tool | -| `github` | `telemetry-host` | `0.8.0` | `0.8.0` | `github:0.8.0` | optional Linux x64 FS.GG.Telemetry.Host dotnet tool — authenticated workspace-scoped ingestion, private dashboard, protected native capture and bounded learning export; independently versioned from the github coordination coherent set | +| `github` | `telemetry-host` | `0.9.0` | `0.8.0` | `github:0.8.0` | optional Linux x64 FS.GG.Telemetry.Host dotnet tool — authenticated workspace-scoped ingestion, private dashboard, protected native capture and bounded learning export; independently versioned from the github coordination coherent set | | `github` | `new-sdd-workspace` | `0.22.0` | `0.21.0` | `github:0.21.0` | the `new-sdd-workspace` scaffolder CLI (package FS.GG.NewSddWorkspace) — one-command full-stack SDD workspace creation, wrapping the FS.GG.Templates `rendering` provider (ADR-0016); shipped as a dotnet tool | | `templates` | `fs-gg-workspace-template` | `0.19.3` | `0.19.3` | `templates:0.19.3` | FS.GG.Workspace.Template package, eight template configurations and seven `dotnet new` selectors: workspace-provider identities — `fs-gg-console` (provider `console` — minimal F# executable, no npm lane, ADR-0072 §2); `fs-gg-web` (provider `web` — F# ASP.NET Core + plain TypeScript/Vite website, ADR-0071 §3); `fs-gg-fable-game` (provider `fable-game` — F# ASP.NET Core + Fable/Elmish game client, SignalR for connection-oriented real-time traffic + plain HTTP endpoints with explicit versioned DTOs for typed request/response per ADR-0073, consuming the published `fs-gg-game-core-fable-lockstep-v1` profile per ADR-0069); `fs-gg-fable-bindings` (provider `fable-bindings` — Fable interop library over an exactly pinned npm package/declaration closure, ADR-0072 §3, first proved by the unreleased EHotwagner/babylonjsBindings reference) — plus `fs-gg-governance`, the separately registered pre-existing governance overlay template folded into this package at the same rename; `fs-gg-project-knowledge` and `fs-gg-python` are also already packaged. The current and legacy fable-game configurations share one selector. | | `game` | `game-skills` | `0.9.0` | `0.9.0` | `game:0.9.0` | FS.GG.Game.Skills 0.9.0 package — owner-sourced game, Fable, replay and rules guidance with schema-v2 per-file digests and workspace bundle predicates; embedded by the SDD 1.8 receiver | diff --git a/.claude/skills/publishing-and-deployment/SKILL.md b/.claude/skills/publishing-and-deployment/SKILL.md index 16e970665..a3dc9ba23 100644 --- a/.claude/skills/publishing-and-deployment/SKILL.md +++ b/.claude/skills/publishing-and-deployment/SKILL.md @@ -35,7 +35,7 @@ Registry release inventory: 18 package-bearing contracts across 9 producers; 16 | `audio` | `fs-gg-audio` | `0.6.0` | `0.6.0` | `audio:0.6.0` | FS.GG.Audio.Core/.Host/.Engine/.Elmish/.WebBrowser public .fsi surfaces — AudioEffect vocabulary + IAudioBackend/IMixingBackend seam + mixing Engine + Audio.Cmd Elmish bridge + browser Web Audio host | | `net` | `fs-gg-net` | `0.6.0` | `0.6.0` | `net:0.6.0` | FS.GG.Net.Core/.WebSocket/.WebSocket.Server/.Protobuf/.Grpc/.Elmish public .fsi surfaces — ITransport/IMessageChannel seam + Sequential/Multiplexed correlation + serve/ServerEcho + WebSocket client/server transport + Google.Protobuf/protobuf-net codecs + gRPC lifecycle bridge + Elmish Cmd/Sub | | `github` | `coord-engine` | `0.113.0` | `0.112.0` | `github:0.112.0` | the `fsgg-coord-engine` CLI verb surface (claim/take/batch/who/widen/set-paths/say/landable/done/release/flush/…) + its exit-code contract, emitted from src/FS.GG.Coord.Core/Protocol.fs; shipped as the FS.GG.Coord.Cli dotnet tool | -| `github` | `telemetry-host` | `0.8.0` | `0.8.0` | `github:0.8.0` | optional Linux x64 FS.GG.Telemetry.Host dotnet tool — authenticated workspace-scoped ingestion, private dashboard, protected native capture and bounded learning export; independently versioned from the github coordination coherent set | +| `github` | `telemetry-host` | `0.9.0` | `0.8.0` | `github:0.8.0` | optional Linux x64 FS.GG.Telemetry.Host dotnet tool — authenticated workspace-scoped ingestion, private dashboard, protected native capture and bounded learning export; independently versioned from the github coordination coherent set | | `github` | `new-sdd-workspace` | `0.22.0` | `0.21.0` | `github:0.21.0` | the `new-sdd-workspace` scaffolder CLI (package FS.GG.NewSddWorkspace) — one-command full-stack SDD workspace creation, wrapping the FS.GG.Templates `rendering` provider (ADR-0016); shipped as a dotnet tool | | `templates` | `fs-gg-workspace-template` | `0.19.3` | `0.19.3` | `templates:0.19.3` | FS.GG.Workspace.Template package, eight template configurations and seven `dotnet new` selectors: workspace-provider identities — `fs-gg-console` (provider `console` — minimal F# executable, no npm lane, ADR-0072 §2); `fs-gg-web` (provider `web` — F# ASP.NET Core + plain TypeScript/Vite website, ADR-0071 §3); `fs-gg-fable-game` (provider `fable-game` — F# ASP.NET Core + Fable/Elmish game client, SignalR for connection-oriented real-time traffic + plain HTTP endpoints with explicit versioned DTOs for typed request/response per ADR-0073, consuming the published `fs-gg-game-core-fable-lockstep-v1` profile per ADR-0069); `fs-gg-fable-bindings` (provider `fable-bindings` — Fable interop library over an exactly pinned npm package/declaration closure, ADR-0072 §3, first proved by the unreleased EHotwagner/babylonjsBindings reference) — plus `fs-gg-governance`, the separately registered pre-existing governance overlay template folded into this package at the same rename; `fs-gg-project-knowledge` and `fs-gg-python` are also already packaged. The current and legacy fable-game configurations share one selector. | | `game` | `game-skills` | `0.9.0` | `0.9.0` | `game:0.9.0` | FS.GG.Game.Skills 0.9.0 package — owner-sourced game, Fable, replay and rules guidance with schema-v2 per-file digests and workspace bundle predicates; embedded by the SDD 1.8 receiver | diff --git a/.github/workflows/release-telemetry-host-successor-candidate.yml b/.github/workflows/release-telemetry-host-successor-candidate.yml index 7bfca8acb..97a70208f 100644 --- a/.github/workflows/release-telemetry-host-successor-candidate.yml +++ b/.github/workflows/release-telemetry-host-successor-candidate.yml @@ -1,10 +1,10 @@ name: release-telemetry-host-successor-candidate -# Published predecessor is immutable Host0.7.0 (utel-host-rel-13). +# Published predecessor is immutable Host0.8.0 (utel-host-rel-14). # Prospective source coordinates only. Separately admitted invocation may establish # feed absence in its mandatory first gate; no candidate work precedes that PASS. # Root still supplies fresh tag/release/journal/protection and source admission. -# Independent Host 0.8.0 source and byte qualification only. This workflow has +# Independent Host 0.9.0 source and byte qualification only. This workflow has # no package-feed, tag, release, journal, OIDC or promotion write capability. on: workflow_dispatch: @@ -19,7 +19,7 @@ concurrency: jobs: candidate: - name: telemetry-host-080-candidate + name: telemetry-host-090-candidate if: github.ref == 'refs/heads/main' runs-on: ubuntu-latest timeout-minutes: 45 @@ -54,14 +54,14 @@ jobs: test "$(git rev-parse HEAD)" = "$SOURCE_SHA" test "$(git ls-remote origin refs/heads/main | cut -f1)" = "$SOURCE_SHA" version="$(dotnet msbuild src/FS.GG.Telemetry.Host/FS.GG.Telemetry.Host.fsproj -getProperty:Version | tr -d '[:space:]')" - test "$version" = 0.8.0 + test "$version" = 0.9.0 - name: Test passive Host HTTP authority before browser qualification timeout-minutes: 5 run: | set -euo pipefail dotnet restore tests/FS.GG.Telemetry.Tests/FS.GG.Telemetry.Tests.fsproj --locked-mode dotnet test tests/FS.GG.Telemetry.Tests/FS.GG.Telemetry.Tests.fsproj -c Release --no-restore --filter "FullyQualifiedName~HostPassiveReceiptTests|FullyQualifiedName~HostSchemaProfileTests|FullyQualifiedName~RemoteTelemetryTests.protected native collector resolves durable dispatch and replays retained envelope" --blame --logger "console;verbosity=normal" --logger "trx;LogFileName=host-passive.trx" --results-directory "$RUNNER_TEMP/host-passive" - python3 scripts/read-trx-count.py "$RUNNER_TEMP/host-passive/host-passive.trx" --minimum 4 --label "passive Host HTTP" --summary-file "$GITHUB_STEP_SUMMARY" + python3 scripts/read-trx-count.py "$RUNNER_TEMP/host-passive/host-passive.trx" --minimum 5 --label "passive Host HTTP" --summary-file "$GITHUB_STEP_SUMMARY" - name: Retain passive Host HTTP diagnostics if: ${{ always() && steps.read-first.outcome == 'success' }} uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 @@ -95,13 +95,13 @@ jobs: mkdir -p artifacts/telemetry-host dotnet pack src/FS.GG.Telemetry.Host/FS.GG.Telemetry.Host.fsproj -c Release --no-restore \ -p:RepositoryCommit="$SOURCE_SHA" -o artifacts/telemetry-host - package=artifacts/telemetry-host/FS.GG.Telemetry.Host.0.8.0.nupkg + package=artifacts/telemetry-host/FS.GG.Telemetry.Host.0.9.0.nupkg manifest=artifacts/telemetry-host/manifest.json test -f "$package" python3 scripts/telemetry-host-release.py prepare --package "$package" \ --lock src/FS.GG.Telemetry.Host/packages.lock.json \ --assets src/FS.GG.Telemetry.Dashboard/Assets \ - --source-sha "$SOURCE_SHA" --version 0.8.0 --tag telemetry-host/v0.8.0 \ + --source-sha "$SOURCE_SHA" --version 0.9.0 --tag telemetry-host/v0.9.0 \ --output "$manifest" python3 scripts/telemetry-host-release.py verify --manifest "$manifest" --package "$package" bash tests/standalone-telemetry-host-package/run.sh "$package" artifacts/telemetry-host/package-evidence.json @@ -115,7 +115,7 @@ jobs: with: name: telemetry-host-successor-candidate-${{ github.sha }}-${{ github.run_id }} path: | - artifacts/telemetry-host/FS.GG.Telemetry.Host.0.8.0.nupkg + artifacts/telemetry-host/FS.GG.Telemetry.Host.0.9.0.nupkg artifacts/telemetry-host/manifest.json artifacts/telemetry-host/package-evidence.json if-no-files-found: error diff --git a/.github/workflows/release-telemetry-host-successor-publish.yml b/.github/workflows/release-telemetry-host-successor-publish.yml index eeb7aed09..0276a48a8 100644 --- a/.github/workflows/release-telemetry-host-successor-publish.yml +++ b/.github/workflows/release-telemetry-host-successor-publish.yml @@ -19,7 +19,7 @@ on: required: true type: string continue_failed_original: - description: Retired Host 0.6 continuation flag; current Host 0.8 refuses true + description: Retired Host 0.6 continuation flag; current Host 0.9 refuses true required: true default: false type: boolean @@ -41,7 +41,7 @@ concurrency: jobs: publish: - name: telemetry-host-080-publisher + name: telemetry-host-090-publisher if: github.ref == 'refs/heads/main' && github.actor == 'EHotwagner' runs-on: ubuntu-latest environment: release-successor diff --git a/.github/workflows/telemetry-host-package.yml b/.github/workflows/telemetry-host-package.yml index 426aae7b0..f716575ab 100644 --- a/.github/workflows/telemetry-host-package.yml +++ b/.github/workflows/telemetry-host-package.yml @@ -49,6 +49,8 @@ on: - "tests/standalone-telemetry-host-package/**" - "tests/telemetry-host-release/**" - "scripts/telemetry-host-release.py" + - "scripts/telemetry-host-published-org-read.py" + - "tests/telemetry-host-published-org-read/**" - "scripts/telemetry-host-successor-*.py" - "scripts/telemetry_host_successor_*.py" - "scripts/release_successor_execution.py" @@ -93,6 +95,8 @@ on: - "tests/standalone-telemetry-host-package/**" - "tests/telemetry-host-release/**" - "scripts/telemetry-host-release.py" + - "scripts/telemetry-host-published-org-read.py" + - "tests/telemetry-host-published-org-read/**" - "scripts/telemetry-host-successor-*.py" - "scripts/telemetry_host_successor_*.py" - "scripts/release_successor_execution.py" @@ -167,6 +171,7 @@ jobs: run: | set -euo pipefail python3 tests/telemetry-host-successor/run.py + python3 tests/telemetry-host-published-org-read/run.py bash tests/telemetry-host-release/run.sh - name: Test passive Host HTTP authority before expensive qualification if: ${{ !(inputs.learn_native_fixture || inputs.learn_native_preflight) }} @@ -175,7 +180,7 @@ jobs: set -euo pipefail dotnet restore tests/FS.GG.Telemetry.Tests/FS.GG.Telemetry.Tests.fsproj --locked-mode dotnet test tests/FS.GG.Telemetry.Tests/FS.GG.Telemetry.Tests.fsproj -c Release --no-restore --filter "FullyQualifiedName~HostPassiveReceiptTests|FullyQualifiedName~HostSchemaProfileTests|FullyQualifiedName~RemoteTelemetryTests.protected native collector resolves durable dispatch and replays retained envelope" --blame --logger "console;verbosity=normal" --logger "trx;LogFileName=host-passive.trx" --results-directory "$RUNNER_TEMP/host-passive" - python3 scripts/read-trx-count.py "$RUNNER_TEMP/host-passive/host-passive.trx" --minimum 4 --label "passive Host HTTP" --summary-file "$GITHUB_STEP_SUMMARY" + python3 scripts/read-trx-count.py "$RUNNER_TEMP/host-passive/host-passive.trx" --minimum 5 --label "passive Host HTTP" --summary-file "$GITHUB_STEP_SUMMARY" - name: Retain passive Host HTTP diagnostics if: ${{ always() && !(inputs.learn_native_fixture || inputs.learn_native_preflight) }} uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 diff --git a/docs/architecture.md b/docs/architecture.md index 9f7d70404..9b355ea84 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -654,7 +654,7 @@ product's `FS.GG.UI.*` pin), which is a different axis from the template package | `fs-gg-audio` | FS.GG.Audio | `0.6.0` | `0.6.0` | | `fs-gg-net` | FS.GG.Net | `0.6.0` | `0.6.0` | | `coord-engine` | FS-GG/.github | `0.113.0` | `0.112.0` | -| `telemetry-host` | FS-GG/.github | `0.8.0` | `0.8.0` | +| `telemetry-host` | FS-GG/.github | `0.9.0` | `0.8.0` | | `new-sdd-workspace` | FS-GG/.github | `0.22.0` | `0.21.0` | | `fs-gg-workspace-template` | FS.GG.Templates | `0.19.3` | `0.19.3` | | `game-skills` | FS.GG.Game | `0.9.0` | `0.9.0` | diff --git a/docs/coordination/telemetry-host-release.md b/docs/coordination/telemetry-host-release.md index f1bb0a821..f5024aed8 100644 --- a/docs/coordination/telemetry-host-release.md +++ b/docs/coordination/telemetry-host-release.md @@ -1,5 +1,46 @@ # Telemetry host package and release boundary +## Prospective Host 0.9 source — Outcome4640, 2026-10-23 + +[PR4802](https://github.com/FS-GG/.github/pull/4802) taught the shared store to admit +passive `provider-usage-observation/1` facts with provenance +`claude-local-transcript/1`, each in its own native identity namespace +(`claude-transcript/1`), beside `codex-local-rollout/1`. Every other provenance is still +refused. Published Host0.8 was built from `74cbd6f0` and predates that change, so it +answers a Claude transcript batch with `invalid-request`. The Host takes the parser +through its Store and Coord.Core project references. It therefore needs a new +independent package rather than any Host logic change. The packaged closure also carries +other coherent113 source compiled through those references (the Claude transcript reader +in `SkillTelemetryReaders.fs` and `Coord.Core/Telemetry.fs`); none of it is reachable from +Host entry points. Root re-diffs the Host closure against `74cbd6f0` at the dispatch SHA. +The bump also re-pins `telemetry-host-release.py` in the read-only org observer +(`scripts/telemetry-host-published-org-read.py`), whose test now runs in the package +workflow so a stale pin fails at PR time. + +This source prepares **0.9.0**, `telemetry-host/v0.9.0` and logical journal15 at +`state/releases/utel-host-rel-15`. The predecessor is the accepted original Host0.8 +publication: [publisher39580298871](https://github.com/FS-GG/.github/actions/runs/39580298871) +with independent readonly [39591087147](https://github.com/FS-GG/.github/actions/runs/39591087147), +nonlatest release419394647 at `74cbd6f0b1b598d48556eb854c4ba6a3c3abda54`, release archive +`44273495…5aba89` and common normalized payload `f036808546…1802ee`, with journal14. +The read-first gate now requires the known0.8 predecessor and normalized0.9 absence. +The other gate, artifact, publisher and journal mechanics are unchanged from Host0.8. +These are prospective coordinates, not an availability claim. Root still verifies +tag/release occupancy, Authority journal15 destination/protection and exact accepted +source before any invocation. Publication needs its own fresh effect admission, and +the publisher stays default-false. + +Store14 and the receipt ABI are unchanged. Since `74cbd6f0`, `TelemetryStore.fs` +differs only in the provenance-to-namespace match. `TelemetryStoreApplication` +(`currentSchemaVersion = 14`, DDL and passive query) is byte-identical, and the release +manifest still selects only schema14. Claude observations use the existing +`current_ingest_facts` rows and corrections path, so a Home Store14 workspace can move +from0.8 to0.9 without migration. The focused passive Host HTTP preflight now requires +five tests. The new test proves that the actual Host receipt endpoint applies a Claude +transcript observation and refuses an unknown provenance or a Codex-namespaced identity. +Home cutover, credential custody and installed acceptance remain separate. +Published0.8/journal14, 0.7/journal13 and held0.6/journal12 remain unchanged. + ## Prospective Host 0.8 source — Delivery4707, 2026-10-21 [Delivery4707](https://github.com/FS-GG/.github/issues/4707) owns the first installed diff --git a/docs/registry/compatibility.md b/docs/registry/compatibility.md index 02c4aa880..6b0d2bf40 100644 --- a/docs/registry/compatibility.md +++ b/docs/registry/compatibility.md @@ -180,7 +180,7 @@ authored `Version` cells of the Versioned contracts table (#748).* | `fs-gg-net` | FS.GG.Net | `0.6.0` | `0.6.0` | | `keyboard-input` | FS.GG.Rendering | `0.5.0` | — | | `coord-engine` | FS-GG/.github | `0.113.0` | `0.112.0` | -| `telemetry-host` | FS-GG/.github | `0.8.0` | `0.8.0` | +| `telemetry-host` | FS-GG/.github | `0.9.0` | `0.8.0` | | `new-sdd-workspace` | FS-GG/.github | `0.22.0` | `0.21.0` | | `fs-gg-workspace-template` | FS.GG.Templates | `0.19.3` | `0.19.3` | | `game-skills` | FS.GG.Game | `0.9.0` | `0.9.0` | diff --git a/registry/CHANGELOG.md b/registry/CHANGELOG.md index 6e9c6366c..10ca63ebc 100644 --- a/registry/CHANGELOG.md +++ b/registry/CHANGELOG.md @@ -143,6 +143,8 @@ no gate behaviour changes — this is purely how humans record the log. ## Entries +- **2026-10-23** — HOST090 SOURCE (.github; #4640): advance only independent Telemetry Host source to0.9.0 so a Host built from current main admits passive provider-usage-observation/1 with provenance claude-local-transcript/1 (#4802); published Host0.8.0 at74cbd6f0 refuses it. New tag telemetry-host/v0.9.0 and journal15 (utel-host-rel-15) are prospective coordinates over predecessor published0.8/journal14. Store14 and the receipt ABI are unchanged; published package-version/tag stay0.8.0. No candidate, preflight, publication or Home adoption is asserted. + ### 2026-10-23 — `wasm-shared-runtime`: 0.7 publication, separate last installed 0.6 (game; Game#728, Game#727) Record original publisher [39894673177/attempt1](https://github.com/FS-GG/FS.GG.Game/actions/runs/39894673177) (release-wasm: preflight, admission, begin, org, public, assets and complete succeeded) at producer `15e287d7c8884f088b7db41140f108f3f0136043` (Game#728), release421418319 and lightweight tag `wasm/v0.7.0` → `15e287d7`. Release assets: SDK `090658bd…`, release-manifest `56e585db…`, promotion-binding `4372fe16…` (binding preparation39891458102/a1, artifact12476030732) and SHA256SUMS `dc62714d…`. Both nuget.org archives (Contracts `79060cef…`, Browser `bfc28ac9…`) were independently verified equal to the originals (`6a778633…`, `1d5e9df2…`) in every entry except `.signature.p7s`; the GitHub Packages side rests on the publisher's own org job. Published0.7 API classification against published0.6 producer `8a5e63ba` is compatible for both packages. 0.7.0 starts the policy deadline at Worker readiness with a separately bounded Worker startup (FS-GG/FS.GG.Game#727); the self-contained bundled Worker it retains has shipped since 0.5. Installed0.7 remains unqualified; the last accepted installed0.6 P8a5e/Q9a98 profile and every prior immutable receipt remain separate. Consumers stay empty; Workspace.Template 0.19.3 selecting [0.7.0] (FS.GG.Templates#748) is a source/package selection, not consumer acceptance. diff --git a/registry/dependencies.yml b/registry/dependencies.yml index 7616eaeca..17be544af 100644 --- a/registry/dependencies.yml +++ b/registry/dependencies.yml @@ -1020,7 +1020,7 @@ contracts: surface: "the `fsgg-coord-engine` CLI verb surface (claim/take/batch/who/widen/set-paths/say/landable/done/release/flush/…) + its exit-code contract, emitted from src/FS.GG.Coord.Core/Protocol.fs; shipped as the FS.GG.Coord.Cli dotnet tool" consumers: [sdd, rendering, governance, templates, game, audio, net] # All SEVEN coordination-kit receivers (registry/repos.yml `receives: coordination-kit`). `scripts/fsgg-coord` resolves and execs this package, so every receiver is a real TOOL consumer edge. `.github` itself is not listed because it resolves the engine from source. - id: telemetry-host - version: "0.8.0" # Accepted original Host0.8 publication: publisher39580298871 plus readonly39591087147; both feeds and all eight journal/promote assertions accepted. Full4707/Home adoption remains open. PRIOR source qualification: PROSPECTIVE SOURCE ONLY under4707: accepted unassigned-usage projection/browser source4685, independent new package/tag/journal14 planning. Org-feed occupancy remains Unknown after native REST/v3 HTTP403; fresh authoritative absence is required before candidate/publication. Published0.7/journal13 remains unchanged; retained0.6/journal12 tag intent remains Unknown. Store14/receipt ABI unchanged; source qualification, publication and Home adoption remain separate. + version: "0.9.0" # PROSPECTIVE SOURCE ONLY under4640: admit passive claude-local-transcript/1 usage (#4802) in an independent new package/tag/journal15 (utel-host-rel-15) over predecessor published Host0.8 (74cbd6f0, release419394647, journal14). No Host logic change; Store14/receipt ABI unchanged, so Home Store14 needs no migration. Fresh authoritative absence, candidate qualification, publication and Home adoption remain separate. PRIOR: Accepted original Host0.8 publication: publisher39580298871 plus readonly39591087147; both feeds and all eight journal/promote assertions accepted. Full4707/Home adoption remains open. PRIOR source qualification: PROSPECTIVE SOURCE ONLY under4707: accepted unassigned-usage projection/browser source4685, independent new package/tag/journal14 planning. Org-feed occupancy remains Unknown after native REST/v3 HTTP403; fresh authoritative absence is required before candidate/publication. Published0.7/journal13 remains unchanged; retained0.6/journal12 tag intent remains Unknown. Store14/receipt ABI unchanged; source qualification, publication and Home adoption remain separate. package-version: "0.8.0" # Accepted original https://github.com/FS-GG/.github/actions/runs/39580298871 with independent readonly https://github.com/FS-GG/.github/actions/runs/39591087147; no republish. PRIOR immutable0.7.0: PUBLISHED: original39148437514/a1, journal13 generation17/all8 verified, release416586994 telemetry-host/v0.7.0, exact source2e60210be1e04d52fe0735a56333b3e3aef42284. Candidate/release archive d59ecac7ef4d1cdc75cebdcb08624b73960309821def1cd83d2909dbda5de253; signed NuGet archive c24af6af3e35f67dc0eca1f0f7c82657a8c82adf3c62ea11a986b1deb6bb4460; common normalized producer payload f8ec077387cfbcefbae89f74781ba7bbcfce1ebc2e97f7c7dd43d7bdcddeda1b. Old0.6 journal12/gen2 remains Unknown; no resend. Home installation/enrollment/restart/restore remain unqualified. PRIOR immutable0.5.0: CURRENT: successful publisher 37567512069; both feeds retain normalized producer payload sha256:4e2762e159229b433a3779f1694c2f8786618c4b51d22df3332086ae8071c68d. GitHub archive sha256:bdc8d0e92ff3c3a218cf78c349186049222e7487e364be434709cee6eba2cddc; signed nuget.org archive sha256:03a0bed87cafee1776970d854d30b458c5e87d35424700e62cdcac94d84278d6. PRIOR immutable0.4.0: first-success publisher 37220419362; both feeds serve normalized producer payload sha256:d7e747cebd6f425d775177bc844d0b3d187a911ae4032e269b008c875a872240. GitHub release archive sha256:150f3e697e088a10170db4f2404fd4e73be856ccefc8d776c53076f55adce45a; nuget.org signed archive sha256:293e2ccdbc7a4aed2c56d72cf82a7f806d7671b549439463cd93164dad574f77. PRIOR immutable 0.3.0: producer payload sha256:5572aa61f284abc5a37a12aa88f99b5169c4379be2aa9959e46c9934532f2836; GitHub archive sha256:c7cbaa474fdcd0ba577f8577ec92bb381f032db7842f86ddb1b3fb050d8a97ad; signed nuget.org archive sha256:7ad2c30894cb3eafbf498e5d247034bc3167ee30dd07c8b09c6b4915657c598a. PRIOR immutable 0.2.1: GitHub Packages archive sha256:4847c15ab207a33556462873840ad4109cf1c1e16fd7a15d5fffae5a8162f589 and signed nuget.org archive sha256:681faeca9941aaeaaad29368f1ab4f1f3c4b0c581655b8a2d8054b6448adc6f2; normalized payload sha256:207843031c78c2711e85a2db14b46a83ec0da9fbd3ee0f144a736320f7e8c2c9. package-tag: "telemetry-host/v0.8.0" # Original Host0.8 publication accepted; source/publication/Home acceptance remain distinct. PRIOR immutable telemetry-host/v0.7.0: Accepted original39148437514/a1, exact three-asset nonlatest release416586994. Coherent latest remains0.106; accepted binding4 unchanged. Observed selector106 is not whole runtime/discovery qualification; prior selector and change timing Unknown. PRIOR immutable telemetry-host/v0.5.0: Promoted release 405350640; exact raw manifest sha256:38ef6cd52df01902545bc923662dc7b606b76ba72e066f109248b664cb2ef1c7 and publication-journal sha256:5363fe78311dbde1742088105a7f56dc5f48f51013cba2bf883b3a743b73c9ce. Canonical manifest sha256:94fed9c145cb437f7385ffb6d7cf3c83a57eeb7ffdc06904a9668057d83d6a7d matches protected journal utel-host-rel-11 head 57a3d1eac66d3e246d4e8de03988e0a35b7b3cb3, generation17/eight verified effects. PRIOR immutable0.4.0: tag telemetry-host/v0.4.0; Promoted three-asset release 403127331; exact raw manifest sha256:144e5aa3d382b8bfc6ddcac76ed583c23ebe8fbb6744f844a94fa5347819d3f0 and publication-journal asset sha256:c2409342cd3448325fb1dc3dff53b6c2e3783c7634b75101834049b86aa11731. Protected journal head 203ba60f7df146743d4a7373a52f55fc6c22858e retains 17 generations and eight verified effects. PRIOR immutable 0.3.0: release 401538149, manifest sha256:7d61b4888d08299b5578df2e3e283dee88b5acabafa53490b4ba8d00e6676704, journal sha256:ec63822c627cfad490f9eea731257ac531c9ac3bd681f4790b9489dcfc8470ce. PRIOR immutable 0.2.1: publisher 36739270730, manifest sha256:f16546855d4ce60060f762b6f7bed715cc115f9ef97cd7b55dad5294b2ea0816 and journal sha256:bce45f3f4c0701031863c6b1eb398a99982b152f585467cb2e7b06c00987c58e. owner: github diff --git a/registry/driver-skill-manifest.json b/registry/driver-skill-manifest.json index 7aacbcd65..9d06ccc6a 100644 --- a/registry/driver-skill-manifest.json +++ b/registry/driver-skill-manifest.json @@ -2519,12 +2519,12 @@ { "id": "publishing-and-deployment", "scope": "operator", - "sha256": "43461e46eacefe131d366f470b6d61be7fa4f077d529e10f1030024287a36b0a", - "tree-sha256": "d30dba44220812f2a1f40bd867438d41a0ea12060cb408ba39c31a2e42c7aeb3", + "sha256": "2d42a8e9a4cda02846fd677265b33d73d5e2e971183ab28acef4f898dc75b93f", + "tree-sha256": "2201ac33635938634da52e598f8cc51ed6bb066bc70f373aa7675fd1cdd29dba", "files": [ { "path": "SKILL.md", - "sha256": "43461e46eacefe131d366f470b6d61be7fa4f077d529e10f1030024287a36b0a", + "sha256": "2d42a8e9a4cda02846fd677265b33d73d5e2e971183ab28acef4f898dc75b93f", "executable": false }, { diff --git a/scripts/telemetry-host-candidate-read-gate.py b/scripts/telemetry-host-candidate-read-gate.py index 35a888468..37e5c2e7a 100644 --- a/scripts/telemetry-host-candidate-read-gate.py +++ b/scripts/telemetry-host-candidate-read-gate.py @@ -20,10 +20,10 @@ ROOT = pathlib.Path(__file__).resolve().parents[1] PACKAGE = "FS.GG.Telemetry.Host" -VERSION = "0.8.0" -PREDECESSOR = "0.7.0" -TAG = "telemetry-host/v0.8.0" -JOURNAL = "refs/heads/fsgg/v2/journal/release/utel-host-rel-14" +VERSION = "0.9.0" +PREDECESSOR = "0.8.0" +TAG = "telemetry-host/v0.9.0" +JOURNAL = "refs/heads/fsgg/v2/journal/release/utel-host-rel-15" API = "https://api.github.com/repos/FS-GG/.github" FEEDS = { "github": "https://nuget.pkg.github.com/FS-GG/download/fs.gg.telemetry.host", diff --git a/scripts/telemetry-host-published-org-read.py b/scripts/telemetry-host-published-org-read.py index 843fda99c..3957b2250 100644 --- a/scripts/telemetry-host-published-org-read.py +++ b/scripts/telemetry-host-published-org-read.py @@ -36,7 +36,7 @@ ORG_URL = "https://nuget.pkg.github.com/FS-GG/download/fs.gg.telemetry.host/0.8.0/fs.gg.telemetry.host.0.8.0.nupkg" ASSETS = {"manifest.json": (662076940, 768, MANIFEST_SHA), "publication-journal.json": (662078749, 831, JOURNAL_SHA)} -PINS = {"telemetry-host-release.py": "b44d74c7f4fb1ab2bb6ce1e0ee00745351577879aede7743d24533b62a60a32c", +PINS = {"telemetry-host-release.py": "b87002d3192c2cf70d9974a8d7a6b2c3cdca248378c171f04b81519d14c40f7e", "release-saga.py": "24bdfa2a4c1491ecc1ddf24f797a072372fdce14881d413d9301b739dc77501c", "fsgg_feed.py": "f7650429432417af72168c288cdee347b338328848870cb4f2f18d6b62d0733f"} CHOICES = ("coherent-0112-absence", "host-080-org-artifact") diff --git a/scripts/telemetry-host-release.py b/scripts/telemetry-host-release.py index 356fa9c75..cb9bde252 100755 --- a/scripts/telemetry-host-release.py +++ b/scripts/telemetry-host-release.py @@ -77,7 +77,7 @@ def tree_digest(root: pathlib.Path) -> str: def store_schema_profile(version: str) -> tuple[int, int]: # The current release advertises only its selected operational schema. Legacy # artifact inspection retains its original profile; this grants no migration. - return (14, 14) if version in {"0.5.0", "0.6.0", "0.7.0", "0.8.0"} else (10, 12) + return (14, 14) if version in {"0.5.0", "0.6.0", "0.7.0", "0.8.0", "0.9.0"} else (10, 12) def build_manifest(args: argparse.Namespace) -> dict: diff --git a/scripts/telemetry-host-successor-artifact.py b/scripts/telemetry-host-successor-artifact.py index 226c24bae..a73bf7a39 100644 --- a/scripts/telemetry-host-successor-artifact.py +++ b/scripts/telemetry-host-successor-artifact.py @@ -17,7 +17,7 @@ REPOSITORY_ID = 1269292704 WORKFLOW = ".github/workflows/release-telemetry-host-successor-candidate.yml" -VERSION = "0.8.0" +VERSION = "0.9.0" PACKAGE = f"FS.GG.Telemetry.Host.{VERSION}.nupkg" MEMBERS = {PACKAGE, "manifest.json", "package-evidence.json"} diff --git a/scripts/telemetry-host-successor-publish.py b/scripts/telemetry-host-successor-publish.py index 6e303fdb9..de6da092a 100644 --- a/scripts/telemetry-host-successor-publish.py +++ b/scripts/telemetry-host-successor-publish.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -"""Publish the exact Host 0.8.0 candidate through fresh admitted effects.""" +"""Publish the exact Host 0.9.0 candidate through fresh admitted effects.""" from __future__ import annotations @@ -19,7 +19,7 @@ from telemetry_host_successor_provider import HostProvider REPOSITORY = "FS-GG/.github" -REF = "refs/heads/fsgg/v2/journal/release/utel-host-rel-14" +REF = "refs/heads/fsgg/v2/journal/release/utel-host-rel-15" def require(ok: bool, detail: str) -> None: @@ -47,14 +47,14 @@ def main() -> int: parser.add_argument("--candidate-run-id", required=True, type=int) parser.add_argument("--candidate-artifact-id", required=True, type=int) parser.add_argument("--candidate-archive-sha256", required=True) - parser.add_argument("--continue-failed-original", action="store_true", help="Retired Host 0.6 flag; current Host 0.8 refuses it") + parser.add_argument("--continue-failed-original", action="store_true", help="Retired Host 0.6 flag; current Host 0.9 refuses it") parser.add_argument("--workdir", required=True, type=pathlib.Path) mode = parser.add_mutually_exclusive_group(required=True) mode.add_argument("--preflight-only", action="store_true") mode.add_argument("--publish", action="store_true") args = parser.parse_args() try: - require(not args.continue_failed_original, "Host 0.6 failed-original continuation is retired; current Host 0.8 requires a new exact-source candidate") + require(not args.continue_failed_original, "Host 0.6 failed-original continuation is retired; current Host 0.9 requires a new exact-source candidate") publisher_sha = os.environ["GITHUB_SHA"] operator = os.environ["GITHUB_ACTOR"] run_id = int(os.environ["GITHUB_RUN_ID"]) @@ -77,7 +77,7 @@ def main() -> int: require(artifact.get("id") == args.candidate_artifact_id, "candidate artifact API identity differs") candidate_source = run.get("head_sha") require(isinstance(candidate_source, str) and len(candidate_source) == 40, "candidate source malformed") - require(candidate_source == publisher_sha, "current Host 0.8 publication requires exact current main candidate") + require(candidate_source == publisher_sha, "current Host 0.9 publication requires exact current main candidate") require(artifact.get("digest") == "sha256:" + args.candidate_archive_sha256, "artifact digest differs") artifact_json = args.workdir / "artifact.json" run_json = args.workdir / "run.json" @@ -97,7 +97,7 @@ def main() -> int: content_id, ordered = effects(manifest) admission = HostAdmission(api, manifest, publisher_sha, run_id, operator, "refs/heads/main") provider = HostProvider(api, manifest_path, github_token, nuget_key) - intent = {"contentId": content_id, "sourceSha": candidate_source, "version": "0.8.0", + intent = {"contentId": content_id, "sourceSha": candidate_source, "version": "0.9.0", "candidateArchiveSha256": args.candidate_archive_sha256, "operator": operator} ledger_api = GitHubAPI(ledger_token) route = ordinary_route() diff --git a/scripts/telemetry_host_successor_admission.py b/scripts/telemetry_host_successor_admission.py index f2e796b27..d75cb3646 100644 --- a/scripts/telemetry_host_successor_admission.py +++ b/scripts/telemetry_host_successor_admission.py @@ -1,4 +1,4 @@ -"""Fresh per-effect native Actions and main-ref admission for Host 0.8.0.""" +"""Fresh per-effect native Actions and main-ref admission for Host 0.9.0.""" from __future__ import annotations diff --git a/scripts/telemetry_host_successor_execution.py b/scripts/telemetry_host_successor_execution.py index 92cc6ffe7..f9f04903b 100644 --- a/scripts/telemetry_host_successor_execution.py +++ b/scripts/telemetry_host_successor_execution.py @@ -9,8 +9,8 @@ from release_successor_execution import Effect, Refused PACKAGE = "FS.GG.Telemetry.Host" -VERSION = "0.8.0" -TAG = "telemetry-host/v0.8.0" +VERSION = "0.9.0" +TAG = "telemetry-host/v0.9.0" def effects(manifest: dict) -> tuple[str, tuple[Effect, ...]]: @@ -23,7 +23,7 @@ def effects(manifest: dict) -> tuple[str, tuple[Effect, ...]]: or not re.fullmatch(r"[0-9a-f]{64}", manifest.get("archiveSha256", "")) or not re.fullmatch(r"sha256:[0-9a-f]{64}", manifest.get("producerPayloadSha256", "")) ): - raise Refused("not an exact Host 0.8.0 release manifest") + raise Refused("not an exact Host 0.9.0 release manifest") canonical = json.dumps(manifest, sort_keys=True, separators=(",", ":"), ensure_ascii=False).encode() manifest_digest = hashlib.sha256(canonical).hexdigest() manifest_archive_digest = hashlib.sha256(canonical + b"\n").hexdigest() diff --git a/src/FS.GG.Telemetry.Host/FS.GG.Telemetry.Host.fsproj b/src/FS.GG.Telemetry.Host/FS.GG.Telemetry.Host.fsproj index cb3f535ec..440873782 100644 --- a/src/FS.GG.Telemetry.Host/FS.GG.Telemetry.Host.fsproj +++ b/src/FS.GG.Telemetry.Host/FS.GG.Telemetry.Host.fsproj @@ -12,7 +12,7 @@ - 0.8.0 + 0.9.0 FS.GG.Telemetry.Host FS.GG.Telemetry.Host true @@ -27,7 +27,7 @@ https://github.com/FS-GG/.github git https://github.com/FS-GG/.github/tree/main/src/FS.GG.Telemetry.Host - 0.8.0 — Prospective independent Host release of the accepted bounded unassigned-usage dashboard projection. Package qualification checks corrected private observations in the actual installed browser before and after restore, with partial population and unknown attribution/cost. Store14 and the existing receipt ABI are unchanged. Host0.7 publication/journal13 and Host0.6 journal12/tag intent remain immutable. Source preparation does not qualify coordinate availability, publication or receiver adoption. + 0.9.0 — Prospective independent Host release that admits passive provider-usage-observation/1 facts with provenance claude-local-transcript/1 in their own native identity namespace, alongside codex-local-rollout/1. Other provenances remain refused. Store14 and the existing receipt ABI are unchanged, so an existing Store14 workspace needs no migration. Host0.8 publication/journal14, Host0.7 publication/journal13 and Host0.6 journal12/tag intent remain immutable. Source preparation does not qualify coordinate availability, publication or receiver adoption. diff --git a/src/FS.GG.Telemetry.Host/README.md b/src/FS.GG.Telemetry.Host/README.md index f9dbc5bbd..cba2dd67c 100644 --- a/src/FS.GG.Telemetry.Host/README.md +++ b/src/FS.GG.Telemetry.Host/README.md @@ -19,9 +19,11 @@ Browser access keys and producer credentials are separate principals. Secrets belong in private files referenced by configuration; do not place them in arguments, URLs, logs, package contents, or source control. -Prospective version `0.8.0` bundles Store14 and the bounded private unassigned-usage +Prospective version `0.9.0` bundles Store14 and the bounded private unassigned-usage display. It accepts partial native response observations through the existing -authenticated generic receipt API. Its manifest +authenticated generic receipt API from local Codex rollouts +(`codex-local-rollout/1`) and Claude Code transcripts (`claude-local-transcript/1`), +each in its own native identity namespace; any other provenance is refused. Its manifest selects Store14 only; no schema migration is implied. Item attribution, inventory, terminal coverage and absent counters remain unknown. Passive observations stay outside qualified item totals. Its fresh release requires a new candidate from the exact @@ -29,7 +31,7 @@ current source. The retained `0.6.0` candidate and its uncertain tag intent rema separate; this version does not retry or complete that publication. Host is independently released under `telemetry-host/v`, separate from -the Kit, Drivers and coordination CLI coherent set. Published Host `0.7.0` and older +the Kit, Drivers and coordination CLI coherent set. Published Host `0.8.0` and older artifacts remain unchanged. Source delivery does not establish a new publication, installed service, TLS trust, client enrollment or receiver durability. diff --git a/tests/FS.GG.Telemetry.Tests/HostPassiveReceiptTests.fs b/tests/FS.GG.Telemetry.Tests/HostPassiveReceiptTests.fs index 736b09595..9126814e3 100644 --- a/tests/FS.GG.Telemetry.Tests/HostPassiveReceiptTests.fs +++ b/tests/FS.GG.Telemetry.Tests/HostPassiveReceiptTests.fs @@ -19,6 +19,114 @@ open FS.GG.Telemetry.Host [] module HostPassiveReceiptTests = + [] + let ``passive HTTP receipt admits Claude transcript provenance and refuses unknown provenance`` () = + task { + let root = Path.Combine(Path.GetTempPath(), "fsgg-host-claude-" + Guid.NewGuid().ToString("N")) + Directory.CreateDirectory root |> ignore + let approved = TelemetryStore.ApprovedLocalDurable + let unwrap value = value |> Result.defaultWith (fun errors -> failwithf "%A" errors) + let store = Path.Combine(root, "store") + let owner: TelemetryReceipt.Scope = { Workspace = "host-claude"; Producer = "owner"; Stream = "observations" } + TelemetryStoreApplication.initialize store approved |> unwrap |> ignore + TelemetryStoreApplication.enrollReceiptPrincipal store approved + { Scope = owner; Role = TelemetryReceipt.Generic; GrantId = Some "owner-grant"; GrantGeneration = Some 1L } |> unwrap |> ignore + use listener = new TcpListener(IPAddress.Loopback, 0) + listener.Start() + let port = (listener.LocalEndpoint :?> IPEndPoint).Port + listener.Stop() + use rsa = RSA.Create(2048) + let request = CertificateRequest("CN=localhost", rsa, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1) + let san = SubjectAlternativeNameBuilder() + san.AddIpAddress(IPAddress.Loopback) + request.CertificateExtensions.Add(san.Build()) + use certificate = request.CreateSelfSigned(DateTimeOffset.UtcNow.AddMinutes(-1), DateTimeOffset.UtcNow.AddDays(1)) + let privateFile (name: string) (value: string) = + let path = Path.Combine(root, name) + File.WriteAllText(path, value) + File.SetUnixFileMode(path, enum 0o600) + path + let secret = "owner" + String('s', 48) + let config: HostConfig = + { Schema = "fsgg.telemetry.host-config/2"; ListenUrl = $"https://127.0.0.1:{port}" + CertificatePath = privateFile "unused.pfx" "not loaded by endpoint fixture" + CertificatePasswordFile = privateFile "unused.pass" "not loaded by endpoint fixture" + ServiceLockPath = Path.Combine(root, "host.lock") + Stores = [| { WorkspaceId = owner.Workspace; Root = store } |] + Credentials = + [| { Reference = owner.Producer; SecretFile = privateFile "owner.key" secret + WorkspaceId = owner.Workspace; ProducerId = owner.Producer; StreamId = owner.Stream + Role = "generic"; GrantId = "owner-grant"; GrantGeneration = 1L; Revoked = false } |] + BrowserPrincipals = [||]; BrowserSession = RemoteTelemetryTests.browserSession } + let state = new Runtime.HostState(config, fun _ -> approved) + state.Ready <- true + state.StartDrain() + let app = (Hosting.createBuilder config.ListenUrl certificate).Build() + Endpoints.configure app state (Configuration.credentials config) + try + do! app.StartAsync() + use handler = new HttpClientHandler(AllowAutoRedirect = false) + // Exact self-signed leaf is trusted only inside this loopback fixture. + handler.ServerCertificateCustomValidationCallback <- fun _ cert _ errors -> + errors = Net.Security.SslPolicyErrors.RemoteCertificateChainErrors + && cert.GetCertHashString() = certificate.GetCertHashString() + use client = new HttpClient(handler) + let nativeThread = "44444444-4444-4444-4444-444444444444" + let nativeTurn = "55555555-5555-5555-5555-555555555555" + let identity (ns: string) = + "passive-" + Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(ns + "\u001f" + nativeThread + "\u001f" + nativeTurn + "\u001fclaude-response"))).ToLowerInvariant() + let fact (ns: string) (provenance: string) = + JsonNode.Parse($"""{{"kind":"provider-usage-observation/1","identity":"{identity ns}","itemId":null,"revision":1,"threadId":"{nativeThread}","turnId":"{nativeTurn}","responseId":"claude-response","provider":null,"model":null,"effort":null,"input":40,"cachedInput":30,"cacheWriteInput":5,"output":7,"reasoning":null,"total":47,"sourceInstance":"{String('c',64)}","sourceGeneration":"claude-g1","sourceDigest":"{String('d',64)}","position":0,"capturedAt":"2026-10-22T08:00:00Z","occurredAt":null,"provenance":"{provenance}","populationCoverage":"partial","inventoryCoverage":"unknown","terminalCoverage":"unknown","attributionCoverage":"unknown"}}""") + let send (batch: string) (event: JsonNode) = + task { + let value = JsonNode.Parse($"""{{"schema":"fsgg.telemetry.envelope/1","workspaceId":"{owner.Workspace}","producerId":"{owner.Producer}","streamId":"{owner.Stream}","batchId":"{batch}","payload":{{"schema":"{TelemetryStore.BatchSchema}","ingestId":"{batch}","sourceIdentity":"{owner.Stream}","generation":"host-g1","cursor":"1","eventCount":1,"events":[]}}}}""") + value["payload"].["events"].AsArray().Add event + use message = new HttpRequestMessage(HttpMethod.Post, config.ListenUrl + "/v1/batches") + message.Headers.Authorization <- Headers.AuthenticationHeaderValue("Bearer", secret) + message.Content <- new ByteArrayContent(Encoding.UTF8.GetBytes(value.ToJsonString())) + use! response = client.SendAsync message + let! body = response.Content.ReadAsByteArrayAsync() + return response.StatusCode, body + } + // Unknown provenance and a Claude fact under the Codex namespace stay refused at the Host boundary. + for batch, ns, provenance in [ "unknown-provenance", "claude-transcript/1", "claude-statusline/1" + "foreign-namespace", "codex-rollout/1", "claude-local-transcript/1" ] do + let! status, body = send batch (fact ns provenance) + Assert.Equal(HttpStatusCode.BadRequest, status) + Assert.Equal(Some "invalid-request", RemoteContract.parseError body) + let! accepted, acceptedBody = send "claude-first" (fact "claude-transcript/1" "claude-local-transcript/1") + Assert.True(accepted = HttpStatusCode.Accepted || accepted = HttpStatusCode.OK, Encoding.UTF8.GetString acceptedBody) + let mutable applied = false + for _ in 1..100 do + if not applied then + use message = new HttpRequestMessage(HttpMethod.Get, config.ListenUrl + "/v1/receipts/claude-first") + message.Headers.Authorization <- Headers.AuthenticationHeaderValue("Bearer", secret) + use! response = client.SendAsync message + let! bytes = response.Content.ReadAsByteArrayAsync() + match RemoteContract.parseReceipt bytes with + | Ok value when value.Status = "applied" -> applied <- true + | _ -> do! Task.Delay 25 + Assert.True(applied, "Claude transcript receipt never became applied") + let snapshot = JsonNode.Parse(TelemetryStoreApplication.passiveObservations store approved owner |> unwrap) + let current = Assert.Single(snapshot["observations"].AsArray()) + Assert.Equal(47L, current["total"].GetValue()) + Assert.Null(current["itemId"]) + use connection = new SqliteConnection($"Data Source={Path.Combine(store, TelemetryStoreApplication.databaseFileName)};Mode=ReadOnly;Pooling=False") + connection.Open() + use command = connection.CreateCommand() + command.CommandText <- "SELECT json_extract(canonical,'$.provenance') || ' ' || identity FROM current_ingest_facts WHERE kind='provider-usage-observation/1';" + Assert.Equal("claude-local-transcript/1 " + identity "claude-transcript/1", string (command.ExecuteScalar())) + use items = connection.CreateCommand() + items.CommandText <- "SELECT count(*) FROM items;" + Assert.Equal(0L, Convert.ToInt64(items.ExecuteScalar())) + connection.Close() + finally + app.StopAsync().GetAwaiter().GetResult() + app.DisposeAsync().AsTask().GetAwaiter().GetResult() + (state :> IDisposable).Dispose() + if Directory.Exists root then Directory.Delete(root, true) + } + [] let ``passive HTTP receipt binds generic authority corrections and unknown attribution`` () = task { diff --git a/tests/FS.GG.Telemetry.Tests/HostSchemaProfileTests.fs b/tests/FS.GG.Telemetry.Tests/HostSchemaProfileTests.fs index 7ca5794b0..fe637afff 100644 --- a/tests/FS.GG.Telemetry.Tests/HostSchemaProfileTests.fs +++ b/tests/FS.GG.Telemetry.Tests/HostSchemaProfileTests.fs @@ -74,7 +74,7 @@ type HostSchemaProfileTests() = let manifestPath = Path.Combine(backup, "backup-manifest.json") let manifestBytes = File.ReadAllBytes manifestPath use manifest = JsonDocument.Parse manifestBytes - Assert.Equal("0.8.0", manifest.RootElement.GetProperty("hostVersion").GetString()) + Assert.Equal("0.9.0", manifest.RootElement.GetProperty("hostVersion").GetString()) Assert.Equal(14, manifest.RootElement.GetProperty("supportedStoreSchemaMin").GetInt32()) Assert.Equal(14, manifest.RootElement.GetProperty("supportedStoreSchemaMax").GetInt32()) let innerPath = Path.Combine(backup, workspace, "manifest.json") diff --git a/tests/telemetry-host-successor/run.py b/tests/telemetry-host-successor/run.py index d49b3c7db..bcc8fd210 100644 --- a/tests/telemetry-host-successor/run.py +++ b/tests/telemetry-host-successor/run.py @@ -31,7 +31,7 @@ def manifest(): return { "schema": "fsgg.telemetry.host-release/1", "packageId": "FS.GG.Telemetry.Host", - "version": "0.8.0", "tag": "telemetry-host/v0.8.0", "sourceSha": "a" * 40, + "version": "0.9.0", "tag": "telemetry-host/v0.9.0", "sourceSha": "a" * 40, "archiveSha256": "b" * 64, "producerPayloadSha256": "sha256:" + "c" * 64, } @@ -46,7 +46,7 @@ def test_prepared_current_package_manifest_uses_only_schema14(self): package = root / "fixture.nupkg" with zipfile.ZipFile(package, "w") as archive: archive.writestr("FS.GG.Telemetry.Host.nuspec", - "FS.GG.Telemetry.Host0.8.0") + "FS.GG.Telemetry.Host0.9.0") archive.writestr("tools/net10.0/linux-x64/FS.GG.Telemetry.Host.dll", b"fixture") lock = root / "lock.json" lock.write_text("{}") @@ -54,21 +54,22 @@ def test_prepared_current_package_manifest_uses_only_schema14(self): assets.mkdir() (assets / "app.js").write_text("fixture") prepared = module.build_manifest(SimpleNamespace(package=package, lock=lock, assets=assets, - version="0.8.0", tag="telemetry-host/v0.8.0", source_sha="a" * 40)) + version="0.9.0", tag="telemetry-host/v0.9.0", source_sha="a" * 40)) self.assertEqual((prepared["supportedStoreSchemaMin"], prepared["supportedStoreSchemaMax"]), (14, 14)) - self.assertEqual(prepared["version"], "0.8.0") + self.assertEqual(prepared["version"], "0.9.0") # Renaming a retained older package cannot become a new genuine candidate. with zipfile.ZipFile(root / "renamed-old.nupkg", "w") as archive: archive.writestr("FS.GG.Telemetry.Host.nuspec", "FS.GG.Telemetry.Host0.6.0") archive.writestr("tools/net10.0/linux-x64/FS.GG.Telemetry.Host.dll", b"older fixture") with self.assertRaisesRegex(ValueError, "package identity"): module.build_manifest(SimpleNamespace(package=root / "renamed-old.nupkg", lock=lock, assets=assets, - version="0.8.0", tag="telemetry-host/v0.8.0", source_sha="a" * 40)) + version="0.9.0", tag="telemetry-host/v0.9.0", source_sha="a" * 40)) def test_current_profile_is_schema14_only_and_legacy_artifact_stays_unchanged(self): spec = importlib.util.spec_from_file_location("host_release_profile", ROOT / "scripts/telemetry-host-release.py") module = importlib.util.module_from_spec(spec) spec.loader.exec_module(module) + self.assertEqual(module.store_schema_profile("0.9.0"), (14, 14)) self.assertEqual(module.store_schema_profile("0.8.0"), (14, 14)) self.assertEqual(module.store_schema_profile("0.7.0"), (14, 14)) self.assertEqual(module.store_schema_profile("0.6.0"), (14, 14)) @@ -93,23 +94,28 @@ def test_current_profile_is_schema14_only_and_legacy_artifact_stays_unchanged(se "supportedStoreSchemaMin": 10, "supportedStoreSchemaMax": 12} path.write_text(json.dumps(old)) self.assertEqual(module.load_manifest(path), old) - with self.assertRaisesRegex(Refused, "exact Host 0.8.0"): + with self.assertRaisesRegex(Refused, "exact Host 0.9.0"): effects(old) previous = {**value, "version": "0.5.0", "tag": "telemetry-host/v0.5.0"} path.write_text(json.dumps(previous)) self.assertEqual(module.load_manifest(path), previous) - with self.assertRaisesRegex(Refused, "exact Host 0.8.0"): + with self.assertRaisesRegex(Refused, "exact Host 0.9.0"): effects(previous) held = {**value, "version": "0.6.0", "tag": "telemetry-host/v0.6.0"} path.write_text(json.dumps(held)) self.assertEqual(module.load_manifest(path), held) - with self.assertRaisesRegex(Refused, "exact Host 0.8.0"): + with self.assertRaisesRegex(Refused, "exact Host 0.9.0"): effects(held) published = {**value, "version": "0.7.0", "tag": "telemetry-host/v0.7.0"} path.write_text(json.dumps(published)) self.assertEqual(module.load_manifest(path), published) - with self.assertRaisesRegex(Refused, "exact Host 0.8.0"): + with self.assertRaisesRegex(Refused, "exact Host 0.9.0"): effects(published) + predecessor = {**value, "version": "0.8.0", "tag": "telemetry-host/v0.8.0"} + path.write_text(json.dumps(predecessor)) + self.assertEqual(module.load_manifest(path), predecessor) + with self.assertRaisesRegex(Refused, "exact Host 0.9.0"): + effects(predecessor) class EarlyQualificationTests(unittest.TestCase): @@ -118,7 +124,7 @@ def check(source, expensive, native=False): pure = source.index("python3 tests/telemetry-host-successor/run.py") test = source.index('--filter "FullyQualifiedName~HostPassiveReceiptTests|FullyQualifiedName~HostSchemaProfileTests|FullyQualifiedName~RemoteTelemetryTests.protected native collector resolves durable dispatch and replays retained envelope"') assert pure < test < source.index(expensive) - assert 'host-passive.trx" --minimum 4' in source + assert 'host-passive.trx" --minimum 5' in source assert 'timeout-minutes: 5' in source assert '--blame ' in source and '--blame-hang' not in source and '--blame-crash' not in source assert 'Retain passive Host HTTP diagnostics' in source and 'always()' in source @@ -135,7 +141,7 @@ def check(source, expensive, native=False): check(source, expensive, native) for mutant in [source.replace('FullyQualifiedName~HostPassiveReceiptTests', 'FullyQualifiedName~Unrelated'), source.replace('FullyQualifiedName~RemoteTelemetryTests.protected native collector', 'FullyQualifiedName~UnrelatedNativeCollector'), - source.replace('host-passive.trx" --minimum 4', 'host-passive.trx" --minimum 0'), + source.replace('host-passive.trx" --minimum 5', 'host-passive.trx" --minimum 0'), source.replace('python3 tests/telemetry-host-successor/run.py', 'echo absent'), source.replace('always()', 'success()'), source.replace('npm run test:browser -- --reporter=line', 'echo browser-not-run'), @@ -301,16 +307,16 @@ def advance(self): def test_successor_identity_and_journal_are_coherent(self): self.assertEqual(self.ordered[0].target_digest, "a" * 40) import xml.etree.ElementTree as ET - self.assertEqual(ET.parse(ROOT / "src/FS.GG.Telemetry.Host/FS.GG.Telemetry.Host.fsproj").findtext(".//Version"), "0.8.0") + self.assertEqual(ET.parse(ROOT / "src/FS.GG.Telemetry.Host/FS.GG.Telemetry.Host.fsproj").findtext(".//Version"), "0.9.0") publisher = (ROOT / "scripts" / "telemetry-host-successor-publish.py").read_text() candidate = (ROOT / ".github" / "workflows" / "release-telemetry-host-successor-candidate.yml").read_text() - self.assertIn('REF = "refs/heads/fsgg/v2/journal/release/utel-host-rel-14"', publisher) + self.assertIn('REF = "refs/heads/fsgg/v2/journal/release/utel-host-rel-15"', publisher) self.assertNotIn("utel-host-rel-10", publisher) - self.assertIn('"version": "0.8.0"', publisher) - self.assertIn("--version 0.8.0 --tag telemetry-host/v0.8.0", candidate) - self.assertIn("FS.GG.Telemetry.Host.0.8.0.nupkg", candidate) - self.assertIn("telemetry-host-080-candidate", candidate) - self.assertIn("telemetry-host-080-publisher", (ROOT / ".github/workflows/release-telemetry-host-successor-publish.yml").read_text()) + self.assertIn('"version": "0.9.0"', publisher) + self.assertIn("--version 0.9.0 --tag telemetry-host/v0.9.0", candidate) + self.assertIn("FS.GG.Telemetry.Host.0.9.0.nupkg", candidate) + self.assertIn("telemetry-host-090-candidate", candidate) + self.assertIn("telemetry-host-090-publisher", (ROOT / ".github/workflows/release-telemetry-host-successor-publish.yml").read_text()) def test_every_effect_is_admitted_and_read_back_once(self): self.assertEqual(len(self.ordered), 8) @@ -347,13 +353,13 @@ def test_candidate_archive_identity_and_first_attempt_are_required(self): archive = root / "candidate.zip" package_digest = hashlib.sha256(b"fixture").hexdigest() positive = {"schema": "fsgg.telemetry-host-package-evidence/v1", "packageId": "FS.GG.Telemetry.Host", - "version": "0.8.0", "archiveSha256": package_digest, "passed": 1, "failed": 0, + "version": "0.9.0", "archiveSha256": package_digest, "passed": 1, "failed": 0, "productionStorage": "eligible-linux-x64", "syntheticTls": True, "passiveReceiptQualified": True} def write_archive(evidence): with zipfile.ZipFile(archive, "w") as zipped: zipped.writestr("manifest.json", json.dumps({**manifest(), "archiveSha256": package_digest})) zipped.writestr("package-evidence.json", json.dumps(evidence)) - zipped.writestr("FS.GG.Telemetry.Host.0.8.0.nupkg", "fixture") + zipped.writestr("FS.GG.Telemetry.Host.0.9.0.nupkg", "fixture") write_archive(positive) digest = hashlib.sha256(archive.read_bytes()).hexdigest() run = {"id": 123, "path": verifier.WORKFLOW, "head_sha": source, @@ -365,7 +371,7 @@ def write_archive(evidence): "repository_id": verifier.REPOSITORY_ID, "head_repository_id": verifier.REPOSITORY_ID}} with patch.object(verifier.subprocess, "run"): - self.assertEqual(verifier.verify(artifact, run, archive, root / "good", source)["version"], "0.8.0") + self.assertEqual(verifier.verify(artifact, run, archive, root / "good", source)["version"], "0.9.0") for field, value in [("productionStorage", "not-qualified:overlay"), ("passiveReceiptQualified", False), ("syntheticTls", False), ("failed", 1), ("failed", False), ("version", "0.5.0"), ("archiveSha256", "0" * 64)]: @@ -389,6 +395,8 @@ def test_prior_release_identity_and_wrong_payload_are_refused(self): ("version", "0.2.1"), ("tag", "telemetry-host/v0.2.1"), ("version", "0.1.3"), ("tag", "telemetry-host/v0.1.3"), ("version", "0.6.0"), ("tag", "telemetry-host/v0.6.0"), + ("version", "0.7.0"), ("tag", "telemetry-host/v0.7.0"), + ("version", "0.8.0"), ("tag", "telemetry-host/v0.8.0"), ("producerPayloadSha256", "bad")): row = manifest(); row[key] = value with self.assertRaises(Refused): @@ -403,9 +411,9 @@ def __init__(self): self.assets = {} self.writes = [] def get(self, path): - if path.endswith("/git/ref/tags/telemetry-host/v0.8.0") and self.tag: + if path.endswith("/git/ref/tags/telemetry-host/v0.9.0") and self.tag: return {"object": {"sha": self.tag}} - if path.endswith("/releases/tags/telemetry-host/v0.8.0") and self.release and not self.release["draft"]: + if path.endswith("/releases/tags/telemetry-host/v0.9.0") and self.release and not self.release["draft"]: return self.release if path.endswith("/releases?per_page=100&page=1"): return [self.release] if self.release else [] @@ -429,7 +437,7 @@ def download_asset(self, asset_id): with tempfile.TemporaryDirectory() as temporary: root = pathlib.Path(temporary) row = manifest() - package = root / "FS.GG.Telemetry.Host.0.8.0.nupkg" + package = root / "FS.GG.Telemetry.Host.0.9.0.nupkg" package.write_bytes(b"exact original Host archive") row["archiveSha256"] = hashlib.sha256(package.read_bytes()).hexdigest() manifest_path = root / "manifest.json" @@ -457,10 +465,10 @@ class API: def __init__(self, raw): self.raw = raw def get(self, path): - if path.endswith("/releases/tags/telemetry-host/v0.8.0"): + if path.endswith("/releases/tags/telemetry-host/v0.9.0"): raise NotFound(path) if path.endswith("/releases?per_page=100&page=1"): - return [{"id": 1, "tag_name": "telemetry-host/v0.8.0", "draft": True}] + return [{"id": 1, "tag_name": "telemetry-host/v0.9.0", "draft": True}] if path.endswith("/releases/1/assets?per_page=100"): return [{"id": 1, "name": "publication-journal.json"}] raise AssertionError(path) @@ -643,7 +651,7 @@ def authority_fixture(self): import ast route = ordinary_route() namespace = dict(copy=copy, NotFound=NotFound, AUTHORITY="FS-GG/FS.GG.Coordination.Authority", - REF="refs/heads/fsgg/v2/journal/release/utel-host-rel-14", + REF="refs/heads/fsgg/v2/journal/release/utel-host-rel-15", reviewed_binding=route.authority_protection()[1]) source = ast.parse((ROOT / "tests/release-successor-live/run.py").read_text()) cls = next(node for node in source.body if isinstance(node, ast.ClassDef) and node.name == "AuthorityAPI") @@ -693,7 +701,7 @@ def observe(self, effect): return Observation("absent") continuation=case == "old-flag", old=case == "old-candidate", journal=Journal) self.assertEqual(result, 0 if case == "current" else 1) self.assertFalse(any(row[0] == "initialize" for row in writes)) - self.assertTrue(all(row[1:] == ("refs/heads/fsgg/v2/journal/release/utel-host-rel-14", True) for row in writes)) + self.assertTrue(all(row[1:] == ("refs/heads/fsgg/v2/journal/release/utel-host-rel-15", True) for row in writes)) if case == "old-flag": self.assertEqual(api.reads, []) self.assertFalse((root / "candidate").exists()) @@ -710,7 +718,7 @@ def get(self, path): path = pathlib.Path(temporary) / "manifest.json"; path.write_text(json.dumps(CURRENT_MANIFEST)) api = API(); provider = HostProvider(api, path, "fixture", "fixture") self.assertEqual(provider.observe(effects(CURRENT_MANIFEST)[1][0]).state, "absent") - self.assertEqual(api.paths, ["repos/FS-GG/.github/git/ref/tags/telemetry-host/v0.8.0"]) + self.assertEqual(api.paths, ["repos/FS-GG/.github/git/ref/tags/telemetry-host/v0.9.0"]) def test_actual_new_journal_preserves_old12_and_unknown_never_resends(self): import ast @@ -787,7 +795,7 @@ def run(command, **kwargs): self.assertEqual(state.effects, {"tag": "intent"}) self.assertEqual(output.getvalue().count("Host successor effect state: waiting"), 2) self.assertNotIn("SECRET", output.getvalue()) - journal.validate_intent({"contentId": effects(CURRENT_MANIFEST)[0], "sourceSha": CURRENT_SOURCE, "version": "0.8.0", + journal.validate_intent({"contentId": effects(CURRENT_MANIFEST)[0], "sourceSha": CURRENT_SOURCE, "version": "0.9.0", "candidateArchiveSha256": CURRENT_ARCHIVE, "operator": "EHotwagner"}) def test_actual_workflow_default_false_and_explicit_flag_shell(self): @@ -845,7 +853,7 @@ def routes(self): for base in gate.FEEDS.values(): index = base + "/index.json" package = base + f"/{gate.VERSION}/fs.gg.telemetry.host.{gate.VERSION}.nupkg" - routes[index] = [self.response(index, {"versions": ["0.6.0", "0.7.0"]})] + routes[index] = [self.response(index, {"versions": ["0.7.0", "0.8.0"]})] routes[package] = [urllib.error.HTTPError(package, 404, "not found", {}, io.BytesIO())] return routes @@ -911,9 +919,9 @@ def test_unreadable_or_occupied_feed_never_reaches_candidate_work(self): elif case == "bad-json": replacement = self.response(index, None, raw=b"not json") elif case == "duplicate-json": - replacement = self.response(index, None, raw=b'{"versions":["0.8.0"],"versions":["0.7.0"]}') + replacement = self.response(index, None, raw=b'{"versions":["0.9.0"],"versions":["0.8.0"]}') elif case == "nonfinite-json": - replacement = self.response(index, None, raw=b'{"versions":["0.7.0"],"ignored":NaN}') + replacement = self.response(index, None, raw=b'{"versions":["0.8.0"],"ignored":NaN}') elif case == "oversize": replacement = self.response(index, None, raw=b" " * (gate.MAX_BYTES + 1)) elif case == "length": @@ -927,9 +935,9 @@ def test_unreadable_or_occupied_feed_never_reaches_candidate_work(self): "never-print-this-secret", {}, io.BytesIO())] replacement = None else: - values = {"empty": [], "shape": "0.7.0", "missing-predecessor": ["0.6.0"], - "occupied": ["0.7.0", "0.8.0"], "normalized-occupied": ["0.7", "0.8.0.0"], - "newer": ["0.7.0", "0.9.0"], "malformed-version": ["0.7.0", "never-print-this-secret"]} + values = {"empty": [], "shape": "0.8.0", "missing-predecessor": ["0.7.0"], + "occupied": ["0.8.0", "0.9.0"], "normalized-occupied": ["0.8", "0.9.0.0"], + "newer": ["0.8.0", "0.10.0"], "malformed-version": ["0.8.0", "never-print-this-secret"]} replacement = self.response(index, {"versions": values[case]}) if replacement is not None: routes[index][0].close() @@ -1007,9 +1015,9 @@ def test_source_journal_literal_is_checked_without_claiming_native_absence(self) (root / path).parent.mkdir(parents=True, exist_ok=True) (root / path).write_bytes((ROOT / path).read_bytes()) self.gate.source_bindings(root) - for path, old, new in [(paths[0], "0.8.0", "0.7.0"), - (paths[1], 'VERSION = "0.8.0"', 'VERSION = "0.6.0"'), - (paths[2], "utel-host-rel-14", "utel-host-rel-12")]: + for path, old, new in [(paths[0], "0.9.0", "0.8.0"), + (paths[1], 'VERSION = "0.9.0"', 'VERSION = "0.8.0"'), + (paths[2], "utel-host-rel-15", "utel-host-rel-14")]: original = (root / path).read_text() (root / path).write_text(original.replace(old, new)) with self.assertRaises(self.gate.Refused): @@ -1034,7 +1042,7 @@ def check(text): assert "name: Retain passive Host HTTP diagnostics" in step assert "always() && steps.read-first.outcome == 'success'" in step assert "dotnet msbuild" in text and "Refuse changed main before retaining candidate" in text - assert "--version 0.8.0 --tag telemetry-host/v0.8.0" in text + assert "--version 0.9.0 --tag telemetry-host/v0.9.0" in text check(source) gate = source.split(" - name: Refuse unreadable", 1)[1].split(" - uses: actions/setup-dotnet", 1)[0]