release: ship governed read federation and operability - #142
Conversation
GSTACK-Checkpoint: 2026-07-12/p1-policy-audit-logging#1 Signed-off-by: Gnani Rahul <gnani.nutakki@gmail.com>
…-audit-logging feat(pep): add structured policy audit logging
GSTACK-Checkpoint: 2026-07-12/release-branch-safety#1 Signed-off-by: Gnani Rahul <gnani.nutakki@gmail.com>
…branch-safety docs(release): prevent durable branch deletion
Add an isolated Prometheus registry and embeddable handler for bounded policy and federation measurements. Passive observers preserve fail-closed reads, normalize every label to a closed vocabulary, and recover from observer faults without changing authorization or snapshot behavior. GSTACK-Checkpoint: 2026-07-12/p1-observability-metrics#1 Signed-off-by: Gnani Rahul <gnani.nutakki@gmail.com>
…ability-metrics feat(e10): add bounded self-observability metrics
Fail release verification closed and record the upstream ClusterGateway authorization gate. GSTACK-Checkpoint: 2026-07-13/security-ocm-upstream-monitor#1 Signed-off-by: Gnani Rahul <gnani.nutakki@gmail.com>
Bumps the actions group with 1 update: [actions/checkout](https://github.com/actions/checkout). Updates `actions/checkout` from 6.0.3 to 7.0.0 - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](actions/checkout@df4cb1c...9c091bb) --- updated-dependencies: - dependency-name: actions/checkout dependency-version: 7.0.0 dependency-type: direct:production update-type: version-update:semver-major dependency-group: actions ... Signed-off-by: dependabot[bot] <support@github.com>
…tions-2217aebe03 chore(deps): bump actions/checkout from 6.0.3 to 7.0.0 in the actions group
…urity-ocm-monitor-20260713 fix(security): pin secure Go toolchain
Add the scoped managed-serviceaccount and direct Konnectivity adapter as a bounded, TLS-pinned alternative to the blocked ClusterGateway route. GSTACK-Checkpoint: 2026-07-13/e2-direct-konnectivity-transport#1 Signed-off-by: Gnani Rahul <gnani.nutakki@gmail.com>
…-konnectivity-transport feat(e2): add direct ClusterProxy snapshot transport
GSTACK-Checkpoint: 2026-07-14/e2-hub-direct-runtime#1 Signed-off-by: Gnani Rahul <gnani.nutakki@gmail.com>
…rect-runtime feat(hub): compose direct OCM runtime
GSTACK-Checkpoint: 2026-07-14/e2-image-digest-search#1 Signed-off-by: Gnani Rahul <gnani.nutakki@gmail.com>
…digest-search feat(e2): add immutable image digest search
GSTACK-Checkpoint: 2026-07-14/e9-hub-migrate#1 Signed-off-by: Gnani Rahul <gnani.nutakki@gmail.com>
…grate feat(e9): add isolated hub migration command
GSTACK-Checkpoint: 2026-07-14/e9-oci-image#1 Signed-off-by: Gnani Rahul <gnani.nutakki@gmail.com>
feat(e9): add immutable OCI image contract
GSTACK-Checkpoint: 2026-07-14/e9-helm-contract#1 Signed-off-by: Gnani Rahul <gnani.nutakki@gmail.com>
…ontract feat(e9): add fail-closed Helm hub chart
GSTACK-Checkpoint: 2026-07-14/e9-chart-profiles#1 Signed-off-by: Gnani Rahul <gnani.nutakki@gmail.com>
…profiles feat(e9): add fail-closed hub resource profiles
GSTACK-Checkpoint: 2026-07-14/e10-trace-context#1 Signed-off-by: Gnani Rahul <gnani.nutakki@gmail.com> Co-authored-by: Gnani Rahul <gnani.nutakki@gmail.com>
Emit one closed local warning for every pre-principal hub authentication refusal without exposing request metadata, credentials, verifier errors, or caller correlation values. Keep correlation scoped to verified hub work and retain bounded asynchronous log delivery as the separately reviewed #140 follow-up. GSTACK-Checkpoint: 2026-07-14/e10-auth-refusal-logs#1 Signed-off-by: Gnani Rahul <gnani.nutakki@gmail.com>
…refusal-logs feat(e10): add sanitized auth refusal logs
📝 WalkthroughWalkthroughThe PR implements a governed hub runtime with direct OCM transport, immutable image search, fail-closed Helm and OCI deployment contracts, isolated migrations, local observability, authentication refusal logging, tracing, and expanded CI/e2e validation. ChangesGoverned hub delivery
Estimated code review effort: 5 (Critical) | ~120 minutes Possibly related issues
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 7
🧹 Nitpick comments (2)
internal/hubocm/credentials.go (1)
77-96: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueRedundant key-validation loop.
Once
len(secret.Data) != 2is checked and bothhasToken/hasCAare confirmed true, the map can only contain exactly{"token", "ca.crt"}— the trailingfor key := range secret.Dataloop (lines 90-94) can never trigger. Harmless, but dead defensive code that could be dropped for clarity.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@internal/hubocm/credentials.go` around lines 77 - 96, Remove the redundant key-validation loop from credentialFromSecret. Keep the len(secret.Data) check and hasToken/hasCA validation unchanged, then proceed directly to constructing the projectedCredential.README.md (1)
191-193: 🩺 Stability & Availability | 🔵 TrivialVerify chart
defaultModematches this strict read-only-file contract.This documents (and
runtime_test.goenforces) that every mounted key/cert/CA file must be read-only — a writable file (e.g. mode0600) is rejected. Kubernetes' own Secret-volume defaultdefaultModeis0644, which sets the owner-write bit. Worth confirmingcharts/sith-hub's volume specs explicitly set a restrictivedefaultMode(e.g.0400) for every mounted secret/cert/CA file, otherwise a chart install using Kubernetes defaults would fail this fail-closed startup check.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@README.md` around lines 191 - 193, Update the volume specifications in the sith-hub chart so every Secret, certificate, key, and CA mount explicitly sets a read-only defaultMode such as 0400, matching the runtime’s strict regular-file permission check. Audit all relevant mounted-file volumes and avoid relying on Kubernetes’ writable 0644 Secret-volume default.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@charts/sith-hub/README.md`:
- Line 15: Update the issue reference line in the README so it no longer begins
with `#103`; prefix the reference with descriptive prose while preserving the
issue link or reference content.
In `@docs/experiments/M0-ocm-falsification.md`:
- Around line 193-195: Update the permissions description for the M0 sith-reader
identity to include both its cluster-wide list grants for Pods, Deployments, and
Rollouts and its namespaced get/list grants for Pods, Services, and
services/proxy, or explicitly distinguish separate identities if that is the
intended design. Ensure the surrounding statements about denied Secrets, Nodes,
writes, watches, and hub API access remain accurate.
In `@internal/hubdb/migrations/0006_fleet_image_digests.sql`:
- Around line 5-7: Update the fleet_image_digests migration around
fleet_facts_inventory_image_digests_idx so index creation does not block writes:
move the concurrent index build to a non-transactional migration path, or
explicitly schedule and document the required write pause before the
transactional CREATE INDEX.
In `@internal/hubserver/auth.go`:
- Around line 120-122: Update refuseAuthentication so AuthObserver delivery
follows the bounded, nonblocking contract from issue `#140` before calling
writeUnauthorized; avoid allowing a blocked observer or congested sink to delay
the response, while preserving the refusal event and unauthorized response
behavior.
In `@Makefile`:
- Around line 11-16: Replace the machine-specific default value of
OCM_SCRATCH_ROOT in the Makefile with a portable scratch-directory path suitable
for CI and other developer environments, while preserving OCM_PREFIX and the
ability to override the variable externally.
In `@sessions/2026-07-13-e2-direct-konnectivity-transport.md`:
- Line 15: Update the sentence beginning with “#103/#104” in the scope paragraph
to prefix the issue references with descriptive prose, preventing Markdown from
interpreting them as a heading while preserving the existing out-of-scope and
upstream-release meaning.
In `@tests/e2e/oci_image_kind_test.go`:
- Around line 8-12: Update the OCI image kind contract test to decode the CLI
JSON response and validate the actual version field, rather than using
strings.Contains on raw output. Reject valid JSON objects that omit version or
place the word in an unrelated field, and remove the now-unneeded
substring-matching usage.
---
Nitpick comments:
In `@internal/hubocm/credentials.go`:
- Around line 77-96: Remove the redundant key-validation loop from
credentialFromSecret. Keep the len(secret.Data) check and hasToken/hasCA
validation unchanged, then proceed directly to constructing the
projectedCredential.
In `@README.md`:
- Around line 191-193: Update the volume specifications in the sith-hub chart so
every Secret, certificate, key, and CA mount explicitly sets a read-only
defaultMode such as 0400, matching the runtime’s strict regular-file permission
check. Audit all relevant mounted-file volumes and avoid relying on Kubernetes’
writable 0644 Secret-volume default.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: d16cdc4f-1f3f-4e5b-8877-8a1c68d4c17d
⛔ Files ignored due to path filters (1)
go.sumis excluded by!**/*.sum
📒 Files selected for processing (96)
.github/workflows/ci.yml.github/workflows/release.ymlContainerfileMakefileREADME.mdcharts/sith-hub/Chart.yamlcharts/sith-hub/README.mdcharts/sith-hub/templates/NOTES.txtcharts/sith-hub/templates/_helpers.tplcharts/sith-hub/templates/clusterrole.yamlcharts/sith-hub/templates/clusterrolebinding.yamlcharts/sith-hub/templates/deployment.yamlcharts/sith-hub/templates/migration-job.yamlcharts/sith-hub/templates/service.yamlcharts/sith-hub/templates/serviceaccount.yamlcharts/sith-hub/values.schema.jsoncharts/sith-hub/values.yamldocs/CONVENTIONS.mddocs/RELEASE.mddocs/ROADMAP.mddocs/experiments/M0-ocm-falsification.mdgo.modhack/experiments/m0-ocm-falsification.shinternal/cli/cli_test.gointernal/cli/hub.gointernal/fleet/graph.gointernal/fleet/graph_test.gointernal/hubdb/fleet.gointernal/hubdb/fleet_query_test.gointernal/hubdb/migrate.gointernal/hubdb/migrate_test.gointernal/hubdb/migrations/0006_fleet_image_digests.sqlinternal/hubdb/postgres_integration_test.gointernal/hubfleet/collector.gointernal/hubfleet/image_search.gointernal/hubfleet/image_search_test.gointernal/hubfleet/metrics.gointernal/hubfleet/metrics_test.gointernal/hubfleet/policy_test.gointernal/hubfleet/source.gointernal/hubfleet/tracing_test.gointernal/hubocm/credentials.gointernal/hubocm/direct.gointernal/hubocm/direct_test.gointernal/hubocm/doc.gointernal/hubocm/ocm_integration_test.gointernal/hubruntime/config.gointernal/hubruntime/doc.gointernal/hubruntime/migrate.gointernal/hubruntime/migrate_test.gointernal/hubruntime/ocm_integration_test.gointernal/hubruntime/runtime.gointernal/hubruntime/runtime_test.gointernal/hubserver/auth.gointernal/hubserver/auth_observability.gointernal/hubserver/auth_observability_test.gointernal/hubserver/auth_test.gointernal/hubserver/fleet.gointernal/hubserver/fleet_test.gointernal/observability/auth.gointernal/observability/auth_test.gointernal/observability/metrics.gointernal/observability/metrics_test.gointernal/observability/tracing.gointernal/observability/tracing_test.gointernal/pep/audit.gointernal/pep/audit_test.gointernal/pep/metrics.gointernal/pep/metrics_test.gointernal/pep/pep.gointernal/pep/tracing_test.gointernal/privacy/boundary_test.gointernal/tracing/context.gointernal/tracing/event.gointernal/tracing/tracing_test.gosessions/2026-07-12-p1-observability-metrics.mdsessions/2026-07-12-p1-policy-audit-logging.mdsessions/2026-07-12-release-branch-safety.mdsessions/2026-07-13-e2-direct-konnectivity-transport.mdsessions/2026-07-13-security-ocm-upstream-monitor.mdsessions/2026-07-14-e10-auth-refusal-logs.mdsessions/2026-07-14-e10-trace-context.mdsessions/2026-07-14-e2-hub-direct-runtime.mdsessions/2026-07-14-e2-image-digest-search.mdsessions/2026-07-14-e9-chart-profiles.mdsessions/2026-07-14-e9-helm-contract.mdsessions/2026-07-14-e9-hub-migrate.mdsessions/2026-07-14-e9-oci-image.mdtests/e2e/helm_chart_test.gotests/e2e/oci_image_helpers_test.gotests/e2e/oci_image_inspect_test.gotests/e2e/oci_image_kind_test.gotests/e2e/oci_image_test.gotests/e2e/smoke_test.gotests/scripts/m0_ocm_falsification_safety_test.shtests/testutil/ocmlab/ocmlab.go
| | `migration.existingSecret` | `owner-database-url` | short-lived `sith hub migrate` hook Job | | ||
|
|
||
| `migration.applicationRole` is a non-secret PostgreSQL role name. The migration hook runs before install and upgrade, blocks the release if it fails, and receives no Kubernetes service-account token or runtime TLS material. The Deployment receives an in-cluster token only to read the fixed `sith-reader` managed-serviceaccount Secret; its ClusterRole permits exactly `get` on that one resource name and no list/watch or write verbs. | ||
|
|
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Avoid rendering the issue reference as a heading.
This line starts with #103, triggering MD018 and rendering unexpectedly. Prefix the references with prose.
Proposed fix
-#103/#104 route remains out of scope and blocked pending an official upstream release.
+issues `#103` and `#104` remain out of scope and blocked pending an official upstream release.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@charts/sith-hub/README.md` at line 15, Update the issue reference line in the
README so it no longer begins with `#103`; prefix the reference with descriptive
prose while preserving the issue link or reference content.
Source: Linters/SAST tools
| To support that product read boundary, each M0 `sith-reader` gets only cluster-wide `list` on | ||
| Pods, Deployments, and Rollouts. The existing namespaced service-proxy Role is separate; there is | ||
| still no grant for Secrets, Nodes, writes, watches, or hub API access. |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Reconcile the stated sith-reader permissions.
This says the identity has only cluster-wide list access for Pods, Deployments, and Rollouts, but Lines 213-216 say the same M0 identity also has namespaced get/list access to Pods, Services, and services/proxy. Document the actual combined RBAC grants or distinguish the identities.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@docs/experiments/M0-ocm-falsification.md` around lines 193 - 195, Update the
permissions description for the M0 sith-reader identity to include both its
cluster-wide list grants for Pods, Deployments, and Rollouts and its namespaced
get/list grants for Pods, Services, and services/proxy, or explicitly
distinguish separate identities if that is the intended design. Ensure the
surrounding statements about denied Secrets, Nodes, writes, watches, and hub API
access remain accurate.
| CREATE INDEX fleet_facts_inventory_image_digests_idx | ||
| ON sith.fleet_facts USING GIN ((payload -> 'image_digests')) | ||
| WHERE kind = 'inventory'; |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
ast-grep outline internal/hubdb/migrate.go --items all
rg -n -C4 'BeginTx|Begin\(|Commit\(|Rollback\(|Exec(Context)?\(' \
internal/hubdb/migrate.go internal/hubdb/migrate_test.go
rg -n -C4 '0006_fleet_image_digests|CREATE INDEX' \
internal/hubdb/migrate.go internal/hubdb/migrate_test.go \
internal/hubdb/migrations/0006_fleet_image_digests.sqlRepository: ArdurAI/sith
Length of output: 4921
Avoid blocking writes during this index build. This migration runs in a transaction, so CREATE INDEX CONCURRENTLY is not usable here as-is; either move it to a non-transactional path or explicitly schedule the write pause.
🧰 Tools
🪛 Squawk (2.59.0)
[warning] 5-7: During normal index creation, table updates are blocked, but reads are still allowed. Use concurrently to avoid blocking writes.
(require-concurrent-index-creation)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@internal/hubdb/migrations/0006_fleet_image_digests.sql` around lines 5 - 7,
Update the fleet_image_digests migration around
fleet_facts_inventory_image_digests_idx so index creation does not block writes:
move the concurrent index build to a non-transactional migration path, or
explicitly schedule and document the required write pause before the
transactional CREATE INDEX.
Source: Linters/SAST tools
| func refuseAuthentication(observer AuthObserver, response http.ResponseWriter) { | ||
| ObserveAuth(observer, AuthEvent{Outcome: AuthOutcomeRefused}) | ||
| writeUnauthorized(response) |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift
Make refusal observation nonblocking before the unauthorized response.
Line 121 invokes the observer synchronously before writeUnauthorized. A blocked observer or congested log sink can therefore hold attacker-triggered authentication requests indefinitely, creating a log-backpressure DoS path despite the observer being documented as passive. Implement the bounded/nonblocking delivery contract tracked in #140 before routing the runtime logger through this path.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@internal/hubserver/auth.go` around lines 120 - 122, Update
refuseAuthentication so AuthObserver delivery follows the bounded, nonblocking
contract from issue `#140` before calling writeUnauthorized; avoid allowing a
blocked observer or congested sink to delay the response, while preserving the
refusal event and unauthorized response behavior.
| HELM ?= helm | ||
| GORELEASER ?= goreleaser | ||
| DOCKER ?= docker | ||
| KUBECTL ?= kubectl | ||
| OCM_SCRATCH_ROOT ?= /Volumes/EXTENDED/tmp/sith-m0 | ||
| OCM_PREFIX ?= sith-m0 |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win
Non-portable hardcoded default for OCM_SCRATCH_ROOT.
/Volumes/EXTENDED/tmp/sith-m0 is a macOS external-volume path specific to one developer's machine. As the checked-in default for e2e-ocm, this will fail to create/use scratch space on CI runners and any other contributor's machine unless explicitly overridden every time.
💡 Suggested fix: use a portable default
-OCM_SCRATCH_ROOT ?= /Volumes/EXTENDED/tmp/sith-m0
+OCM_SCRATCH_ROOT ?= $(shell mktemp -d /tmp/sith-m0.XXXXXX 2>/dev/null || echo /tmp/sith-m0)📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| HELM ?= helm | |
| GORELEASER ?= goreleaser | |
| DOCKER ?= docker | |
| KUBECTL ?= kubectl | |
| OCM_SCRATCH_ROOT ?= /Volumes/EXTENDED/tmp/sith-m0 | |
| OCM_PREFIX ?= sith-m0 | |
| HELM ?= helm | |
| GORELEASER ?= goreleaser | |
| DOCKER ?= docker | |
| KUBECTL ?= kubectl | |
| OCM_SCRATCH_ROOT ?= $(shell mktemp -d /tmp/sith-m0.XXXXXX 2>/dev/null || echo /tmp/sith-m0) | |
| OCM_PREFIX ?= sith-m0 |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@Makefile` around lines 11 - 16, Replace the machine-specific default value of
OCM_SCRATCH_ROOT in the Makefile with a portable scratch-directory path suitable
for CI and other developer environments, while preserving OCM_PREFIX and the
ability to override the variable externally.
|
|
||
| [S] Scope: `internal/hubocm`, the M0 experiment and its safety suite, narrowly reviewed privacy | ||
| boundary exceptions, dependencies, and operator-facing documentation. The ClusterGateway-specific | ||
| #103/#104 route remains out of scope and blocked pending an official upstream release. |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Prevent the issue reference from becoming a heading.
Prefix the references with prose so this remains a continuation of the scope paragraph.
Proposed fix
-#103/#104 route remains out of scope and blocked pending an official upstream release.
+issues `#103` and `#104` remain out of scope and blocked pending an official upstream release.📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| #103/#104 route remains out of scope and blocked pending an official upstream release. | |
| issues `#103` and `#104` remain out of scope and blocked pending an official upstream release. |
🧰 Tools
🪛 markdownlint-cli2 (0.23.0)
[warning] 15-15: No space after hash on atx style heading
(MD018, no-missing-space-atx)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@sessions/2026-07-13-e2-direct-konnectivity-transport.md` at line 15, Update
the sentence beginning with “#103/#104” in the scope paragraph to prefix the
issue references with descriptive prose, preventing Markdown from interpreting
them as a heading while preserving the existing out-of-scope and
upstream-release meaning.
Source: Linters/SAST tools
| "encoding/json" | ||
| "fmt" | ||
| "os/exec" | ||
| "runtime" | ||
| "strings" |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Validate the decoded version field, not a substring.
json.Valid plus strings.Contains accepts unrelated output such as {"message":"version"}, allowing the contract test to pass after the CLI drops its version field.
Proposed fix
import (
"context"
"encoding/json"
"fmt"
"os/exec"
"runtime"
- "strings"
"testing"
"time"
)
...
output, err := client.CoreV1().Pods("default").GetLogs(pods.Items[0].Name, &corev1.PodLogOptions{}).Do(ctx).Raw()
- if err != nil || !json.Valid(output) || !strings.Contains(string(output), "\"version\"") {
+ var result struct {
+ Version string `json:"version"`
+ }
+ if err != nil || json.Unmarshal(output, &result) != nil || result.Version == "" {
t.Fatalf("OCI Job output on %s = %q / %v", clusterName, output, err)
}Also applies to: 113-115
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@tests/e2e/oci_image_kind_test.go` around lines 8 - 12, Update the OCI image
kind contract test to decode the CLI JSON response and validate the actual
version field, rather than using strings.Contains on raw output. Reject valid
JSON objects that omit version or place the word in an unrelated field, and
remove the now-unneeded substring-matching usage.
Release scope
Promote the coherent governed-read and operability increment accumulated on dev since main merge 5c68df7.
Gates
Known boundaries
This PR preserves dev.
Summary by CodeRabbit
New Features
Documentation
Tests