Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ HELM ?= helm
GORELEASER ?= goreleaser
DOCKER ?= docker
KUBECTL ?= kubectl
OCM_SCRATCH_ROOT ?= /Volumes/EXTENDED/tmp/sith-m0
OCM_SCRATCH_ROOT ?= $(shell python3 -c 'import os; print(os.path.join(os.path.realpath(os.environ.get("TMPDIR", "/tmp")), "sith-m0-{}".format(os.getuid()), "lab"))')
OCM_PREFIX ?= sith-m0

KIND_NODE_IMAGE ?= kindest/node:v1.36.1@sha256:3489c7674813ba5d8b1a9977baea8a6e553784dab7b84759d1014dbd78f7ebd5
Expand Down
20 changes: 11 additions & 9 deletions docs/experiments/M0-ocm-falsification.md
Original file line number Diff line number Diff line change
Expand Up @@ -135,12 +135,13 @@ including `--force-internal-endpoint-lookup` for local kind clusters.

## Reproduce

The default scratch root is `/Volumes/EXTENDED/tmp/sith-m0`; the runner refuses another
filesystem unless the operator explicitly opts in. The path must be canonical, must not already
exist for a fresh run, and is deleted only when its regular ownership marker belongs to the
current user. The runner verifies the entered parent's device/inode against the validated path,
enters it once, and uses relative child paths from that held working directory, so racing or later
renaming a writable ancestor cannot redirect creation or cleanup.
The default scratch root is a private canonical `${TMPDIR:-/tmp}/sith-m0-<uid>/lab`; the runner
creates its parent with mode `0700`. Another non-EXTENDED path requires the operator to set
`SITH_M0_ALLOW_NON_EXTENDED=1` deliberately. The path must be canonical, must not already exist
for a fresh run, and is deleted only when its regular ownership marker belongs to the current
user. The runner verifies the entered parent's device/inode against the validated path, enters it
once, and uses relative child paths from that held working directory, so racing or later renaming a
writable ancestor cannot redirect creation or cleanup.
It uses a dedicated kubeconfig and isolated Helm state, requires a local Unix-socket Docker
endpoint, gives the image builder only a fixture-only context, and never reads or modifies the
user's kubeconfig.
Expand Down Expand Up @@ -190,9 +191,10 @@ through the narrow `get` reader. It requires direct TLS-verified snapshots from
MSA-token `Forbidden` for a cluster-wide Secrets list, and a replacement projection with a changed
token before a subsequent snapshot. No token, CA, response body, or port-forward output is printed.

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.
To support that product read boundary, each M0 `sith-reader` gets cluster-wide `list` on Pods,
Deployments, and Rollouts plus namespaced `get/list` on Pods, Services, and `services/proxy` in
`sith-demo`. Those are the complete combined grants; there is still no grant for Secrets, Nodes,
writes, watches, or hub API access.

## What the runner proves

Expand Down
31 changes: 26 additions & 5 deletions hack/experiments/m0-ocm-falsification.sh
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,16 @@ readonly SPOKE_B_NAME="${LAB_PREFIX}-spoke-b"
readonly HUB_CONTEXT="kind-${HUB_NAME}"
readonly SPOKE_A_CONTEXT="kind-${SPOKE_A_NAME}"
readonly SPOKE_B_CONTEXT="kind-${SPOKE_B_NAME}"
readonly SCRATCH_ROOT="${SITH_M0_SCRATCH_ROOT:-/Volumes/EXTENDED/tmp/sith-m0}"
DEFAULT_TMPDIR="$(${PYTHON_BIN} - "${TMPDIR:-/tmp}" <<'PY'
import os
import sys

print(os.path.realpath(sys.argv[1]))
PY
)"
readonly DEFAULT_TMPDIR
readonly DEFAULT_SCRATCH_ROOT="${DEFAULT_TMPDIR}/sith-m0-${EUID}/lab"
readonly SCRATCH_ROOT="${SITH_M0_SCRATCH_ROOT:-${DEFAULT_SCRATCH_ROOT}}"
SCRATCH_PARENT="$(dirname "${SCRATCH_ROOT}")"
readonly SCRATCH_PARENT
SCRATCH_NAME="$(basename "${SCRATCH_ROOT}")"
Expand Down Expand Up @@ -66,7 +75,7 @@ Commands:

Environment:
SITH_M0_KEEP_CLUSTERS=1 Retain clusters and scratch after `run`.
SITH_M0_SCRATCH_ROOT=<path> Scratch path; defaults to EXTENDED storage.
SITH_M0_SCRATCH_ROOT=<path> Scratch path; defaults to a private TMPDIR path.
SITH_M0_ALLOW_NON_EXTENDED=1 Permit an explicit non-EXTENDED scratch path.
SITH_M0_PREFIX=<name> Override the disposable kind cluster prefix.

Expand Down Expand Up @@ -125,8 +134,8 @@ PY
case "${SCRATCH_ROOT}" in
/Volumes/EXTENDED/*) ;;
*)
[[ "${SITH_M0_ALLOW_NON_EXTENDED:-0}" == "1" ]] ||
die "scratch must remain on /Volumes/EXTENDED (or explicitly set SITH_M0_ALLOW_NON_EXTENDED=1)"
[[ "${SCRATCH_ROOT}" == "${DEFAULT_SCRATCH_ROOT}" || "${SITH_M0_ALLOW_NON_EXTENDED:-0}" == "1" ]] ||
die "scratch must use the private default or explicitly set SITH_M0_ALLOW_NON_EXTENDED=1"
;;
esac

Expand Down Expand Up @@ -160,6 +169,14 @@ validate_scratch_parent() {
scratch_directory_identity "${SCRATCH_PARENT}" >/dev/null
}

prepare_default_scratch_parent() {
[[ "${SCRATCH_ROOT}" == "${DEFAULT_SCRATCH_ROOT}" ]] || return 0
[[ ! -e "${SCRATCH_PARENT}" && ! -L "${SCRATCH_PARENT}" ]] || return 0

umask 077
mkdir -p -m 0700 -- "${SCRATCH_PARENT}"
}

enter_scratch_parent() {
local entered_identity
local expected_identity
Expand Down Expand Up @@ -253,6 +270,7 @@ check_tools() {
}

prepare_scratch() {
prepare_default_scratch_parent
enter_scratch_parent
[[ ! -e "${SCRATCH_NAME}" && ! -L "${SCRATCH_NAME}" ]] ||
die "scratch root already exists; use verify or cleanup before a fresh run"
Expand Down Expand Up @@ -289,6 +307,9 @@ delete_clusters() {
}

remove_scratch() {
if [[ "${SCRATCH_ROOT}" == "${DEFAULT_SCRATCH_ROOT}" && ! -e "${SCRATCH_PARENT}" && ! -L "${SCRATCH_PARENT}" ]]; then
return 0
fi
enter_scratch_parent
[[ -e "${SCRATCH_NAME}" || -L "${SCRATCH_NAME}" ]] || return 0
validate_owned_scratch
Expand Down Expand Up @@ -859,6 +880,7 @@ run_lab() {
local started_at
local finished_at
started_at="$(date +%s)"
prepare_default_scratch_parent
enter_scratch_parent
check_tools
ensure_lab_absent
Expand All @@ -878,7 +900,6 @@ run_lab() {
}

cleanup_lab() {
enter_scratch_parent
check_tools
delete_clusters || die "cluster deletion failed; retaining scratch for recovery"
remove_scratch
Expand Down
2 changes: 1 addition & 1 deletion sessions/2026-07-13-e2-direct-konnectivity-transport.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ cross the `hubfleet.Transport` seam.

[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.
Issues #103 and #104 remain out of scope and blocked pending an official upstream release.

[A] Action: chose the released ClusterProxy `0.10.0`-matched Konnectivity client `v0.31.2`, rather
than an unreleased ClusterGateway fix or a custom agent/tunnel. The adapter treats
Expand Down
59 changes: 59 additions & 0 deletions sessions/2026-07-14-release-hardening.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# Session — 2026-07-14 — release hardening

**Issue:** [#143](https://github.com/ArdurAI/sith/issues/143)
**Branch:** `gnanirahulnutakki/fix/release-hardening`
**Base:** `origin/dev` at `ba102f467415db3e309cbf35fe7b02f88929c593`

## [G] Goal

Resolve the portable-scratch, OCI assertion, and operator-documentation findings discovered during
the governed-read release promotion before cutting a stable public tag.

## [S] Scope

- Replace the machine-specific M0 scratch default with a canonical private per-UID temporary path.
- Preserve the explicit opt-in for arbitrary non-EXTENDED roots, private-parent validation, held
directory identity, marker-owned cleanup, and local-Docker requirement.
- Decode the OCI Job's JSON and require a non-empty `version` field.
- Correct the rendered issue reference and document the complete combined M0 reader grants.

## [A] Analysis and red-team checks

- The first real M0 run exposed macOS `$TMPDIR` aliasing through `/var` to `/private/var`. The
harness correctly rejected that non-canonical path, so the default is canonicalized rather than
weakening the no-symlink rule.
- The second run exposed that the default private parent was prepared after the first entry
validation. Preparation now happens before entry; the helper remains idempotent for focused
safety tests.
- The generated default is the only non-EXTENDED root accepted without an opt-in. Any other
non-EXTENDED override still requires `SITH_M0_ALLOW_NON_EXTENDED=1` and then must pass the same
canonical, owner, mode, and inode checks.
- `CREATE INDEX` in migration `0006` remains transactional by design: the indexed table is created
empty in that same migration, so no pre-existing workload can be blocked by that index creation.
- Authentication refusal delivery remains separately tracked in [#140](https://github.com/ArdurAI/sith/issues/140): the existing arbitrary `slog` sink contract cannot guarantee both nonblocking delivery and leak-free shutdown without a dedicated lifecycle-owned transport.

## [T] Tests and evidence

- Shell safety suite: PASS — `bash -n hack/experiments/m0-ocm-falsification.sh` and
`make test-scripts` (19 assertions), including the generated default lifecycle and a symlinked
temporary-directory canonicalization case.
- OCI assertion unit: PASS — `go test -race -count=1 -tags='e2e kind' -run
'^TestValidateVersionOutput$' ./tests/e2e`; valid JSON without `version` and empty versions fail.
- Real M0 integration: PASS — `KIND=/Volumes/EXTENDED/MacData/tools/bin/kind make e2e-ocm` created
a hub and two spokes under the portable default, proved scoped reads, Secrets/Nodes denial,
outbound-only controls, credential replacement, direct adapter and runtime tests, then removed
all three clusters. M0 elapsed time was 186 seconds.
- Real multi-cluster Kind integration: PASS — `KIND=/Volumes/EXTENDED/MacData/tools/bin/kind make
e2e-kind` in 177.961 seconds.
- Repository CI: PASS — `make ci`, including race tests, static analysis (0 issues), dependency
vulnerability scan (no vulnerabilities), shell safety, UI latency, and tagged binary e2e.
- Isolation and release: PASS — `make e2e-isolation` with PostgreSQL/RLS coverage and the fixed
50,000-execution workspace fuzz campaign; `make release-check` rebuilt and verified all four
Darwin/Linux archives twice, produced SPDX SBOMs, and rendered the formula.
- Independent local staged-diff review: one stale error-message finding, fixed and rerun through
focused syntax, safety, OCI unit, formatting, and whitespace checks.

## [C] Checkpoint

- README review found no required change. The source, documentation, tests, session evidence, and
validations are ready for a signed, DCO, GSTACK commit and a narrow PR into `dev`.
40 changes: 38 additions & 2 deletions tests/e2e/oci_image_kind_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import (
"fmt"
"os/exec"
"runtime"
"strings"
"testing"
"time"

Expand Down Expand Up @@ -111,7 +110,44 @@ func assertOCIImageJob(ctx context.Context, t *testing.T, kindBinary, clusterNam
t.Fatalf("find OCI Job Pod on %s: %#v", clusterName, pods.Items)
}
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\"") {
if err == nil {
err = validateVersionOutput(output)
}
if err != nil {
t.Fatalf("OCI Job output on %s = %q / %v", clusterName, output, err)
}
}

func validateVersionOutput(output []byte) error {
var versionOutput struct {
Version string `json:"version"`
}
if err := json.Unmarshal(output, &versionOutput); err != nil {
return fmt.Errorf("decode version output: %w", err)
}
if versionOutput.Version == "" {
return fmt.Errorf("version output omitted a non-empty version")
}
return nil
}

func TestValidateVersionOutput(t *testing.T) {
t.Parallel()

for name, test := range map[string]struct {
output string
valid bool
}{
"version": {output: `{"version":"v0.2.0"}`, valid: true},
"unrelated version string": {output: `{"message":"version"}`},
"empty version": {output: `{"version":""}`},
"invalid JSON": {output: `{`},
} {
t.Run(name, func(t *testing.T) {
err := validateVersionOutput([]byte(test.output))
if (err == nil) != test.valid {
t.Fatalf("validateVersionOutput(%q) error = %v, want valid=%t", test.output, err, test.valid)
}
})
}
}
25 changes: 25 additions & 0 deletions tests/scripts/m0_ocm_falsification_safety_test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,31 @@ env SITH_M0_SCRATCH_ROOT="${owned_root}" SITH_M0_ALLOW_NON_EXTENDED=1 \
' _ "${SCRIPT}"
pass "owned scratch lifecycle preserves the valid control"

default_tmpdir="${TEST_ROOT}/default-tmp"
mkdir -m 0700 "${default_tmpdir}"
env TMPDIR="${default_tmpdir}" bash -c '
source "$1"
[[ "${SCRATCH_ROOT}" == "${DEFAULT_SCRATCH_ROOT}" ]]
prepare_scratch
validate_owned_scratch
remove_scratch
[[ ! -e "${SCRATCH_ROOT}" ]]
' _ "${SCRIPT}"
pass "portable default scratch lifecycle remains private and removable"

default_tmpdir_alias="${TEST_ROOT}/default-tmp-alias"
ln -s "${default_tmpdir}" "${default_tmpdir_alias}"
env TMPDIR="${default_tmpdir_alias}" bash -c '
source "$1"
[[ "${DEFAULT_TMPDIR}" == "${2}" ]]
[[ "${SCRATCH_ROOT}" == "${DEFAULT_SCRATCH_ROOT}" ]]
prepare_scratch
validate_owned_scratch
remove_scratch
[[ ! -e "${SCRATCH_ROOT}" ]]
' _ "${SCRIPT}" "${default_tmpdir}"
pass "portable default canonicalizes a symlinked temporary directory"

unowned_root="${TEST_ROOT}/unowned-root"
mkdir -p "${unowned_root}"
printf 'keep\n' >"${unowned_root}/sentinel"
Expand Down