Task 6: scaffold, config, dependency guard #1

Open
chris wants to merge 19 commits from v1 into main
Owner

Scaffold for ci-dispatch (runner classes phase 0-1, Task 6). Stays on v1 until the release review.

Scaffold for ci-dispatch (runner classes phase 0-1, Task 6). Stays on v1 until the release review.
Scaffold: module, model, config, secret, CI and the dependency guard
Some checks failed
ci / image (pull_request) Has been cancelled
ci / test (pull_request) Has been cancelled
6e7363c269
Go module git.realliance.net/realliance-ops/ci-dispatch with exactly three
direct requirements (go-crypto v1.5.2, yaml v3.0.6, sqlite v1.61.0), guarded
by deps_test.go. Model routing, strict config loading and the redacting
secret package (from forge-bot 9be4857). CI follows forge-bot's.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FWhaDeswMqYvQaVdpL8oit
README: fix a sentence
All checks were successful
ci / test (pull_request) Successful in 42s
ci / image (pull_request) Has been skipped
2c59c7e961
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FWhaDeswMqYvQaVdpL8oit
Dependency guard checks the real import graph; CI verifies and tidies
Some checks failed
ci / image (pull_request) Has been cancelled
ci / test (pull_request) Has been cancelled
d0fdee2426
deps_test.go now walks go list -deps -test: own packages may import only the
approved three modules, every graph module and every go.mod requirement must
be in an explicit allowed set with reasons. CI adds go mod verify and
go mod tidy -diff. README fixes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FWhaDeswMqYvQaVdpL8oit
README: restore the dependencies paragraph
All checks were successful
ci / test (pull_request) Successful in 6s
ci / image (pull_request) Has been skipped
aab348f1db
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FWhaDeswMqYvQaVdpL8oit
verify.go, ParseFingerprints and their tests and fixture, from forge-bot 9be4857.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FWhaDeswMqYvQaVdpL8oit
limits: Owner-signed ci.yaml, ceilings in code, last-verified fallback
All checks were successful
ci / test (pull_request) Successful in 13s
ci / image (pull_request) Has been skipped
7a77052787
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FWhaDeswMqYvQaVdpL8oit
limits: bind stored config to the signed commit, refuse rollbacks, typed reload states, owner ids
All checks were successful
ci / test (pull_request) Successful in 14s
ci / image (pull_request) Has been skipped
623f4ac5c1
Review round 1 of Task 7.
- signed: tree.go proves a file belongs to a signed commit (commit tree,
  tree objects, blob); commit.go (CommitterTime, FirstParent) copied from
  forge-bot 9be4857.
- limits.Source: restore re-proves the whole chain offline and fails closed;
  adopts a head only if its signed committer time is strictly later; returns
  ErrForgeUnavailable, ErrUntrusted, ErrRollback or ErrUnknownOwner and
  exposes State and Findings.
- ci.yaml: each owner and deny entry carries name and numeric id; a name that
  resolves to another id gets the defaults, and a deny keeps the original id.
- strict whole numbers, caps on minutes and paid concurrency, canonical head sha.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FWhaDeswMqYvQaVdpL8oit
limits: enforce by numeric id, names are labels; restore hardening
All checks were successful
ci / test (pull_request) Successful in 14s
ci / image (pull_request) Has been skipped
ce6f95810e
Review round 2 of Task 7.
- Owner overrides and denies bind to the id in ci.yaml. Name lookups only
  raise findings (owner-name-stale, owner-name-missing, owner-names-unchecked)
  and never change the limits or fail the load.
- Restore applies the +2 min future-date bar, never replaces a config verified
  in this process, and counts rejections in StoreRejectedTotal (monotonic).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FWhaDeswMqYvQaVdpL8oit
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FWhaDeswMqYvQaVdpL8oit
forgejo: jobs, runners, inventory reads; two writes only
All checks were successful
ci / test (pull_request) Successful in 16s
ci / image (pull_request) Has been skipped
f3efab9bc4
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FWhaDeswMqYvQaVdpL8oit
forgejo: fix round 1: structural write check, guard tests, DeleteRunner bound to identity
All checks were successful
ci / test (pull_request) Successful in 16s
ci / image (pull_request) Has been skipped
5b761708cb
DeleteRunner reads the runner first and refuses unless it is a ci- ephemeral
instance-scope runner with the given uuid. The guard checks the registration
body, query keys and Host. Runners are read with X-Total-Count, Jobs in one
call, TreeEntries to total_count, and a missing verification is unavailable.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FWhaDeswMqYvQaVdpL8oit
kube, tiny: one namespace, seven requests; the tiny Job and its Secret
All checks were successful
ci / test (pull_request) Successful in 17s
ci / image (pull_request) Has been skipped
8dc0e2da10
internal/kube is a plain net/http client for one namespace with the
in-cluster CA and a token re-read every minute. It makes seven request
shapes only: list Jobs and Pods, create a Job and a Secret, resume a Job
(merge patch {"spec":{"suspend":false}} and nothing else), delete a Job
(Background) and delete a Secret. A transport allowlist refuses anything
else before it is sent: other namespaces, dot/slash/percent tricks,
trailing segments, queries outside the per-shape list, a foreign Host,
redirects, any GET on a Secret, other PATCH bodies, and POST bodies whose
kind, name or namespace do not match. An AST test keeps each write method
in one function behind a constant.

internal/tiny builds the suspended Job (pod shape from the plan, the
NetworkPolicy spec netpol-gate init container verbatim, activeDeadline
from HardStop) and its Secret, owned by the Job. SecretManifest returns a
type that prints as [redacted].

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FWhaDeswMqYvQaVdpL8oit
kube, tiny: fix round 1: guard Job spec, fail-closed token, secret manifest as secret.Value
All checks were successful
ci / test (pull_request) Successful in 17s
ci / image (pull_request) Has been skipped
82eebecfdc
I1: a Job POST must carry spec.suspend literal true, activeDeadlineSeconds
in 1..3600 and template.spec.automountServiceAccountToken false; duplicate
keys at the top level, in metadata, spec, template and pod spec are refused
by a token-walk decoder. POST needs Content-Type application/json and any
Impersonate-* header is refused.
I2: a due token re-read that fails or finds an empty file drops the cached
token and errors before a request is built; an empty file at New errors.
I3: SecretManifest returns secret.Value and CreateSecret takes it, revealing
only while writing the body; the Manifest byte type is gone.
M1-M6: Opaque URL case, a real QuoteMeta test, whole-object manifest
assertions, https-only PublicURL, HardStop > RunnerTimeout, Name <= 50.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014pyfmBr47fTDcLKRXsAvEE
kube, tiny, config: fix round 2: one hard-stop ceiling; raw header checks
All checks were successful
ci / test (pull_request) Successful in 18s
ci / image (pull_request) Has been skipped
46675593a8
N1: tiny.MaxHardStop (1h) is the single ceiling. config.Load refuses a
larger tiny.hard_stop with a clear message, JobManifest refuses it, and the
guard's activeDeadlineSeconds bound is derived from it. A kube test builds a
real Job and Secret with tiny and sends both through the real guard.
N2: the guard judges headers by raw key: a request with a non-canonical
header key, or with other than exactly one Content-Type value on POST and
PATCH, is refused.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014pyfmBr47fTDcLKRXsAvEE
ledger: SQLite, one active launch per key, flock, nightly snapshot
All checks were successful
ci / test (pull_request) Successful in 23s
ci / image (pull_request) Has been skipped
196948708b
internal/ledger holds launches, holds, the last verified signed config
(opaque body; an empty store reads as empty sha with a nil error, as
limits.Store expects) and snapshots via VACUUM INTO, keeping seven. A
partial unique index allows one active launch per job attempt; every
write is BEGIN IMMEDIATE; temp_store=MEMORY so a read-only root with an
unwritable TMPDIR still works. The directory flock uses stdlib
syscall.Flock. No column holds a runner token and the package never logs.

Also closes the Task 9 carry: a static test that internal/tiny imports no
network package.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014pyfmBr47fTDcLKRXsAvEE
ledger: fix round 1: charging rules, concurrency tests, held tries, AllHolds
All checks were successful
ci / test (pull_request) Successful in 30s
ci / image (pull_request) Has been skipped
126d8a592a
End refuses negative minutes and caps the charge at the reservation;
Reserve refuses negative ReservedMinutes; Undo is refused from Running.
Tries skips launches undone as OutcomeHeld. Adds AllHolds and LaunchCount;
SetHold keeps since while the reason is unchanged. A failed ROLLBACK
discards the connection. New tests: concurrent Reserve/End with a barrier
per key, every active state in the index, the midnight instant, exact
column sets, Owner filter and limit, time.Local set off UTC, pragmas on
several connections. internal/tiny's no-network test walks go list -deps.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014pyfmBr47fTDcLKRXsAvEE
gate: the spec's gates in order, fair across owners
All checks were successful
ci / test (pull_request) Successful in 31s
ci / image (pull_request) Has been skipped
01ae83a43a
Plan decides each waiting job without a launch: label, class on, deny by
owner id, no-budget, launch-failing, back-off, owner concurrency, global
tiny capacity, owner minutes, ready. Owner and global budgets are both
checked, so an owner limit above the global cannot exceed it. Held undos
are not tries; a per-key back-off window (1, 2, 5, 10 minutes) is a pure
input. Adds model.ReasonBackoff.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014pyfmBr47fTDcLKRXsAvEE
gate: fix round 1: fair share across ticks, dedupe keys, fail closed on bad ids and reserve
All checks were successful
ci / test (pull_request) Successful in 31s
ci / image (pull_request) Has been skipped
c9c706d039
Owners go in order of today's tiny use, then oldest job, so a heavy owner's queue cannot hold the only slot (global 1). A key is decided once per plan. A job with an id, attempt or owner id of zero or less is held as invalid; a reserve under one minute is not-ready. More precedence, clamp, fairness and back-off tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014pyfmBr47fTDcLKRXsAvEE
loop: reconcile, launch with undo, reap, settle; restart-safe
All checks were successful
ci / test (pull_request) Successful in 38s
ci / image (pull_request) Has been skipped
c234860037
internal/loop is ci-dispatch's control loop (runner classes Task 12).

- Tick reads the ci-tiny Jobs and Pods, the active launches, and the job
  listing. A failed Kubernetes or ledger read skips everything; a failed
  job listing skips only the gate, the launches, and the holds. Expiry
  reaping needs no Forgejo. The success timestamp moves only when every
  read succeeded.
- Launches follow D17: reserve, register, record, Job (suspended), Secret,
  resume, record created. A failed step undoes the earlier ones. The Job
  goes before its Secret, and when the Job delete fails the launch stays
  whole for the reconcile, so a resumed Job never loses its credential.
- Reconcile (Start, and every Tick) finds crash leftovers by their derived
  name: it undoes a reserved or suspended launch and adopts a resumed one.
  A row stays active until its compute delete succeeds.
- Reaper: pickup timeout (idle runner = Forgejo hold: undone as held, no
  charge, no try, back-off 1/2/5/10 min; otherwise reaped and charged),
  gone before pickup, expired, orphan Jobs and ci- ephemeral instance
  runners after 10 min. ErrNotOurRunner is a refusal: logged at error,
  kept in the orphan gauge, never retried.
- Settle: max(1, ceil(minutes since compute)), capped by the ledger, from
  Forgejo state and Job presence (ttlSecondsAfterFinished 0), never from
  JobStatus counts.
- Sweep: the stored config read gates the first Reload and Ready (R1);
  orphan runners; the nightly snapshot from 03:00 UTC (MkdirAll first).
- Healthy is a wedge detector; Run never overlaps steps and lets the step
  in flight finish on shutdown.

internal/metrics (moved here from Task 13): hand-written exposition, every
label from a fixed set, every series present from the start.

internal/ledger gains Reap (state reaped, charge capped), and UndoHeld
and Held, which time the back-off.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014pyfmBr47fTDcLKRXsAvEE
All checks were successful
ci / test (pull_request) Successful in 38s
ci / image (pull_request) Has been skipped
This pull request can be merged automatically.
You are not authorized to merge this pull request.
View command line instructions

Checkout

From your project repository, check out a new branch and test the changes.
git fetch -u origin v1:v1
git switch v1
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
realliance-ops/ci-dispatch!1
No description provided.