Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
59 commits
Select commit Hold shift + click to select a range
c90aa6b
docs(k3s): add P0 opt-in owner proof - Dockerfile
kvnloo Oct 28, 2026
a2acb63
docs(k3s): add P0 opt-in owner proof - k3s.yaml
kvnloo Oct 28, 2026
dfe4ff3
docs(k3s): add P0 opt-in owner proof - render.py
kvnloo Oct 28, 2026
c7eeb47
docs(k3s): add P0 opt-in owner proof - test_contract.py
kvnloo Oct 28, 2026
391e44f
docs(k3s): add P0 opt-in owner proof - k3s-p0-contract.yml
kvnloo Oct 28, 2026
9ba8194
docs(k3s): add P0 opt-in owner proof - README.md
kvnloo Oct 28, 2026
0e70b0d
fix(k3s): parse release checksum filename without awk regex ambiguity
kvnloo Oct 28, 2026
62f083f
test(k3s): add read-only restart identity evidence probe
kvnloo Oct 28, 2026
6d6ecaf
test(k3s): cover identity drift, private route and safe evidence snap…
kvnloo Oct 28, 2026
6a89e7c
docs(k3s): add private, read-only before/after restart acceptance
kvnloo Oct 28, 2026
87b91fe
docs(k3s): separate acceptance checklist step
kvnloo Oct 28, 2026
f435345
fix(k3s): require observed replacement Pod in restart identity proof
kvnloo Oct 28, 2026
79eead5
test(k3s): reject stale rollout evidence and Pod readiness drift
kvnloo Oct 28, 2026
8da283b
docs(k3s): require distinct Ready Pod UID for restart acceptance
kvnloo Oct 28, 2026
4cbc645
test(k3s-p0): verify actual Pod PVC and private Service/Ingress wiring
kvnloo Oct 28, 2026
8514ad8
test(k3s-p0): prove live owner Service endpoints and rollout freshness
kvnloo Oct 28, 2026
8b2d429
test(k3s-p0): reject unverified ingress and additive policy bypass
kvnloo Oct 28, 2026
9e065fb
test(k3s): verify private HTTPS descriptor matches persisted P0 identity
kvnloo Oct 28, 2026
ce1e860
test(k3s): cover HTTPS identity, TLS, redirect and private baseline f…
kvnloo Oct 28, 2026
2727b81
docs(k3s): add human-only HTTPS descriptor identity acceptance
kvnloo Oct 28, 2026
6cb5054
docs(k3s): use copy-pasteable one-line descriptor probe command
kvnloo Oct 28, 2026
13651d6
test(k3s-p0): add read-only disposable-cluster preflight
kvnloo Oct 28, 2026
a92338e
test(k3s-p0): cover preflight fail-closed scenarios
kvnloo Oct 28, 2026
8a65a31
docs(k3s-p0): gate manual apply on read-only preflight
kvnloo Oct 28, 2026
78c8d6a
test(k3s-p0): reject non-K3s or malformed cluster versions
kvnloo Oct 29, 2026
533dbee
fix(k3s-p0): require genuine stable K3s server preflight
kvnloo Oct 29, 2026
5bbb91b
docs(k3s-p0): state K3s-only preflight contract
kvnloo Oct 29, 2026
44f61c1
fix(k3s-p0): reject secondary and annotated owner exposure routes
kvnloo Oct 30, 2026
a6fa8e2
test(k3s-p0): cover hidden namespace routes and exposure annotations
kvnloo Oct 30, 2026
a22421d
refactor(k3s-p0): keep route fixture metadata explicit
kvnloo Oct 30, 2026
564bd28
fix(k3s-p0): fail closed on malformed route metadata
kvnloo Oct 30, 2026
566ede8
docs(k3s-p0): record exclusive route checks and snapshot limits
kvnloo Oct 30, 2026
09e7f4d
test(k3s-p0): cover malformed route inventories and exposure flags
kvnloo Oct 30, 2026
d39155d
fix(k3s-p0): fail closed on admitted Pod confinement drift
kvnloo Oct 30, 2026
cb59726
test(k3s-p0): exercise admitted Pod privilege and volume drift
kvnloo Oct 30, 2026
0786424
fix(k3s-p0): reject malformed Pod security and volume objects
kvnloo Oct 30, 2026
1725a9c
test(k3s-p0): malformed admitted Pod objects fail closed
kvnloo Oct 30, 2026
6dfa0e3
docs(k3s-p0): qualify admitted Pod confinement checks
kvnloo Oct 30, 2026
bdc4379
fix(k3s-p0): verify PV binding for restart identity receipt
kvnloo Oct 30, 2026
404eab8
fix(k3s-p0): verify PV binding for restart identity receipt
kvnloo Oct 30, 2026
185b2f7
fix(k3s-p0): require PV evidence in private descriptor baseline
kvnloo Oct 30, 2026
fad0266
fix(k3s-p0): require PV evidence in private descriptor baseline
kvnloo Oct 30, 2026
6af4fae
docs(k3s-p0): explain PV claim and reclaim proof
kvnloo Oct 30, 2026
a647cc7
test(k3s-p0): verify admitted hostname and resource limits
kvnloo Oct 30, 2026
9a2e625
fix(k3s-p0): reject malformed preflight API structures safely
kvnloo Oct 30, 2026
d56c49e
fix(k3s-p0): reject admitted container identity and lifecycle overrides
kvnloo Oct 30, 2026
6915ce2
fix(k3s-p0): close stdlib HTTPS descriptor connections
kvnloo Oct 30, 2026
b39b6ff
fix(k3s-p0): validate private bounded restart evidence before comparison
kvnloo Oct 30, 2026
5158a70
test(k3s-p0): reject unsafe and malformed restart baseline files
kvnloo Oct 30, 2026
e11d6de
fix(k3s-p0): reject FIFO descriptor baselines without blocking
kvnloo Oct 30, 2026
0bcc804
fix(k3s-p0): require observed 10Gi PVC and PV capacity
kvnloo Oct 30, 2026
c05bae5
fix(k3s-p0): reject non-object Kubernetes GET evidence
kvnloo Oct 30, 2026
497e43e
test(k3s-p0): reject list, null and scalar named API responses
kvnloo Oct 30, 2026
a495093
fix(k3s-p0): verify live quota and service account before identity ac…
kvnloo Oct 30, 2026
09e7aa8
test(k3s-p0): cover quota scope and token drift rejection
kvnloo Oct 30, 2026
0565602
docs(k3s-p0): qualify live namespace guardrail inspection
kvnloo Oct 30, 2026
838cda8
fix(k3s-p0): publish rendered manifest without overwriting files
kvnloo Oct 30, 2026
46d86d7
fix(k3s-p0): reject invalid digest-pinned image repository syntax
kvnloo Oct 30, 2026
162bc62
test(k3s-p0): reject malformed registry and repository digests
kvnloo Oct 30, 2026
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
26 changes: 26 additions & 0 deletions .github/workflows/k3s-p0-contract.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
name: K3s P0 offline contract
on:
pull_request:
paths:
- "examples/k3s-p0/**"
- ".github/workflows/k3s-p0-contract.yml"
push:
branches-ignore: [main]
paths:
- "examples/k3s-p0/**"
- ".github/workflows/k3s-p0-contract.yml"
workflow_dispatch:
jobs:
offline:
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Install YAML parser
run: python -m pip install "PyYAML==6.0.2"
- name: Validate bounded P0 manifest and digest renderer
run: python -m unittest discover -s examples/k3s-p0 -p 'test_*.py' -v
47 changes: 47 additions & 0 deletions examples/k3s-p0/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# P0 proof only: one persistent server, no worker provisioning.
# The released, self-contained CLI is verified against upstream SHA256SUMS.
# The Debian base tag is not digest-pinned; record/pin the built image digest before deployment.
FROM debian:bookworm-slim

ARG TARGETARCH
ARG T3_VERSION=0.1.3
RUN apt-get update \
&& apt-get install -y --no-install-recommends ca-certificates curl dumb-init git openssh-client tar \
&& rm -rf /var/lib/apt/lists/*

RUN set -eu; \
case "${TARGETARCH}" in \
amd64) t3_arch=x64 ;; \
arm64) t3_arch=arm64 ;; \
*) echo "Unsupported TARGETARCH: ${TARGETARCH}" >&2; exit 1 ;; \
esac; \
archive="t3-${T3_VERSION}-linux-${t3_arch}.tar.gz"; \
base="https://github.com/pingdotgg/t3code/releases/download/v${T3_VERSION}"; \
curl --fail --silent --show-error --location "${base}/SHA256SUMS" -o /tmp/SHA256SUMS; \
curl --fail --silent --show-error --location "${base}/${archive}" -o /tmp/t3.tar.gz; \
expected="$(awk -v name="${archive}" '{ n=$2; if (substr(n, 1, 1) == "*") n=substr(n, 2); if (n == name) { print $1; exit } }' /tmp/SHA256SUMS)"; \
test -n "${expected}"; \
printf '%s %s\n' "${expected}" /tmp/t3.tar.gz | sha256sum --check --status; \
mkdir -p /opt/t3; \
tar -xzf /tmp/t3.tar.gz --strip-components=1 -C /opt/t3; \
/opt/t3/t3 --version; \
rm -f /tmp/SHA256SUMS /tmp/t3.tar.gz

RUN groupadd --gid 10001 t3 \
&& useradd --uid 10001 --gid 10001 --home-dir /data --no-create-home --shell /bin/sh t3 \
&& mkdir -p /data \
&& chown 10001:10001 /data

ENV HOME=/data \
T3CODE_HOME=/data/t3 \
XDG_CACHE_HOME=/data/.cache \
XDG_CONFIG_HOME=/data/.config
USER 10001:10001
WORKDIR /data
EXPOSE 3773

# 'serve' prints headless pairing details; 'start --no-browser' does not need
# to print one-time pairing URLs into long-lived Kubernetes pod logs.
# Pair explicitly and transiently via `t3 auth pairing create`.
ENTRYPOINT ["/usr/bin/dumb-init", "--", "/opt/t3/t3"]
CMD ["start", "--no-browser", "--host", "0.0.0.0", "--port", "3773", "--base-dir", "/data/t3"]
233 changes: 233 additions & 0 deletions examples/k3s-p0/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,233 @@
# Experimental P0: persistent T3 environment on self-owned K3s

**Downstream-only, NOT deployed/tested on a live cluster.** This is one **owner**
T3 server with a stable /data PVC and private Tailscale HTTPS ingress, not a
worker broker, task scheduler, T3 Connect replacement, or permissioned peer link.
Nothing is applied automatically. Keep the user's current T3 deployment intact.

## Source and version boundary

- Reviewed upstream `pingdotgg/t3code` main at
`5211acfe95a0c6d82a60e2524b5e46b47fe67a3e` (2026-10-28). The image
instead packages the **released** self-contained CLI `v0.1.3`, published
2026-10-27. A release is not proof that every latest-main linked-environment
beta behavior exists in it. P1 must pin and validate a compatible build.
- Dockerfile checks the downloaded release archive against GitHub Release
`SHA256SUMS`, on either linux/amd64 or linux/arm64. Its Debian base tag is
**mutable**; this is not a hermetic source build. Record the final pushed
**OCI image digest**, and deploy by digest, never `:latest`.
- The base image supplies T3 + Git/SSH, but **no provider CLI or login**.
Provider installation, writable homedir needs, browser libraries and actual
workload execution must be evaluated separately. Do not bake provider secrets.
- The existing independent [kind -> K3s CPU Job pilot](https://github.com/kvnloo/oss-factory/issues/127)
/ [draft #128](https://github.com/kvnloo/oss-factory/pull/128) tests bounded
Kubernetes Job rendering, not T3 server identity or delegation; reuse its
offline lessons, not its scheduler.

## Prerequisites

A disposable/opt-in **single-node** K3s context, working default StorageClass,
Tailscale Kubernetes Operator with `tailscale` IngressClass and proxy namespace
`tailscale`, MagicDNS + HTTPS certs, and correctly restricted tailnet ACLs/grants
for this workload. No Tailscale Funnel. Verify the operator's actual proxy
namespace and tailnet policy before use; this example assumes the default name.
K3s must enforce NetworkPolicy for the ingress allowlist to work. Pods do
**not** provide VM-grade isolation for untrusted code; the manifest leaves
**egress unrestricted** for the owner process. Use a dedicated cluster/node
for the first experiment, not existing household infrastructure.

## Read-only cluster preflight (before any apply)

On an **owner-approved disposable test context**, check basic API prerequisites
without creating, deleting or restarting anything:

```sh
python3 examples/k3s-p0/preflight.py --context YOUR_DISPOSABLE_K3S_CONTEXT
```

The probe uses explicit-context `kubectl get` requests only. It requires
a stable K3s build on Kubernetes 1.28+ (`/version.gitVersion` must end in
`+k3sN`; generic Kubernetes distributions are rejected), exactly one
Ready/un-tainted/schedulable node, an **absent**
`t3-k3s-p0` namespace (protecting existing state), the `tailscale` namespace
and IngressClass controller `tailscale.com/ts-ingress`, and exactly one named
default StorageClass. Missing RBAC or API data fails closed; errors never copy
kubectl stderr that could expose private endpoints. No credentials are exported.

**This cannot prove a cluster is disposable or yours.** It also does not test
whether the Tailscale operator is healthy, certificates are issued, the
selected StorageClass can actually bind a PVC, NetworkPolicy is enforced by the
CNI, or tailnet ACLs/route identity are correct. Verify those separately, and
review the selected kubeconfig context and physical host before approving any
live apply. A PASS means *prerequisite metadata only*, not P0 acceptance.

## Offline proof (safe)

```sh
python3 -m pip install 'PyYAML==6.0.2'
python3 -m unittest discover -s examples/k3s-p0 -p 'test_*.py' -v
docker buildx build --platform linux/amd64 -f examples/k3s-p0/Dockerfile \
-t ghcr.io/REPLACE_OWNER/t3-k3s-p0:0.1.3 --load .
```

For a real test, push to a registry you control, find its pushed **manifest**
digest (not the local image ID), and render a copy. The renderer rejects tags,
missing digests, YAML injection and direct modification of tracked source.
It stages a private mode-0600 file, then publishes it without overwriting an
existing path or following an output symlink. Use a fresh output path on reruns:

```sh
python3 examples/k3s-p0/render.py \
--image ghcr.io/YOUR_OWNER/t3-k3s-p0@sha256:YOUR_64_LOWERCASE_HEX_DIGEST \
--output /tmp/t3-k3s-p0.yaml
kubectl --context YOUR_DISPOSABLE_K3S_CONTEXT apply --dry-run=client -f /tmp/t3-k3s-p0.yaml
kubectl --context YOUR_DISPOSABLE_K3S_CONTEXT diff -f /tmp/t3-k3s-p0.yaml
```

**Inspect diff and context before a deliberate live apply.** Never point these
commands at an existing T3 state/PVC, and never reuse credentials from the
home machine. The checked-in YAML intentionally has an unpullable image token.

## Human-only deployment and acceptance

1. Confirm cluster context/namespace do not already contain any T3 resources.
Apply the rendered YAML once: `kubectl --context YOUR_DISPOSABLE_K3S_CONTEXT
apply -f /tmp/t3-k3s-p0.yaml`. Check pod readiness, warnings, storage
events and Tailscale operator ingress address. A TCP probe only proves a
listening socket; it does not prove T3 auth, UI or provider readiness.
2. From a trusted admin shell, generate a **short-lived** pairing URL *on
demand*, without sending it to GitHub or storing it in pod logs:
`kubectl --context YOUR_DISPOSABLE_K3S_CONTEXT -n t3-k3s-p0 exec
deployment/t3-owner -- /opt/t3/t3 auth pairing create --base-dir /data/t3
--base-url https://ACTUAL_INGRESS_FQDN --ttl 5m`.
Pair a web/desktop/mobile client reachable via the same tailnet. Verify
the server descriptor and its environment ID. Pairing a client does **not**
create an OAuth peer link; linked environments beta is separately consented.
3. Before/after restart, compare
`/data/t3/userdata/environment-id` (do not post its value publicly), T3
project/thread data, client connection and the tailnet HTTPS endpoint.
Restart with `kubectl --context YOUR_DISPOSABLE_K3S_CONTEXT -n t3-k3s-p0
rollout restart deployment/t3-owner`. Confirm the identity **and** data are
unchanged. The fixed pod hostname mitigates `os.hostname()` drift in
usage fingerprints (see [upstream #11849](https://github.com/pingdotgg/t3code/issues/11849)),
but is not itself proof of identity persistence.

For **read-only, reproducible identity evidence**, capture a local baseline
after the first verified Ready deployment, then verify it **after** the
human-triggered restart above (never point at the existing home cluster):

```sh
python3 examples/k3s-p0/acceptance.py --context YOUR_DISPOSABLE_K3S_CONTEXT \
--write-baseline /tmp/t3-p0-identity.json
# Human explicitly performs rollout restart; wait for Ready before continuing.
python3 examples/k3s-p0/acceptance.py --context YOUR_DISPOSABLE_K3S_CONTEXT \
--check-baseline /tmp/t3-p0-identity.json
```

From an **authorized tailnet client**, independently check that the
**actual HTTPS endpoint** answers as the same persisted environment. Use the
private v3 baseline created above, before **and again after** the human
restart. This second script sends only a credential-free GET to the public
T3 descriptor, `/.well-known/t3/environment`, with a fresh HTTPS connection,
OS-trusted certificate and hostname verification. It compares the returned
ID as a hash and prints no ID or pairing token. It does **not** use an
insecure TLS bypass, redirects, proxy settings or a bearer token.

```sh
python3 examples/k3s-p0/descriptor_probe.py --baseline /tmp/t3-p0-identity.json
```

A PASS adds **remote HTTPS/T3 descriptor identity** evidence, not tailnet
ACL or CNI enforcement, client pairing/auth, UI functionality, project data
restoration or provider readiness. Perform **negative ACL tests from an
unauthorized identity** and verify a real authenticated web/mobile client
and previously saved thread separately. DNS/TLS failure must block P0;
do not work around it with `curl -k` or a public proxy. Run this only on
a disposable owner-approved cluster and never share the private baseline.

The probe uses only read-only `kubectl get` and `kubectl exec ... cat`
requests. It also verifies actual Pod image/labels, PVC mount, no API-token
mount or hostPath, Service selector/port, private Ingress backend/TLS,
and live EndpointSlice target Pod UID, IP, readiness and T3 port 3773.
It additionally rejects an *admitted Pod* whose service account, host
process/network namespaces, stable hostname/host aliases, CPU/memory/ephemeral
requests and limits, non-root/seccomp settings, container privilege,
commands/environments, or exact PVC/tmp mounts differ from the reviewed
fixture. A safe Deployment template alone cannot attest the effective Pod
after admission. It also verifies the P0 ResourceQuota's exact hard
limits, reconciled status and absence of scopes, plus the ServiceAccount's
disabled token automount and absence of secret references. These API checks
do **not** prove Pod Security Admission or quota/NetworkPolicy enforcement;
test those on the disposable cluster.
It checks the **exact requested** `t3-k3s-p0.<tailnet>.ts.net` Ingress
hostname and rejects additional reported ingress addresses. It checks
**all namespace Services and Ingresses** against the one approved owner
route, rejecting hidden secondary/public routes, ClusterIP
`tailscale.com/expose` annotations, unexpected controller annotations and
external Service load-balancer status. This strict inventory can reject
operator-added routes until a human reviews them; a snapshot cannot rule
out resources added after the read. It also verifies that
**the namespace has exactly one unchanged, narrow ingress NetworkPolicy**.
Kubernetes policies are additive, so a second broad allow would invalidate
the isolation claim. This is intentionally strict for this isolated fixture:
additional operator-injected policies require review instead of a green
acceptance result. The check needs permission to **list NetworkPolicies**,
**get ResourceQuotas/ServiceAccounts**, and `pods/exec` permission to read the environment ID; it is not a
read-only-RBAC-only Kubernetes probe. It also rejects an
unobserved/incomplete Deployment rollout. This closes
false-green checks where an unrelated/stale Pod or Service looks Ready
without actually selecting the intended T3 data. The API evidence is
**not** a dataplane/TLS/ACL or CNI enforcement probe; test those separately.
A matching Ingress name does not prove its DNS resolves to the expected
private tailnet destination: independently resolve the hostname, test
HTTPS certificate/identity from an approved client, and confirm denied
access from an unauthorized tailnet identity and unrelated Pod namespace. It requires
**one Running/Ready Pod with a new Pod UID** after the
human-triggered rollout, while checking unchanged hashed environment ID,
PVC UID, bound PV UID and reclaim policy, ingress hostname and pinned image
digest. It checks the PV's claimRef UID, ReadWriteOnce Filesystem mode,
10Gi request, 10Gi reported PVC/PV capacity and matching storage class;
missing/undersized backing capacity or an orphaned/rebound PV fails closed.
A PV with a `Delete` reclaim policy can still erase data if its claim
is deleted. The probe does not change reclaim behavior or prove on-disk
durability; it needs cluster-scoped permission to `get persistentvolumes`.
Merely running the probe twice without restarting must fail. Each baseline
is format v3; discard offline v1/v2 baselines and collect a new sample. It never
prints the raw ID, grants access, restarts a server or applies/deletes
resources. Keep the private identity fingerprint out of Git. A PASS is **not** proof of
working HTTPS, ACL enforcement, client pairing, saved threads, app readiness
or provider execution; perform the manual checks separately.

4. Verify pod/container image IDs, CPU/memory/ephemeral limits, absence of
mounted Kubernetes tokens, tailnet policy denial from an unauthorized device,
and **negative ingress test from a non-Tailscale namespace**. Inspect
`kubectl logs` for inadvertent pairing tokens *privately*. Reject this
deployment if tokens are logged. Record K3s version, CNI, storage class,
image digest, T3 version, latency, restarts and errors.
5. Stop without deleting state: `kubectl --context YOUR_DISPOSABLE_K3S_CONTEXT
-n t3-k3s-p0 scale deployment/t3-owner --replicas=0`.
To remove the public-looking endpoint independently, delete the private
`Ingress/t3-owner` in this namespace. **Do not** use
`kubectl delete -f /tmp/t3-k3s-p0.yaml` or delete the namespace as a
cleanup shortcut: this includes the PVC, and K3s local-path reclaim can
permanently delete the data. Retain/archive/delete PVC only after a
deliberate data-retention decision.

## Gated next stage

P1 will add an **external opt-in** broker that allocates separate worker
namespaces/PVCs using an idempotent lease, without changing T3 core or touching
the P0 owner. A client environment entry is not a peer authorization grant.
Require explicit `t3 environment link` OAuth consent, stable descriptor
verification, a pinned repository revision and one observed remote
`delegate_task` receipt. Upstream [#17051](https://github.com/pingdotgg/t3code/issues/17051)
still describes the record-before-launch gap: don't claim exactly-once
delegation until owner restart/lost-response tests prove it. Watch upstream
[#26475](https://github.com/pingdotgg/t3code/pull/26475) rather than using
plain `100.x` HTTP as a trust shortcut. No automatic worker enrollment into
client load balancing or shared worktrees.

**Verification status:** offline contract tests and image/K3s/tailnet/client
acceptance must be separately recorded on [downstream issue #45](https://github.com/kvnloo/t3code/issues/45).
No live K3s, Docker build, provider, device, pairing or delegation result is
implied by this example.
Loading