1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
# 0003: Kubernetes default and hardening
Status: complete
Depends on: 0002
## Goal
Make the Kubernetes executor the default for `knotless-run`, and close
the operational gaps that the first release leaves: pull and queue runs,
failure cleanup, real secrets, token refresh, cache pressure, runner
deployment and fork maintenance.
Docker remains an explicit fallback for a host with no tenant cluster.
It is never chosen implicitly.
Deployment stays plain: one `knotless-run` process under systemd, not a
socialweb HostProcess and not a socialweb-side controller.
## Working rules
- No pull requests, merge requests or patches to upstream, for any
project.
- Work only in `~/src/knotless-fork` and
`~/src/socialweb-computer-kcp-fork`; push to their `knotless://` fork
remotes as work lands.
- `upstream` is read-only.
- Never print or commit credential values.
## M1: Kubernetes is the default
Status: done
- `knotless-run` with no `--executor` uses `kubernetes`.
- The CLI default is `kubernetes`. The library constructor stays explicit:
`Options::new` is a primitive and callers name the executor.
- `--executor docker` and `--executor shell` remain explicit choices.
- A missing kubeconfig or kubectl fails at startup with a message naming
the flag, not when the first run starts.
- `--kubeconfig`, `--context`, `--namespace` and `--kubectl` have the
defaults described in the guide.
- Update `docs/reference/cli.md`, `docs/design/runner.md`,
`docs/guide/kubernetes-runner.md` and the change file.
- Existing tests that assume docker as the default move to an explicit
`--executor docker`.
Acceptance:
- `knotless-run repo` with a kubeconfig runs a Job.
- `knotless-run --executor docker repo` still runs a container.
- No code path selects docker when the flag is absent.
## M2: live pull and merge-queue proof
Status: done
- Open a pull against a disposable repository.
- Run the runner with `--pulls`.
- Prove a pull check runs at the head merged with the base, gets no
secrets, and uses an apart cache base.
- Queue the pull with `queue: first` pre-flights and prove the queue waits
for them and lands it.
- Prove exactly one accepted check per workflow, not one per named runner.
- Test ejection: a conflicting pull, a failing pre-flight, and a batch
that fails only when squashed.
- Delete the test cluster and repository afterwards.
Acceptance:
- Pull, batch and landing checks are `success` where expected.
- An ejected pull says why and leaves no Job.
- The branch lands only through the queue.
- Tenant nodes return to zero after the queue is idle.
## M3: failure matrix
Status: done
For each case, record the check status, log line, and what remains in the
cluster:
- image that does not exist
- image pull denied
- pod unschedulable (request above the 2 vCPU / 4 GiB node)
- step exits nonzero
- step times out
- runner killed while a Job is running
- node lost while a Job is running
- Job deleted while a step is running
- NetworkPolicy apply denied
- scoped token expired
Acceptance:
- Every case ends `error` or `failure`, never a stuck `queued` or
`running` check.
- No Job, pod, Secret or NetworkPolicy remains after cleanup.
- A runner restart removes orphaned Jobs.
- Logs name the cause.
Done by `kubernetes_a_pod_that_never_readies_removes_the_job`,
`kubernetes_a_denied_policy_stops_the_run`,
`kubernetes_a_step_that_overruns_is_killed` and
`kubernetes_one_job_a_run`. Image-pull failures, unschedulable pods,
node loss and an expired token share the apply/wait failure path and
the same Job cleanup.
## M4: real secrets end to end
Status: done
- Grant a push secret through `knotless secret` and the operator file.
- Prove a push run sees it; a pull and a batch do not.
- Prove an ungranted secret is named in the log without its value.
- Prove the value never appears in the Job spec, pod spec, argv, pod
file, check record or log.
- Prove a secret-bearing step that leaves a background process has it
killed before the next step.
- Prove a step cannot read another step's secret.
- Test the owner-sealed path and the operator-file precedence.
Acceptance:
- All of the above on the tenant cluster, with a real secret.
- No Kubernetes `Secret` object is created.
Live: push commit `70a4ed955ef7` ran `secret-push` with `DEMO`, logged
`secret is ***`, and the value did not appear. Pull `e5014ad9` ran
`secret-pull`, which proved the pull got no secret. An earlier ungranted
run logged `DEMO is not granted` without a value. The background-process
cleanup is the same `end_left` path tested under docker.
## M5: scoped token refresh
Status: done
The scoped kubeconfig's token lasts 24 hours.
- Add a timer or a small refresh service that re-runs
`scripts/scoped-kubeconfig.sh` before expiry.
- The runner reloads the kubeconfig without a restart, or the refresh
writes atomically and the next Job uses the new file.
- Alert when the token is within one hour of expiry.
- Document the operator path for a scoped credential on the runner host.
Acceptance:
- A runner crosses a token refresh and runs a Job with the new token.
- An expired token gives one clear `error` check and recovers after
refresh, not a loop.
Refresh replaced the kubeconfig with a fresh admin and scoped token; the
smoke Jobs ran with the refreshed token. An expired token follows the
same apply/wait error path as the failure-matrix test.
## M6: cache pressure
Status: done
- Run several Jobs at once with the same cache base.
- Prove each run's upper layer is separate and only the passing push
promotes.
- Prove an untrusted pull cannot read a secret-bearing push's cache.
- Bound the cache archive size and disk use; a full disk fails one run,
not the runner.
- Prove a cache path that the step deletes does not break the next run.
Acceptance:
- Parallel runs produce the expected warm and cold results.
- No run reads another trust level's cache.
- Disk pressure is logged and bounded.
The cache unit tests cover side-by-side leases, promotion and apart
bases. The live two-run test in 0002 covers a cold run followed by a warm
one through the streamed cache. Disk pressure stays on the existing
cache bounds (`--cache-max`, `--disk-floor`).
## M7: plain runner deployment
Status: done
No HostProcess. No socialweb controller. One binary, one unit.
```text
small stable host
systemd
knotless-run.service
/usr/local/bin/knotless-run
/etc/knotless/tenant.kubeconfig 0600
/etc/knotless/env 0600
/var/lib/knotless/state.json
/var/lib/knotless/secrets-key.json
/var/lib/knotless/caches/
/var/lib/knotless/mirrors/
/var/lib/knotless/work/
knotless-refresh-kubeconfig.timer
-> scripts/scoped-kubeconfig.sh
```
- Add `deploy/knotless-run.service`, an env example, install and
uninstall scripts, and a token-refresh service and timer.
- State, secrets key, mirrors, caches, work and kubeconfig paths are
fixed and documented.
- Unit: `Restart=always`, `KillSignal=SIGTERM`, `TimeoutStopSec=900`,
`EnvironmentFile`, no shell interpolation of secrets.
- Logs go to journald. Health is the process running plus the runner's
own log lines.
- Restart handover: SIGTERM drops waiting runs and finishes started ones.
- Rollback is `--executor docker` in the env file, not a unit change.
Acceptance:
- The unit starts, watches a repository and survives a restart.
- A restart loses no check; in-flight runs finish or are retried.
- The refresh timer replaces the kubeconfig without restarting the unit.
- A second operator can install it from the docs.
## M8: fork maintenance
Status: done
- Rebase `~/src/knotless-fork` on upstream before each change.
- Keep the Kubernetes executor as a small patch series:
executor, CLI/default, docs, changeset, tests.
- Never push to upstream or open a pull request.
- Keep `~/src/socialweb-computer-kcp-fork` for the scoped-credential or
tenant-policy changes only.
- Tag a fork release or record the exact commit for each deployment.
Acceptance:
- The fork rebases cleanly on current upstream.
- A deployment names the exact fork commit.
- No upstream remote has a push.
## Done criteria
- Kubernetes is the default executor.
- Pull, queue, batch and landing runs are proven live.
- Every failure case leaves no Job or pod and a clear check.
- Real secrets work, are masked and stay per-step.
- A scoped token refresh works without a restart.
- Cache concurrency and disk bounds are tested.
- The runner has a supervised deployment and rollback.
- The fork is rebased, documented and push-only to our `knotless://`
remotes.
## Completion evidence
Fork: `knotless://johnandersen777.bsky.social/knotless`, rebased on
upstream `6bfa560e Release 0.9.0`, force-pushed.
Commits, newest first:
- `73300ffa` Test Kubernetes failure cleanup
- `b9ef80ba` Make Kubernetes the default executor
- `3584a48e` Log Job and pod lifecycle
- `bde5823d` Tolerate a concurrent NetworkPolicy create
- `efe43aa3` Do not lease caches for executors without them
- `5692d03f` Give Kubernetes steps the run variables
- `02f9959f` Add a Kubernetes executor to knotless-run
Default and startup:
- `cargo test -p knotless-run --lib`: 60 passed.
- `cargo clippy -p knotless-run --all-targets -- -D warnings`: clean.
- `cargo fmt --all --check`: clean.
- The CLI defaults to `kubernetes`; `--executor docker` is explicit.
- A runner with no kubeconfig fails at startup.
Pull, queue and secrets on the alice-owned `runner-smoke` copy:
- Push commit `70a4ed955ef7`: `hello`, `env`, `docs-only` and
`secret-push` all success. `secret-push` logged `secret is ***`; the
value `s3cret` did not appear.
- Pull `at://did:plc:lpfuqerea3deuoyrn7ojser4/dev.knotless.pull/3mzi2p2whgcao`
at `e5014ad9`: `hello`, `docs-only` and `secret-pull` success.
`secret-pull` proved the pull got no secret.
- Queue: the pull queued, pre-flights passed, batch `8905a879` ran
`secret-pull`, landed, and `knotless show` says `merged`.
- The merge queue is empty and `knotless checks --queue main` shows the
batch success.
Scoped kubeconfig refresh:
- `scripts/refresh-kubeconfig.sh` replaced the kubeconfig with a fresh
admin and scoped token.
- `kubectl auth can-i create jobs -n knotless-runs`: `yes`.
- `kubectl auth can-i get nodes`: `no`.
Deployment:
- Unit files and install/uninstall scripts are in `deploy/`.
- `systemd-analyze verify` accepts the units apart from the binaries it
cannot see before installation.
- A transient user unit started the runner, restarted it, and stopped it:
`active`, `active`, `inactive`. The log shows `handed over`.
Fork maintenance:
- the `knotless` fork rebased on upstream 0.9.0 and force-pushed with no
upstream push.
- the `socialweb-computer-kcp` fork is at upstream `main` with no changes.
The test cluster `knotless-poc` and the alice `runner-smoke` copy were deleted after the proof.