Files
truf-server/openspec/changes/add-web-operations-control-plane/HANDOFF.md
T
2026-09-30 20:30:56 +03:00

1550 lines
84 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Implementation Handoff
Updated: 2026-09-21
This file preserves implementation context for the active OpenSpec change
`add-web-operations-control-plane`. It is a working handoff, not a normative
specification. The OpenSpec artifacts and `tasks.md` remain authoritative.
## User Direction
- Continue implementation autonomously and pragmatically.
- Avoid overengineering. Ask through the question tool only when a real product
or architecture choice is unclear.
- Keep changes minimal and scoped to the current OpenSpec task.
- Do not modify or remove unrelated worktree changes.
- Do not touch the production server unless explicitly requested. The only
previously approved remote host was `sec`; `prod` was explicitly excluded.
## Non-Negotiable Provider Architecture
These decisions are also recorded in the repository `AGENTS.md`.
- The server validates assignment shape and canonical immutable identity.
- Git planning may bind an exact commit.
- Docker planning may resolve a mutable tag to an immutable digest.
- These planning operations are not provider-access proofs.
- The worker is the final authority for real provider access.
- Discovery credentials do not enter direct assignment fields.
- Do not add server-side provider probes, durable public-access proof state,
proof TTL/freshness migrations, broad worker environment scrubbing,
credential sandboxes, or post-hoc redaction pipelines without explicit user
approval and a new OpenSpec requirement/task.
- Worker ambient HOME/XDG/Git/Docker/provider environment belongs to the worker
operator.
- Permanent provider outcomes include target-scoped auth/access/not-found.
- Retryable provider outcomes include network/rate-limit/provider 5xx failures.
## OpenSpec Progress
- Change: `add-web-operations-control-plane`
- Schema: `spec-driven`
- Completed through task 9.4.
- Current count: 50/58 complete.
- Current task: 9.5 (fixed host installation and production wiring).
## Major Completed Work
### Operations authority and distributed sources
- Added PostgreSQL/SQLite operation-control, operation, and append-only audit
authority in migration 29.
- Discovery and dispatch pause gates and drain reconciliation are transactional.
- Server producers are discovery-only for GitLab, DockerHub, and HuggingFace.
- Protocol 2 and package schema 3 support exact GitLab, Docker direct, and
HuggingFace Space assignment capabilities.
- New assignment claims are protocol 2; protocol 1 remains completion-only.
### Removed rejected infrastructure
- Docker anonymous-access proof/preflight machinery was removed.
- Proof columns, claim gates, resolver proof handling, and migration 30 were
removed; migration count remains 29.
- Broad Docker/HuggingFace child-environment sanitization was removed.
- Docker tag-to-digest immutable planning remains.
### Worker/package and canary verification
- Windows/Linux packaged-worker E2E covers real GitLab scan/recovery plus
synthetic DockerHub/HuggingFace protocol-2 claim-to-ingestion flows.
- Bounded test-only DockerHub canary covers discovery, enqueue, claim,
worker-classified permanent/retryable failures, upload, ingestion,
projection, replay, expiry, and drain.
- Synthetic test transport is test-only. The live public DockerHub canary is
task 10.5.
### Runtime documents
- Strict bounded UTF-8 YAML loader rejects duplicate keys, including effective
merge collisions, and exposes content-free errors.
- Combined config/secrets validator enforces exact types, bounds, core profile,
auth pools/references, package capability evidence, and deployment paths.
- Startup, Supervisor launch, preview, and secrets import use the shared
validator without replacing existing lifecycle/path security checks.
- Candidate files use fixed paths:
- `/data/runtime-document-candidates/config.yaml`
- `/data/runtime-document-candidates/secrets.yaml`
- `/data/runtime-document-candidates/candidate.lock`
- Candidate storage uses private descriptor-based reads, raw SHA-256 CAS,
durable atomic replacement, bounded config diffs, and aggregate-only secrets
diffs.
- Candidate concurrency/error-redaction tests cover stale active/candidate
revisions, hardlinks/symlinks, interrupted writes, rollback, lock contention,
cancellation locals, and orphan temporary cleanup.
### Supervisor and operation services
- Control protocol stays schema 1 for compatibility.
- Legacy textual snapshot/command remains for CLI and health compatibility.
- Web-facing paths use only exact typed actions; they never call the generic
parser or shell.
- Structured runtime/source snapshot covers every configured managed child.
- Typed lifecycle/settings support all practical Supervisor-managed children,
including dashboard through its separate exact action.
- Bounded log tail accepts only an exact managed-source ID and line count.
- Durable operation methods support apply/restart, managed-source actions,
worker-admin mutations, and content-free hash-chained audits.
### Web console through task 7.5
- Caddy strips inbound private admin headers and injects authenticated Basic
username plus private edge marker.
- Backend trusts operator identity only with the private marker and stores it on
request-local state.
- Shared navigation includes Workers / Dispatch, Overview, Search, Supervisor,
Logs, Config, and Secrets as pages are implemented.
- Overview uses independently degradable bounded health components.
- Search provides persistent discovery pause/resume and exact producer controls.
- Workers / Dispatch provides dispatch pause/resume, drain controls/progress,
package compatibility, users, devices, assignments, and deferred requeue.
- Supervisor page exposes structured state and exact typed controls for all
managed children. Logs page exposes bounded allowlisted tails only.
- Every mutation implemented so far receives trusted actor attribution and a
durable accepted/terminal audit operation.
## Important Web Decisions
- Admin pages are server-rendered and contain no application JavaScript.
- Every response remains `no-store` with restrictive security headers.
- Paths are relative so the random public Caddy admin prefix is preserved.
- Actor is the raw validated Basic-auth username; no actor form field exists.
- Exact form shapes reject missing, extra, and duplicate fields.
- POST mutations use canonical operation UUIDs embedded by the server.
- Plaintext device tokens appear only once in their direct POST response.
- Config/secrets content must never enter generic file-service scope.
- Worker status/upload/report/receipt endpoints remain independent of admin
page health.
## Completed Task 7.6
Task text:
> Add config and plaintext secrets preview/save/apply pages with no-store
> rendering, revision/hash conflicts, and no value leakage outside the editor.
### Implemented UI and service behavior
- GET `/admin-internal/config`
- GET `/admin-internal/secrets`
- POST `/{config|secrets}/preview`
- POST `/{config|secrets}/save`
- POST `/{config|secrets}/apply`
- POST `/runtime/apply-both`
- Editors load the candidate when present, otherwise the active document.
- Config editor never receives plaintext secrets.
- Secrets plaintext appears only inside the escaped no-store textarea.
- Preview uses the shared validator and candidate pairing rules.
- Config diff now redacts every changed string value.
- Secrets diff is aggregate-only and contains no pool, entry, username, token,
hash fragment, or secret length.
- Save stages only a candidate and never activates an active file.
- Production currently has no host-agent apply provider. Apply returns 503
before operation creation. Host apply belongs to tasks 9.x.
- Save operations are durable and content-free:
- `runtime.config.save`
- `runtime.secrets.save`
- Accepted/terminal audit records contain only document name, hashes, byte
counts, written flag, trusted actor, and fixed outcome/category.
### Active files
- `app/admin_api.py`
- `app/runtime_document_io.py`
- `app/runtime_document.py`
- `app/scanner_db.py`
- `app/worker_api.py`
- `tests/test_admin_api.py`
- `tests/test_runtime_document_io.py`
- `tests/test_operations_service.py`
### Completed replay and cancellation-safety fixes
#### 1. Save replay after the candidate changed length
Resolved problem:
- `_save_runtime_document_candidate_bytes()` previews the current candidate and
recomputes `candidate_before_bytes` before asking the database to replay the
existing operation.
- After a successful candidate write, current byte length may differ from the
originally accepted before length.
- Reusing the same operation UUID then conflicts even though it is the exact
request replay.
Implemented fix:
- Look up `runtime_operation(operation_id)` before creating a new document
operation.
- If it exists, validate actor, action, target kind/ref, submitted active
hashes, selected candidate-before hash, proposed candidate-after raw hash and
byte count against persisted expected identity.
- Persist and validate the counterpart candidate hash as part of the document
operation expected identity. The accepted operation must bind all four CAS
hashes, not only the selected candidate.
- For a running replay:
- active config/secrets and counterpart candidate must still match accepted
hashes;
- if selected candidate still has the accepted before hash, retry the save;
- if selected candidate already has the accepted after hash, complete the
operation without writing again;
- any other state is a 409 revision conflict.
- Do not recompute accepted before-byte count from post-save state.
- Extend the real ScannerDB operation test and make the admin fake compare the
complete immutable identity, so changed-length replay is covered.
#### 2. Preserve recovery identity on save errors
Resolved problem:
- A 503 `runtime document completion is pending` page renders pre-save state and
generates a fresh operation UUID.
- Pressing Save from that page starts another operation instead of replaying the
pending one.
Implemented fix:
- `_render_runtime_document_page()` should accept an optional operation UUID.
- Preview/error responses must keep the submitted operation UUID.
- After save failure/pending completion, reload editor state before rendering so
hashes represent the physical candidate now on disk.
- The textarea may retain the submitted document only inside the editor.
#### 3. Apply replay after host-side file changes
Resolved problem:
- `request_runtime_apply()` verifies current candidate/active filesystem state
before checking for an already persisted operation.
- If the host agent changed active files and its response was lost, an exact
POST retry can fail revision verification instead of recognizing the durable
operation.
Implemented fix:
- Check `runtime_operation(operation_id)` first.
- Validate actor, action, target kind/ref and persisted expected hash identity
against the submitted request.
- Terminal success returns without filesystem verification.
- Terminal failure returns 409.
- Requested/running exact replay redispatches the same operation ID/action
without re-verifying post-apply filesystem state.
- A fresh operation still performs full candidate verification before durable
operation creation and dispatch.
- Provider dispatch is expected to be idempotent by operation ID; task 9.x host
agent will enforce persisted-operation/hash checks.
#### 4. Cancellation traceback plaintext cleanup
Resolved problem:
- `asyncio.CancelledError` is a `BaseException` path and can leave raw
URL-encoded body, decoded fields, or `document_text` in traceback locals.
Implemented fix:
- Add `try/finally` cleanup in `_form_fields()` for body, decoded text, parsed
pairs, field mapping, and current chunk locals.
- Wrap the document branch of `_dispatch()` in `try/finally` and clear fields,
hashes, editor, preview, operation ID, and document text locals.
- Add a cancellation test that injects cancellation after decoding and asserts
the sentinel is absent from all `admin_api` traceback frame locals.
## Task 7.6 Verification
- PyCompile passed for all changed production and test modules.
- Focused runtime-document/admin/operation suites: `96 passed, 1 skipped`.
- Broader admin/runtime/operation/worker/edge/query suites: `179 passed, 1 skipped`.
- Container unit selection: 37 modules, 810 test definitions, no runner skips.
- Desktop/mobile Chrome checks passed for Config and Secrets: no body overflow,
bounded scrollable textarea, prefix-safe relative navigation, no script,
local storage or service worker use, and no console errors.
- The secrets sentinel occurred exactly once in serialized HTML and nowhere
outside the textarea.
- Strict OpenSpec validation passed.
- `git diff --check` passed. The checkout still has no useful tracked baseline.
- Live PostgreSQL was unavailable locally; SQLite service tests and SQL-shape
checks remain the current database coverage.
## Completed Task 7.7
Task 7.7 added durable operation-status and bounded paginated audit pages that
remain backed by database state across runtime/admin restarts.
### Implementation
- `ScannerDB.runtime_audit_events(before_event_id=None, limit=50)` returns a
deterministic newest-first keyset page using event ID as the cursor.
- Cursor and page limits are strictly bounded. The query fetches at most one
extra row to determine whether an older page exists.
- The audit read uses a SQLite transaction or PostgreSQL repeatable-read,
read-only transaction and loads referenced operations in one bounded batch.
- Only operation-bound audit rows are exposed. Nullable unlinked storage events
are excluded because they have no typed operation identity safe for the web
console.
- Every returned event is revalidated against its normalized durable operation:
actor, action, target, result/category, canonical expected/resulting identity,
byte counts, parent hash metadata, timestamp, and event hash.
- Added explicit no-store server-rendered routes:
- `/admin-internal/operations`
- `/admin-internal/operations/{canonical-operation-uuid}`
- `/admin-internal/audit`
- `/admin-internal/audit?before={positive-event-id}`
- Unknown or noncanonical operation IDs return 404 without filesystem or agent
probing. Duplicate, extra, or malformed query fields fail closed with 400.
- Operation status renders only normalized persisted safe fields and canonical
expected/resulting identities.
- Audit renders only safe operation-bound event fields and hash identities.
- Apply POST redirects to the durable operation URL after successful dispatch.
- Relative URL roots preserve the random external admin prefix: list/audit pages
use `.`, operation detail uses `..`, and apply redirects use
`../operations/{operation_id}`.
- Operations and Audit were added to shared navigation.
### Task 7.7 tests and review
- Admin tests cover operation list/detail, audit pagination, random-prefix URL
resolution, strict query shapes, unsupported methods, security headers,
escaping, and absence of editor/Supervisor fixture secrets.
- SQLite service tests cover pagination without overlap, cursor exhaustion,
operation completion identities, and close/reopen persistence.
- PostgreSQL integration coverage was added for pagination across ScannerDB
reopen and rejection of audit UPDATE, DELETE, and TRUNCATE. All 59 bundled
PostgreSQL tests were discovered but skipped locally because PostgreSQL is
unavailable.
- An independent review found and then verified fixes for relative URL escape,
nullable unlinked audit rows, and missing PostgreSQL coverage. Its final
result reported no concrete findings.
- Focused admin/operation suites passed: 54 tests.
- Broader selected admin, runtime-document, operation/control/schema, worker API,
edge deployment, and production-query checks passed; the unittest modules
reported 183 tests with one expected Windows skip.
- Container unit selection passed: 37 modules, 814 test definitions, no runner
skips.
- Desktop and 390px mobile browser checks passed for operation list, operation
detail, and audit pages. The document had no horizontal overflow; wide tables
scroll inside their bounded container. All links remained under the admin
prefix. No scripts, local storage, service-worker controller, fixture-secret
leakage, console warnings, or console errors were present.
- Strict OpenSpec validation and `git diff --check` passed.
## Completed Task 7.8
Task 7.8 adds admin API and browser coverage for routes, methods, exact form
shapes, actor spoofing, Origin/CSRF, stale forms, random-prefix navigation, CSP,
and secret non-retention.
### Production fixes already implemented
- `_dispatch()` rejects query fields before any state read or mutation. Only
`GET /admin-internal/audit` accepts the exact optional `before` cursor; every
other route and method requires an empty query.
- `_AdminRoute(Route)` upgrades Starlette method-only partial matches and calls
the admin endpoint directly, so arbitrary methods such as `PROPFIND` receive
the application's secured 405 response instead of an unprotected framework
response.
- Supervisor controls were compacted without changing the typed backend:
Dashboard and every managed source use collapsed `<details>` panels. Exact
action routes, CSRF, operation UUIDs, allowlists, and no-JavaScript behavior
remain unchanged. No shell, generic command/action field, or parser was added.
### API and edge test work implemented
- `tests/edge_e2e_client.py` now submits canonical operation UUIDs for user
creation, expects the current 303 redirect, and verifies PostgreSQL operation
plus accepted/succeeded audit rows attribute the authenticated Basic user,
not spoofed inbound operator headers.
- A table-driven contract test covers all 45 exact POST routes. Each route
rejects the wrong method, incomplete fields, and attacker Origin with secure
headers and no side effects.
- All six stale control forms return secured 409 without a transition.
- Config/secrets preview, save, individual apply, and apply-both now have direct
valid-route coverage.
- Eighteen stale runtime-document hash cases cover every active, selected
candidate, and counterpart candidate hash before mutation/operation creation.
- All rendered Search and Supervisor exact actions/settings have successful
dispatch coverage; semantically invalid keychecks loop mode remains excluded.
- CSRF/Origin tests now cover missing/duplicate CSRF and duplicate/wrong Origin,
proving only the valid request mutates.
- Static random-prefix crawl covers root, overview, Search, Supervisor, Logs,
Config, Secrets, Operations, operation detail, and Audit. Every link/form
remains under the simulated private prefix; every response has strict headers
and no script/browser-storage API references.
- The fixture secret occurs exactly once inside the Secrets textarea and nowhere
outside it or on other pages. Form and textarea autocomplete are off.
- `tests/test_admin_browser.py` starts a real local ASGI fixture behind a
simulated random external prefix and drives Chromium through pinned Playwright
(`tests/requirements-browser.txt`). Its dedicated lane fails rather than
silently skipping when Playwright is absent.
- The browser test visits every navigation page, validates exact response
security headers and inline-script CSP enforcement, rejects unexpected console
errors, checks desktop/mobile body overflow, and proves every link, stylesheet,
and form remains under the random external prefix.
- The browser test also proves the plaintext fixture secret exists only in the
Secrets textarea, no local/session/Cache/IndexedDB/service-worker persistence
exists, and an unsaved sentinel is absent after a network reload. Native
browser back-forward memory is not treated as application persistence.
### Final verification state
- PyCompile passed for `app/admin_api.py`, `tests/test_admin_api.py`, and
`tests/test_admin_browser.py` plus `tests/edge_e2e_client.py`.
- `tests/test_admin_api.py` passed: 45 tests.
- `tests/test_admin_browser.py` passed in real local Chrome: 1 test.
- Broad selected admin/runtime-document/operation/control/schema/worker/edge/
query suites passed: 190 unittest tests, one expected Windows symlink skip.
- Container selection passed: 37 modules, 821 test definitions, no runner skips.
- Full local edge E2E passed with current rebuilt images. It proved durable
operation/audit attribution to the authenticated Basic user, spoofed operator
header stripping, Origin/CSRF behavior, worker-route independence, fail2ban
behavior, cleanup, and unchanged foreign Docker state.
- Desktop and 390px mobile real-browser checks passed with no body overflow,
scripts, unexpected console errors, or secret leakage. Wide Audit content
scrolls only inside its bounded table container.
- An independent read-only review found no remaining concrete task 7.8 defect.
- Strict OpenSpec validation and `git diff --check` passed.
## Completed Task 8.1
Task 8.1 defines the trusted logical-root policy and exact per-root permissions
and limits without opening files or prematurely implementing traversal, file I/O,
HTTP Files pages, or mutation audit behavior from tasks 8.2–8.4.
### Policy and configuration
- Added `app/managed_files.py` with immutable typed root, permission, limit, and
registry records plus the exact operation enum: list, read, create-replace,
and delete.
- Root IDs are bounded lowercase logical names; absolute host/container paths
exist only in trusted server configuration and are hidden from root repr.
- Every root must explicitly provide all four permissions and all six limits:
relative-path bytes, component bytes, path depth, listing entries, listing
bytes, and file bytes. Per-root values cannot exceed fixed hard caps.
- The predefined existing roots are `runtime-logs`, `runtime-keychecks`, and
`runtime-results` at their fixed runtime directories. They are forced to
list/read only and cannot be renamed or granted mutation permissions.
- `runtime-results` permits files up to 256 MiB so active and rotated
`scan_results.jsonl` and `found_secrets.jsonl` generations remain
downloadable. Only active names and exact six-digit generation names are
visible; locks, databases, ledgers, scan errors, temporary/quarantine
directories, malformed generations, and recovery artifacts are excluded.
Keycheck and log roots retain the narrower 64 MiB per-file bound, and custom
roots cannot opt into the larger result bound.
- Future writable roots are permitted only as immediate children of the isolated
`/data/managed-files` directory. No writable root is enabled by default.
- This narrow allowlist excludes config/secrets, candidates, PostgreSQL,
application code, sockets/control state, host-agent metadata, raw result
bundles/spool, runtime state/queues/caches/work, and imported archives in both
normal and parent-container directions. Projected results and keycheck output
are available only through their fixed read-only roots.
- Root mappings are deterministically ordered, duplicate paths are rejected,
nested fields are exact, booleans cannot pass integer limits, and errors do
not echo submitted IDs or paths.
- Missing roots mean an empty registry. Existing explicit `admin: null`
compatibility remains an empty disabled admin configuration, while a present
`managed_file_roots: null` or any other malformed section fails closed.
### Runtime wiring
- `app/config.linux.yaml` defines the read-only `runtime-logs`,
`runtime-keychecks`, and `runtime-results` roots with bounded deployment
defaults.
- `app/runtime_document.py` treats root IDs as a strict dynamic mapping, validates
the policy even while admin is disabled, preserves omission compatibility, and
includes the new dynamic shape in the pinned template schema hash.
- `app/worker_api.py` validates roots before DSN/secrets/assignment-builder work
and passes the immutable registry into `AdminService`.
- `AdminService` accepts only a `ManagedFileRootRegistry` and otherwise defaults
to an empty registry. No `/files` navigation or route exists yet; that belongs
to task 8.4.
- Task 8.1 performs no `open`, stat, path resolution, directory creation, or
network work. Descriptor opening and target traversal remain task 8.2.
### Verification
- Added `tests/test_managed_files.py`; 8 policy/configuration tests passed.
- Runtime-document validation passed: 22 tests.
- Worker runtime wiring passed: 12 tests.
- Admin API passed: 46 tests.
- Runtime-document I/O regressions passed: 25 tests with one expected Windows
symlink skip.
- Container selection passed: 38 modules, 832 test definitions, no runner skips.
- Strict OpenSpec validation and `git diff --check` passed.
- Independent read-only review found no remaining concrete correctness or
security findings.
## Completed Task 8.2
Task 8.2 adds the Linux descriptor-containment layer. It deliberately does not
yet enumerate directory entries, return file bytes, mutate files, add HTTP
routes, or write audit events; those belong to tasks 8.3 and 8.4.
### Canonical path and descriptor model
- `parse_managed_relative_path()` requires an exact UTF-8 string and returns
unchanged components only after enforcing the configured byte, component, and
depth limits.
- Empty, absolute, drive-qualified (including nested drive components),
backslash, NUL, repeated-separator, trailing-separator, dot, and dot-dot paths
fail before target traversal.
- `ManagedFileTraversal` is Linux-only for nonempty registries and fails closed
unless `O_PATH`, `O_DIRECTORY`, `O_NOFOLLOW`, `O_CLOEXEC`, descriptor-relative
`os.open`, and the required nonblocking flags are available.
- Each configured absolute root is opened from `/` one component at a time with
`dir_fd`, `O_PATH | O_DIRECTORY | O_NOFOLLOW`, and retained for the traversal
lifetime. Symlinked root or parent components cannot be followed.
- Each operation duplicates the retained root under a lock before walking. This
prevents concurrent `close()` and descriptor-number reuse from redirecting a
request; already-started operations remain anchored after close.
- Intermediate client components use the same `O_PATH | O_DIRECTORY |
O_NOFOLLOW` traversal. A renamed/replaced configured pathname does not change
the retained root object.
- Listing opens only a verified directory descriptor. `None` is the internal
root-list sentinel; an empty client path remains invalid.
- Read targets are first opened with `O_PATH | O_NOFOLLOW`, checked as a
single-link regular inode, then reopened through their own `/proc/self/fd`
descriptor and revalidated by type, link count, device, and inode before a
readable descriptor is returned. Devices/FIFOs/sockets therefore are not
opened for reading before type rejection.
- Opened targets are context-managed; partial root walks, operation walks,
ordinary exceptions, and `BaseException` paths close every descriptor they
unambiguously own. `close()` is lock-safe and idempotent; use-after-close
fails closed.
- Errors expose only bounded categories such as `invalid_path`, `unknown_root`,
`operation_not_allowed`, `not_found`, `unsafe_target`, `root_unavailable`,
`filesystem_unavailable`, and `closed`. They never echo client paths, root
paths, errno text, or filenames.
### Adversarial coverage and packaging
- Traversal tests cover canonical ASCII/Unicode parsing, UTF-8 byte accounting,
exact no-follow/dir-fd flags, partial-constructor and operation cancellation,
close/open races, permissions, unknown roots, non-Linux behavior, and procfs
failure classification.
- Real Linux tests cover nested list/read descriptor opens, root rename/path
replacement anchoring, root/intermediate/final symlinks, hardlinks,
directory-as-file, FIFO, Unix socket, component swap after intermediate open,
idempotent close, and use-after-close.
- `.dockerignore` now admits only the new managed-file module/test explicitly,
and `container_unit.py` selects the policy, mock traversal, and Linux kernel
traversal classes.
### Verification
- Native managed-file suite: 17 tests, 13 passed and 4 expected Linux-only
skips.
- WSL Linux managed-file suite: 17/17 passed.
- Rebuilt read-only Linux test image: 17/17 managed-file tests passed under UID
10001 and the container audit fences.
- Related runtime-document, worker-runtime, admin, and runtime-document-I/O
suites passed: 105 tests with one expected Windows symlink skip.
- Container selection passed: 38 modules, 841 test definitions, no runner skips.
- Strict OpenSpec validation and `git diff --check` passed.
- Final independent security review found no concrete task 8.2 findings.
## Completed Task 8.3
Task 8.3 adds the bounded listing/download and durable file-mutation service on
top of task 8.2 descriptor containment. It deliberately adds no HTTP route,
operator form, durable operation row, or audit event; those belong to task 8.4.
### Listing and download
- `list_directory()` lazily scans an already verified directory descriptor,
counts every encountered entry against the configured work limit, bounds
returned UTF-8 name bytes, and sorts accepted logical names deterministically.
- Listings expose only directories and bounded single-link regular files.
Symlinks, hardlinks, special files, over-limit files, invalid names, and
internal temporary names are omitted without revealing host paths.
- `download_file()` reuses the `O_PATH`/`/proc/self/fd` inode-safe open, rejects
oversized files before reading, reads at most the configured bound, computes
SHA-256 while reading, and verifies stable inode/type/link/size/mtime/ctime
metadata before returning content.
- Public result records expose logical names, file/directory kind, bounded byte
counts, hashes, and content only on the direct download result. Download
content and host descriptors remain hidden from repr.
### Durable create/replace/delete
- Client relative components beginning with the reserved internal temporary
prefix are rejected; the prefix is also hidden from listings.
- Create/replace accepts exact `bytes` up to the root file limit. `None` expected
hash means create-only; a canonical lowercase SHA-256 means replace-only.
- Temporary files are created with random same-directory names using
`O_RDWR | O_CREAT | O_EXCL | O_NOFOLLOW | O_CLOEXEC`, start private, are
hardened to exact mode `0600`, and must be regular, single-link, and owned by
the effective runtime UID.
- Writes handle short writes/interruption, verify exact size and SHA-256, fsync
the temporary inode, and retain its descriptor through publication.
- Create publishes atomically with Linux `renameat2(RENAME_NOREPLACE)`, so an
existing name is never overwritten. Replace revalidates the expected inode
and hash before descriptor-relative `os.replace`; readers observe complete old
or complete new files.
- Every mutation takes both an in-process lock and an advisory exclusive flock
on the opened parent-directory inode. Separate traversal instances/processes
using this service therefore implement one cooperative hash-CAS authority.
Writable roots are dedicated immediate children of `/data/managed-files`;
arbitrary SSH/root writers that ignore the lock are outside this contract.
- Namespace publication and unlink run inside a `finally`-protected parent
directory fsync. A failure after visible mutation is reported only as bounded
`durability_uncertain`/`concurrent_change`; the service never attempts an
unsafe rollback over a later writer.
- Final published files are re-read and verified for expected hash/bytes,
original staged inode, effective UID, exact `0600`, regular type, and one
link. The parent descriptor is then revalidated.
- Replace with identical expected/proposed identity is an idempotent no-write.
- Delete requires the exact current SHA-256, revalidates the named inode,
unlinks descriptor-relatively, and fsyncs the parent directory.
- Prepublication failure and `BaseException` paths unlink and directory-fsync
the known temporary name before potentially interrupted descriptor close.
Explicit `LOCK_UN` prevents an interrupted parent close from retaining the
mutation lock. Cancellation is never hidden by an ordinary conflict/error.
- Errors remain content/path/errno-free categories such as `invalid_hash`,
`invalid_content`, `limit_exceeded`, `hash_conflict`, `concurrent_change`,
`durability_uncertain`, and the task-8.2 access categories.
### Verification
- Native Windows managed-file suite: 28 tests, 13 passed and 15 expected
Linux-only skips.
- WSL Linux managed-file suite: 28/28 passed, including real `renameat2`, flock,
inode/mode/owner checks, fsync, cleanup, and cross-instance concurrency.
- Rebuilt read-only Linux test image: 28/28 managed-file tests passed under UID
10001 with network disabled; 1048 unrelated tests were deselected.
- Cross-instance and independent-process replace/replace and replace/delete
probes produced exactly one winner.
- Related runtime-document, worker-runtime, admin, runtime-document-I/O, and
operation-service suites passed: 121 tests with one expected Windows symlink
skip.
- Container selection passed: 38 modules, 852 test definitions, no runner skips.
- Final independent security review found no concrete task 8.3 findings.
## Completed Task 8.4
Task 8.4 exposes the descriptor-safe managed-file service through exact
server-rendered admin routes and binds every accepted mutation to a durable,
content-free operation and audit lifecycle.
### Files pages and forms
- Shared admin navigation now includes `Files`.
- `GET /admin-internal/files` renders either the logical-root index or a bounded
root/nested listing selected by exact `root_id` and optional canonical
`relative_path` query fields.
- `GET /admin-internal/files/download` requires exact logical root/path fields
and returns `application/octet-stream` with no-store/security headers, exact
length, SHA-256 ETag, and a logical-leaf attachment filename. Ordinary files
retain the bounded in-memory response. An allowed result projection is first
copied and hashed in 64 KiB chunks into an anonymous stable snapshot on the
same data volume, then streamed from that snapshot with bounded memory.
Source revision/size changes retry once and then fail closed; only one result
snapshot may exist at a time, and response completion/failure releases it.
No host path is passed to a response object.
- Async request cancellation retains cleanup ownership until a background
snapshot build finishes, while the streaming response closes the snapshot in
a response-level `finally`; disconnects and ASGI send failures therefore
cannot strand the sole result-download permit.
- Exact POST routes are `/files/create`, `/files/replace`, and `/files/delete`.
They accept only CSRF, operation UUID, logical root ID, canonical relative
path, canonical expected hash where required, and canonical URL-safe Base64
content for create/replace.
- The existing bounded URL-encoded admin body is the explicit web-upload cap;
the page displays that bound. No multipart parser, JavaScript, command field,
actor field, absolute root, or generic action field was added.
- Root pages expose only logical IDs, permissions, configured limits, safe
relative names/kinds/byte counts, and permission-appropriate forms. Private
configured absolute roots and arbitrary file bytes never enter HTML.
- Query and form shapes reject duplicate, missing, extra, partial, empty, or
noncanonical values before filesystem or database mutation.
- Managed-file errors map only to bounded HTTP outcomes (400/403/404/409/413/
503) and never expose paths, errno text, content, or raw exceptions.
### Durable mutation authority
- ScannerDB recognizes exact actions `files.create`, `files.replace`, and
`files.delete` with target kind `managed-file` and logical root ID as the
bounded target reference.
- Canonical expected identity contains only root ID, relative path, expected
SHA-256, proposed SHA-256, and proposed byte count. Successful resulting
identity contains only before/after hashes and counts, outcome, and written
flag. No migration was required; migration count remains 29.
- `create_runtime_managed_file_operation()` atomically commits a running
operation and accepted audit before physical mutation.
- `complete_runtime_managed_file_operation()` commits one terminal audit and
safe resulting identity, or the fixed content-free failure category
`managed_file_mutation_failed`.
- Uploaded bytes, Base64, CSRF/Origin/auth data, credentials, absolute roots,
exception text, and rendered content cannot be accepted by the operation or
audit APIs.
- List and download remain read-only and create no operation/audit rows.
### Replay and execution serialization
- Each mutation holds a per-operation execution claim on a dedicated DB
connection across terminal lookup, preflight, acceptance, physical CAS, and
terminal completion.
- PostgreSQL uses a session advisory lock keyed by the canonical operation UUID.
SQLite uses deterministic process-global lock stripes for local/test
connections. Different operation IDs targeting one path remain serialized by
task 8.3 parent-directory flock and hash CAS.
- Lock order is operation advisory claim, AdminService local lock, then traversal
parent-directory lock. Release and connection close run through unconditional
nested cleanup even under `BaseException`; cancellation remains primary and
uploaded payload references are cleared.
- Exact terminal success validates immutable operation identity and returns
without requiring current traversal/root availability. Terminal failure
requires a new operation UUID.
- A fresh request validates current root permission/path and expected namespace
state before accepted audit creation. Create requires absence; replace/delete
require the exact expected hash. A fresh CAS loss always fails even if another
writer produced matching bytes.
- A running exact replay may retry only from its accepted before state. It may
complete without a second mutation only after observing the accepted desired
after-state (or stable deletion), fsyncing the parent directory, and
revalidating the same named inode/revision or absence after fsync.
- Matching replayed create/replace after-state must also be a private regular
single-link file owned by the runtime UID with exact mode `0600`.
- Content-bearing parser, dispatcher, service, and traversal frames clear or
release URL-encoded, Base64, decoded payload, and memory-view locals on normal,
error, and cancellation paths.
### Traversal lifecycle
- Worker API lifespan constructs exactly one retained `ManagedFileTraversal`
when admin is enabled and reuses it for all Files requests.
- Root-open failure safely degrades Files to 503 without taking down worker
claim/status/upload/report routes and logs only a bounded category/type.
- Nested shutdown cleanup clears app state and closes the traversal exactly once
even when assignment-reaper shutdown fails. Admin-disabled apps never open
managed roots.
### Verification
- Focused host suites passed: operation service 19/19, admin API 54/54, Worker
API 35/35, and real-browser admin coverage 1/1.
- Managed-file tests passed 30/30 on WSL Linux; native Windows ran the same 30
with 17 expected Linux-only skips.
- Related runtime-document, runtime-document-I/O, worker-runtime, operation
control/schema, edge-deployment, and production-query suites passed; the only
skip was the expected Windows symlink case.
- Real Chromium covered Files navigation under the random prefix, security/CSP,
no scripts/storage, mobile layout, and console cleanliness.
- Rebuilt read-only/no-network Linux test image passed all 46 selected
managed-file/admin/operation/lifecycle tests under UID 10001; 1046 unrelated
tests were deselected.
- Container selection passed: 38 modules, 868 test definitions, no runner skips.
- SQLite two-connection execution serialization and cancellation cleanup have
dedicated regressions. A PostgreSQL two-session advisory-block/unlock/
session-close integration test is committed and discovered, but skipped
locally because bundled PostgreSQL is unavailable.
- Strict OpenSpec validation and `git diff --check` passed.
- Final independent security review found no concrete task 8.4 findings.
## Completed Task 8.5
Task 8.5 closes the managed-file service with adversarial traversal, link,
special-file, limit, concurrency, durability, transport-encoding, and
forbidden-root coverage.
### Production hardening
- Nested listings validate the full logical child path, not only the leaf name,
before exposing an entry. Children beyond the configured path-byte or depth
bound remain hidden.
- Temporary-file cleanup fsyncs the parent directory even when the allocated
temporary name is already absent, preserving durable cleanup evidence.
- Admin dispatch requires byte-valued ASGI `raw_path` to be the exact canonical
ASCII encoding of the decoded fixed route. Missing or percent-encoded route
aliases fail with a secured 404; query and form values still decode exactly
once through their normal parsers.
- Download stability again requires an unchanged regular single-link inode,
including device, inode, size, link count, mtime and ctime. On a concurrent
atomic replacement, download performs at most one canonical reopen and strict
reread, so callers receive one complete version rather than mixed bytes.
### Adversarial coverage
- Policy tests explicitly exclude active secrets, candidate documents,
PostgreSQL, host-agent metadata, Docker/runtime sockets, application code,
raw bundles, spool/results/state/queues/cache/work, and imported archives.
- Portable tests prove FIFO, socket, character-device, block-device and
directory modes are rejected before a readable `/proc/self/fd` reopen.
- Real Linux tests cover root and final symlink swaps, retained-descriptor
anchoring, hardlinks, directories, FIFOs, hidden unsafe listing entries, and
rejection by download/replace/delete.
- Exact and one-over tests cover total path bytes, component bytes, depth,
listing entry/name-byte limits, nested logical paths, and file download/upload
limits.
- Two traversal instances prove one winner for create/create and delete/delete;
prior replace/replace and replace/delete tests remain. Reader replacement
tests prove complete-version behavior, including a hostile in-place write,
restored mtime, and atomic pathname replacement.
- Durability fault tests cover temporary inode fsync, already-absent temporary
cleanup, cleanup unlink/fsync failures, and replace/delete parent-fsync
uncertainty while asserting the resulting visible namespace.
- HTTP tests cover encoded and mixed-case traversal, backslash, NUL, drive
prefixes, exact one-pass double decoding, encoded fixed-route aliases,
forbidden logical roots, generic action/command/path/service field injection,
and exact/over/streaming request-body limits before decode, traversal, or
operation/audit creation.
### Writer authority boundary
- Writable roots are dedicated managed-service roots. Cooperating app instances
and processes serialize mutations with the parent-directory advisory flock
and hash CAS.
- Arbitrary SSH/root writers that deliberately ignore that flock are outside
this authority contract; POSIX has no atomic primitive for
`replace/unlink iff current content hash equals X` against such writers.
- The service still detects bounded concurrent drift wherever possible and
never follows a swapped symlink target.
### Verification
- Managed-file tests passed 37/37 on WSL Linux; native Windows ran the same 37
with 23 expected Linux-only skips.
- Native scoped review reported 71 passed plus the expected Linux skips, and the
admin API adversarial suite passed.
- Broad runtime-document, operation/control/schema, Worker API/runtime,
production-query, edge-deployment, and real-browser suites passed; the only
broad skip was the expected Windows symlink case.
- Container selection passed: 38 modules, 878 test definitions, no runner
skips.
- Rebuilt `truf-worker-test:task-8-5`; a read-only, no-network Linux run under
UID 10001 passed all 55 selected managed-file/admin/operation/lifecycle tests,
including all 37 Linux managed-file tests; 1047 unrelated tests were
deselected.
- Independent final security review found no concrete in-scope correctness or
security findings.
- Strict OpenSpec validation and `git diff --check` passed before artifact
closure.
## Completed Task 9.4
Task 9.4 adds one fixed automatic rollback attempt, durable terminal evidence,
and a global failed-hold fence without adding request-selected lifecycle or file
surfaces.
### Byte-identical rollback
- Stopped-runtime proofs are operation-bound, purpose-bound (`forward` or
`rollback`), uniquely issued, and single-use. Forward proof cannot authorize
restoration and rollback proof cannot authorize candidate publication.
- `HostApplySession` retains authoritative original snapshots separately from
observed active/candidate state and classifies publication as `original`,
`partial`, or `candidate`.
- Exact mixed old/candidate replay is recovery-only. It never reapplies the
candidate deployment.
- `restore_backups()` accepts no paths or payloads. It rereads fixed root-owned
operation backups, accepts active bytes only when exactly original or exact
candidate, stages byte-identical originals, rechecks CAS, atomically restores,
adopts fixed runtime ownership/mode, fsyncs, and verifies the complete original
pair. Backups and candidates remain as evidence.
- Exact root-owned originals left by interruption between rollback rename and
ownership adoption are recognized and safely adopted on replay. Any unknown
active bytes fail closed without overwrite.
- Publication state becomes `partial` before the first restoration mutation, so
a failed second restore cannot be misreported as a complete candidate state.
### One-attempt recovery and failed hold
- Lifecycle execution retains a private mutable attempt before the first stop,
including the original attested image/config identities and every observed
stop/remove/recreate milestone.
- Any failure after lifecycle mutation quiesces only the exact fixed edge and
runtime containers, restores selected backups, and recreates/verifies the
original attested runtime and edge exactly once with the same strict aggregate
health gates.
- Successful recovery emits `rolled_back` with both original active hashes and
the bounded forward failure category. It never reports apply success.
- Recovery failure writes the global failed-hold marker before one containment
stop, preserves operation/backups/candidates, emits `failed_hold`, and performs
no third deployment attempt, restoration loop, or forced authority release.
- A global marker fences every different operation. The same operation may only
finish a missing `failed_hold` phase/result; it skips document preparation and
cannot resume forward or rollback lifecycle work.
- Pre-mutation validation/identity failure records `failed` without downtime or
rollback.
### Durable phase and result evidence
- New `app/host_agent_state.py` uses only fixed operation/result/hold paths under
`/var/lib/truf/host-agent`, canonical bounded JSON, no-follow stable reads,
root metadata checks, same-directory atomic replacement, file fsync, directory
fsync, and exact-byte idempotent replay.
- Closed phases are `prepared`, `forward_started`, `rollback_started`,
`succeeded`, `failed`, `rolled_back`, and `failed_hold`; transitions bind the
exact operation UUID, action, four request hashes, publication state, bounded
category/detail, and containment evidence.
- The terminal phase is persisted before the immutable seven-field ScannerDB-
compatible result. Missing results are deterministically reconstructed from a
terminal phase without lifecycle mutation.
- A write that renamed phase evidence but could not confirm parent durability is
classified `uncertain`. Terminal uncertainty never rolls back or contains an
already healthy deployment; nonterminal rollback uncertainty durably fences
and contains instead of retrying.
- `KeyboardInterrupt` and `SystemExit` propagate directly before mutation. After
mutation, the one rollback/failed-hold safety path runs first and the original
cancellation is still propagated, including result-write and phase-rename
uncertainty windows.
### Verification
- Lifecycle tests passed 32 selected cases in the final Linux container and
native Windows passed with four expected POSIX-only skips.
- Host apply passed 23/23 under WSL root; native passed 18 with five expected
root/POSIX skips. Durable state passed 6/6, protocol passed 15/15.
- Broad operations, container-runtime, runtime-document/I/O, admin, Worker API,
worker-runtime, and edge-deployment regressions passed.
- Container selection passed: 43 modules, 968 test definitions, no runner skips.
- Rebuilt `truf-worker-test:task-9-4`; a read-only, no-network container run as
UID 10001 passed 76 selected host-agent tests with two expected distinct-root-
ownership skips; 1119 unrelated tests were deselected.
- Independent security/correctness review completed repeated crash/cancellation
review rounds and returned no findings after terminal ordering, early fencing,
replay, uncertainty, and cancellation fixes.
- Strict OpenSpec validation and `git diff --check` passed before artifact
closure.
## Completed Task 9.1
Task 9.1 establishes the authenticated, closed privileged-agent request
boundary without implementing file replacement, restart, health, or rollback.
### Protocol and client
- `app/host_agent_protocol.py` defines a strict canonical UTF-8 JSON protocol
framed by a four-byte big-endian payload length.
- Requests are bounded to 1024 payload bytes and contain exactly operation UUID,
one of `apply-config`, `apply-secrets`, `apply-both`, or `restart`, and the
four active/candidate hash fields used by ScannerDB expected identity.
- Canonical nonzero UUID, lowercase SHA-256 values, and the action-specific
candidate-null/hash matrix are enforced. Duplicate, missing, extra, reordered,
whitespace-altered, non-finite, or incorrectly typed fields fail closed.
- No path, service, unit, command, argv, environment, Docker/Compose argument,
timeout, actor, content, or generic options field exists.
- Each connection carries exactly one request and one response. The client
half-closes its write side; the server requires EOF, rejecting trailing or
pipelined bytes. Absolute monotonic deadlines prevent trickle extension.
- `app/host_agent_client.py` has no configurable socket path and uses only
`/run/truf/host-agent.sock`. It requires a root-owned socket pathname and
kernel `SO_PEERCRED` proving the connected server UID is root; there is no
retry loop.
### Root agent boundary
- `app/host_agent_server.py` checks kernel peer credentials before reading any
body and permits only the fixed runtime UID 10001. GID is intentionally not
part of the identity contract.
- The server validates an inherited systemd socket as exact `AF_UNIX`, exact
`SOCK_STREAM`, listening, fixed-path, socket-typed, and root-owned.
- `deploy/host-agent/truf_host_agent.py` requires root, no command-line
arguments, and exactly one systemd-activated listener.
- The production task-9.1 handler always returns `unavailable`. It never claims
`accepted` before task 9.2 has verified the persisted operation and durably
assumed ownership.
- Systemd units, host path installation and socket permissions remain task 9.5;
the agent executable is ready for that fixed activation contract but no unit
was added early.
### Runtime integration boundary
- Admin apply provider calls now pass the persisted expected identity fields:
active config/secrets hashes and candidate config/secrets hashes, in addition
to operation ID and action.
- Production Worker API still installs no apply provider. An absent agent
therefore preserves the existing 503 before operation creation. Wiring is
deferred until persisted verification/execution exists; no request can
currently trigger privileged lifecycle work.
### Verification
- Portable protocol/client/server tests passed 15/15, including exact schemas,
candidate matrix, framing bounds, duplicate/extra fields, absolute trickle
deadlines, response pipelining, cancellation cleanup, unauthorized-before-
read ordering, listener type/listening checks, and inherited-FD cleanup.
- Real Linux tests passed 2/2 under fixed UID 10001 using kernel `SO_PEERCRED`.
- Rebuilt `truf-worker-test:task-9-1`; a read-only, no-network container run as
UID 10001 passed all 17 selected host-agent tests, including the real Linux
peer path; 1102 unrelated tests were deselected.
- Broad admin 57, operations 19, Worker API 35, worker-runtime 12, edge-static,
and provider-hash-binding regressions passed.
- Container selection passed: 40 modules, 895 test definitions, no runner
skips.
- Independent security review found no concrete task-9.1 correctness or
security findings after exact `SO_TYPE`/`SO_ACCEPTCONN` hardening.
- Strict OpenSpec validation and `git diff --check` passed before artifact
closure.
## Completed Task 9.2
Task 9.2 adds a fail-closed execution core for one persisted runtime apply under
fixed host paths. It remains deliberately unwired until task 9.3 can stop the
runtime before publication and task 9.5 installs protected DB/path authority.
### Persisted operation claim
- `ScannerDB.claim_runtime_operation_execution(...)` atomically binds the
canonical operation UUID, action, runtime-deployment target, and exact four
active/candidate hashes to the requested-to-running transition.
- The method uses the existing SQLite write transaction or PostgreSQL control
and operation row locks. The host session separately requires PostgreSQL;
SQLite support exists only for deterministic database tests.
- Requested/pending operations can be claimed once. An exact running/running
operation replays; unknown, mismatched, terminal, or incoherent rows fail.
- Claim requires exactly one canonical accepted audit for the operation and
verifies its actor/action/target/identity/time, global predecessor link, the
predecessor's own payload hash, and the accepted event hash before transition.
- Schema-valid unlinked audit predecessors remain supported. No schema migration
or new authority table was added.
### Fixed host apply session
- `app/host_agent_apply.py` defines only fixed constants: active config/secrets,
candidate config/secrets, root apply lock, operation-ID backup directory, and
the trusted packaged config template. Socket requests cannot provide paths,
commands, services, environment, or options.
- `HostApplySession` takes the root-private nonblocking singleton filesystem lock
before claim and keeps it through validation, backup, publication, and later
task-9.3 lifecycle insertion points. The lock survives PostgreSQL shutdown.
- The session requires PostgreSQL authority, claims the exact persisted request,
stable-reads regular single-link active/candidate/template files with nofollow,
enforces fixed owner/group/mode and raw hash CAS, selects the action-specific
effective pair, and calls the shared strict runtime-document validator.
- Package capability evidence is host-supplied trusted input, never a socket
field. Production remains unwired until task 9.5 provides fixed protected
package/DB/path mappings.
### Backup, publication, and replay
- Selected active bytes are backed up byte-for-byte under the canonical
operation UUID using exclusive creation, root-only metadata, file fsync,
reread/hash verification, and parent-directory fsync. Existing backups are
accepted only when their bytes and identity exactly match.
- All selected replacements are staged and fsynced before publication. Stages
remain root-owned until same-directory atomic rename, then receive fixed
runtime ownership/mode and are fsynced again; the runtime UID cannot mutate a
predictable stage before publication.
- Active and candidate snapshots are rechecked immediately before publication.
On POSIX, the displaced active inode is retained and reread after rename, so a
late in-place writer becomes a conservative partial result rather than silent
data loss.
- Candidates and backups remain as evidence. A stale fixed stage from the same
operation is durably removed before retry.
- Exact running replay distinguishes not-published, completely-published, and
partially-published state. Complete replay requires exact backups, candidates,
and active bytes, recovers a root-owned post-rename/pre-adoption file, and
revalidates again at `replace()`. Partial replay requires valid backups and
fails closed for task 9.4 recovery.
- Each config/secrets rename is atomic; the pair is not a filesystem transaction.
A failure after any publication is categorized `partial`; task 9.2 does not
invent rollback or failed-hold behavior ahead of task 9.4.
- `restart` still claims and validates the active pair but creates no backup and
publishes no file.
### Production boundary
- `deploy/host-agent/truf_host_agent.py` was not wired to this core and still
returns `unavailable`. Current deployment has no protected host PostgreSQL
endpoint, fixed mounts/permissions, or safe stop/recreate sequence.
- Task 9.3 must guarantee stopped-runtime publication and perform fixed
recreation/health checks. Task 9.4 owns rollback. Task 9.5 owns installation,
host DB/path authority, systemd units, and permissions. Task 9.6 owns the full
crash/fault matrix.
### Verification
- Host apply tests passed 17/17 under WSL root with distinct root/runtime
ownership; native Windows passed with four expected POSIX-only skips.
- Operation tests passed 25/25, including exact claim/replay, mismatch,
accepted/predecessor corruption, valid unlinked predecessor, missing, and
terminal cases.
- Broad protocol 15, runtime-document 22, runtime-document I/O 25, admin 57,
Worker API 35, worker-runtime 12, and edge deployment suites passed.
- Container selection passed: 41 modules, 918 test definitions, no runner skips.
- Rebuilt `truf-worker-test:task-9-2`; a read-only, no-network container run as
UID 10001 passed 33 selected host-agent tests with one expected root-only
ownership-recovery skip; 1108 unrelated tests were deselected.
- Independent security/correctness review found no findings after root-owned
staging, displaced-inode detection, durable replay, and predecessor-hash fixes.
- Strict OpenSpec validation and `git diff --check` passed before artifact
closure.
## Completed Task 9.3
Task 9.3 adds a fixed, fail-closed runtime/edge lifecycle and bounded aggregate
health verification. It remains deliberately unwired until rollback and
installation authority are complete.
### Fixed lifecycle authority
- `app/host_agent_lifecycle.py` accepts no production paths, services, commands,
environment, or timeouts. It uses only `/usr/bin/docker`, project
`truf-docker`, the two fixed Compose files, fixed edge environment, and the
`runtime` and `edge` services.
- Preflight validates the fixed Compose model, captures immutable runtime/edge
image IDs, resolves exactly one container per service, and inspects only a
bounded selected projection. It never reads container environment, full state,
health logs, or host mount source paths.
- Attestation binds IDs, images, users, entrypoint/command, runtime healthcheck,
Compose labels/config hash, capabilities/security options, read-only and
privileged state, namespaces, restart policy, resource limits, stop policy,
log driver, destination-only mounts, ports/tmpfs, and absence of device access.
- Edge stops first with a fixed 30-second grace; runtime then receives the fixed
600-second coordinated stop. Each captured container must prove exited, PID 0,
exit 0, non-OOM, non-restarting/non-dead, and unchanged restart count.
- Immediately before publication, lifecycle re-resolves both service IDs,
revalidates their stopped state and immutable image tags, and issues a private
operation-bound stopped proof. `HostApplySession.replace()` rejects absent,
forged, or wrong-operation proof.
- The executor removes only the stopped edge and runtime containers, recreates
runtime with `--no-deps --no-build --pull never --force-recreate`, requires a
new exact identity, waits under one bounded deadline, and recreates edge only
after strict runtime health. Edge likewise requires a new exact identity,
fixed network namespace, successful fixed Caddy validation, and bounded stable
running observation.
- No `down`, kill, build/pull, volume/orphan, provision, systemd, fail2ban,
request-selected action, automatic recreation retry, rollback, or terminal
result reconciliation was added.
### Document and execution ordering
- `execute_fixed_forward()` keeps the task-9.2 singleton lock across backup,
deployment preflight, pre-stop CAS, stop proof, publication, recreation, and
health verification.
- `HostApplySession.revalidate_for_stop()` rechecks active/candidate identities
after backup/preflight and before downtime; `replace()` repeats validation
after clean stop and immediately before publication.
- Restart follows the same fixed stop/recreate/health sequence but does not back
up or publish documents. Complete publication replay recreates and verifies
once rather than treating file presence as runtime health evidence.
### Bounded aggregate health
- `container_runtime.py health --require-worker-api` preserves all existing
authenticated Supervisor ACTIVE, PostgreSQL READY/schema/cutover/PG16/storage,
and instance-bound ingester/projector lease checks, while requiring Worker API
to be explicitly enabled and running.
- Strict Worker API health sends a fixed non-secret invalid Bearer credential to
loopback `/api/v1/worker/claim` and requires the exact bounded 401 Bearer
response. Authentication performs the normal ScannerDB lookup before rejection,
proving the API-to-PostgreSQL path without a real credential or mutation.
- Ordinary Compose health remains unchanged; the strict flag is valid only for
health/status. Production cannot pass strict mode until task 9.5 installs the
intended enabled Worker API configuration.
### Command and deadline hardening
- Docker subprocesses use a fixed minimal environment, no shell, closed stdin,
suppressed stderr, a new process session, bounded 16 KiB stdout, and monotonic
phase deadlines.
- Timeout, output overflow, reader failure, cancellation, and a descendant that
retains stdout terminate only the owned process group with bounded waits.
Successful commands never signal a stale process-group ID, and the raw output
descriptor has single-owner close semantics.
- Stop/recreate/publication commands run at most once. Only bounded state/health
observation polls.
### Verification
- Lifecycle tests passed 18/18 on WSL, including real timeout, output-overflow,
retained-descendant, and successful-process cleanup behavior; native Windows
had four expected POSIX-only skips.
- Host apply tests passed 18/18 on WSL and protocol tests passed 15/15. The full
container-runtime suite passed with strict Worker API positive and hostile
response coverage.
- Broad operations 25, admin 57, Worker API 35, worker-runtime 12,
runtime-document 22, runtime-document I/O 25, and edge deployment suites
passed.
- Container selection passed: 42 modules, 943 test definitions, no runner
skips.
- Rebuilt `truf-worker-test:task-9-3`; a read-only, no-network container run as
UID 10001 passed 52 selected protocol/apply/lifecycle tests with one expected
root-ownership skip; 1119 unrelated tests were deselected.
- The selected real Docker inspect format was exercised locally and parsed all
49 expected keys; a legacy container with an extra bind/import command was
correctly rejected.
- Independent security/correctness review found no findings after stopped-proof,
pre-stop CAS, attestation, Worker API DB-path, deadline, process-group, and FD
ownership fixes.
- Strict OpenSpec validation and `git diff --check` passed before artifact
closure.
## Known Residual Boundaries
- Apply cannot execute in production until crash verification from task 9.6 is
implemented. Returning 503 before operation creation remains correct when the
fixed host-agent socket or result authority is unavailable.
- A process crash exactly after an external side effect and before terminal DB
persistence is a distributed boundary. Stable operation IDs and idempotent
replay are the intended mitigation; do not add a general coordinator here.
- Worker API self-stop/restart can terminate the admin request before terminal
operation completion. Full external lifecycle reconciliation belongs to the
host operation architecture, not task 7.5/7.6.
- Managed log pages display bounded child output as written. Do not add a
post-hoc redaction pipeline without user approval.
- Candidate and active file handoff across privileged apply will be revalidated
by the host agent; task 7.6 must not implement active replacement itself.
## Known Environment/Test Issues Unrelated to Current Work
- Several broad Supervisor safety tests fail in this checkout because external
runtime authority fixture files such as `runtime/check-openrouter-keys.ps1`
are absent or legacy fixture config is incomplete. Focused clean runs exclude
only the documented affected classes; do not weaken production code for them.
- Bundled PostgreSQL integration tests skip when `initdb`/test DSN is not
available.
- Windows symlink tests may skip when the process lacks symlink privilege.
- The worktree has no useful commit baseline and contains many pre-existing
untracked/dirty files. Never perform broad resets or cleanup.
- Caddy 2.10.2 does not implement SIGUSR1 reload while production fail2ban
documentation assumes it. Edge E2E uses an admin-enabled test reload path.
This predates task 7.1 and needs a separate Caddy-upgrade/reload decision.
- Post-fail2ban-reload generic 404/401 responses in edge E2E do not reliably
retain global security headers. Authenticated admin pages and mutations still
enforce strict headers; the harness isolates the unrelated generic response
behavior.
## Important Successful End-to-End Evidence
- Packaged Windows/Linux protocol-2 E2E passed with GitLab plus synthetic
DockerHub/HuggingFace claim-to-ingestion.
- Full container E2E passed after strict runtime-document integration.
- Real edge E2E passed twice after rebuilding edge, fail2ban and runtime images;
final safe result proved authenticated Basic username attribution, private
marker acceptance, worker header stripping, worker availability, and fail2ban
behavior.
- Tasks 7.2–7.8 received desktop/mobile browser checks with real Starlette
routes and trusted headers; task 7.8 also has committed real-Chromium coverage.
## Task 9.5 Completion
- Added the fixed systemd socket/service, tmpfiles layout, root-only installer,
installed `/usr/lib/truf-host-agent` entrypoints, graceful host-agent shutdown,
PostgreSQL peer authority, async operation dispatch, and durable result
reconciliation.
- Runtime Compose now uses exact long-form mounts with host-path creation
disabled. Host config and secrets use the read-only `/etc/truf/runtime` to
`/data/config` authority, while immutable package manifests use the separate
root-owned `/etc/truf/worker-packages` to `/data/worker-packages` authority.
Candidates, result handoff, host-agent socket, and PostgreSQL socket mounts
retain their fixed access. Runtime-generated initialization state, lock, and
PostgreSQL password remain in the private writable `/data` volume.
- Lifecycle inspection validates exact bind sources and the named data volume.
The installer parses bounded rendered Compose JSON and independently requires
the same source/target/read-only/create-host-path projection.
- Package manifests are stable-read as root-owned `0644` files beneath a fixed
descriptor-walked root. Every descendant directory is root-owned and not
group/other writable; host and container readers consume byte-identical files.
- The installer validates the full root-owned application tree, installed bytes,
executables, Docker and agent sockets, units, tmpfiles, directories, and file
modes. Repeat install stops active socket/service units before replacement,
reloads systemd, enables the socket, and explicitly restarts it. Existing
active config and secrets are never overwritten.
### Task 9.5 Verification
- Native focused runtime-document/security/host deployment suites passed 67
tests with seven platform skips; the wider host deployment/lifecycle focused
suites passed independently.
- WSL root stdlib suites passed 109/109. `systemd-analyze verify` accepted both
units; warnings were limited to Windows-mounted source-file permissions.
- Canonical combined base+edge Compose rendered JSON passed the exact installer
projection validator. Base Compose validation also passed under WSL Docker.
- The deny-by-default test image rebuilt successfully. Its UID 10001, read-only,
no-network run passed all task-9.5 host-agent/deployment selections. Four of
five initially failed broad tests passed alone after packaging `docker/verify.py`;
the remaining deep-JSON hostile-input assertion belongs to task 9.6.
- Container selection is AST-clean with 47 modules and 994 test definitions.
- Three independent review passes found no remaining task-9.5 blocker after
application-tree, mount-source, cleanup, manifest-root, Compose-projection,
and repeat-install fixes.
## Task 9.6 Completion
- Host-result JSON rejects nesting deeper than 64 before parsing, using an
iterative string-aware scanner so hostile depth is deterministic across Python
versions without recursive traversal.
- Candidate validation after a durable database claim now terminalizes as an
exact `failed/validation_failed` result before acceptance. A failed result
publication is replayable, and non-original prepared state is never rewritten.
- Crash-boundary coverage now proves interrupted combined-backup replay, retained
rollback evidence, restart-specific rollback, and durable rollback failure.
- Protocol coverage rejects deeply nested unknown request fields at the real
server boundary without invoking the privileged handler.
### Task 9.6 Verification
- Native task-matrix suites passed 106 tests with nine platform skips; the WSL
root stdlib matrix also exited successfully.
- Targeted UID 10001/Python 3.12 container tests passed 102 tests with two
expected root-identity skips.
- The full hardened, read-only, no-network container suite passed 1219 tests
with ten platform skips and one deprecation warning.
- Container selection is AST-clean with 47 modules and 1000 test definitions.
- Strict OpenSpec validation and diff checks passed. A final independent audit
found no remaining task-9.6 blocker.
## Production Completion Status
- The authorized production host now uses the exact root-selected
`shared-host-edge-v1` profile. Existing host Caddy remains the sole owner of
ports 80/443, the managed Truf edge binds only `127.0.0.1:18766`, and the
host agent has no lifecycle authority over host Caddy, X-UI, or another
unrelated service.
- Task 10.4 production execution is complete: protocol-2 package, runtime,
admin, edge, and agent checks passed; one reconciled restart succeeded; one
health-triggered automatic rollback restored the original documents and
reconciled as `rolled_back`; only then were production controls reopened.
## Task 10.1 Completion
- The production server image and lifecycle authority are remote-only and
Git-only; TruffleHog remains confined to worker and test images.
- Explicit code authority covers the runtime-document, managed-file, and
host-agent modules plus the core-profile launcher.
- Existing marked volumes gain only the fixed private `/data/managed-files`
directory; all other incomplete or unsafe layouts still fail closed.
- Production runbooks now require the fixed host-agent installer, protected
edge environment, active documents, package manifests, socket, and combined
runtime/edge Compose deployment in the correct order.
- Container selection includes the operations, discovery-only, core-profile,
multisource, and direct-source fixtures. The edge fixture advertises the
exact GitLab, DockerHub, and HuggingFace profile and real-Caddy coverage
traverses every new admin route plus operation detail.
## Task 10.1 Verification
- Focused native coverage passed 401 tests with eleven platform skips; the
final edge deployment suite passed 32 tests with one skip.
- Container selection is AST-clean with 54 modules and 1058 definitions.
- A hardened container run passed 1282 tests before fixture-only corrections;
all five corrected image/static/real-CLI cases then passed in the rebuilt
image.
- A built production runtime image was verified to contain executable Git and
no `/usr/local/bin/trufflehog`.
- Two independent reviews found no remaining task-10.1 blocker. Strict
OpenSpec validation and diff checks passed.
## Task 10.2 Completion
- The offline rollback test removes the protocol-2 worker and operations
authorities, applies the real stopped-runtime migration, and confirms all
additive tables are restored.
- A live remote reservation and pre-commit bundle prevent drain completion;
after both resolve, the authoritative state advances to `drained` with no
blockers.
- A raw previous-image enqueue contract then opens the same database and writes
using only legacy columns. The new control state, operation/audit evidence,
and protocol-2 tables remain intact.
- The complete operations-control suite passed 13 tests on Windows and 13 under
WSL. Container selection is AST-clean with 54 modules and 1059 definitions;
strict OpenSpec validation and diff checks passed.
## Task 10.3 Completion
- Focused unit, browser, packaged-worker, edge, PostgreSQL, and formal
verification gates completed without a remaining failure.
- The PostgreSQL fixture now supports an explicitly selected POSIX PostgreSQL
binary directory while preserving its existing bundled-Windows default.
- Real PostgreSQL execution exposed and closed one exact local-admission
recovery identity omission plus stale protocol-2 fixture limits and bundle
path evidence.
## Task 10.3 Verification
- The real PostgreSQL 16 integration suite passed all 60 tests as UID 10001 in
a read-only, no-network, capability-free container. Native Windows collected
the same 60 tests and skipped them because the optional bundled PostgreSQL is
absent.
- Packaged Windows and Linux worker verification passed after rebuilding the
protocol-2 artifacts. The real edge E2E passed with 28 baseline responses,
the exact core source profile, authenticated Caddy routes, and fail2ban
restart, unban, and expiry evidence.
- Browser coverage passed. The focused task-10.1 matrix passed 401 tests with
eleven platform skips, and the operations-control suite passed on Windows
and WSL.
- Container selection is AST-clean with 54 modules and 1059 definitions.
Strict OpenSpec validation and diff checks passed.
## Task 10.4 Completion
- The production cutover used `shared-host-edge-v1` while both explicit gates
were paused and drain was authoritative with zero blockers. Root-owned profile,
package, Compose projection, loopback edge, runtime, admin, agent, Caddy, and
X-UI service checks passed without granting the Truf lifecycle authority any
control over shared-host services.
- Reconciled restart operation `08dc7e8b-551f-5c64-b803-de905f040420`
completed `succeeded/succeeded` with no safe failure category, healthy runtime
and edge, and no failed hold.
- Reconciled rollback operation `6d09ea58-2c48-5366-b5ee-951c341fcd6f`
deliberately applied a valid candidate with Worker API disabled. Strict
forward health classified the failure as `health_check_failed`; the agent
restored the byte-identical original config and secrets exactly once and
completed `rolled_back/rolled_back` with no failed hold.
- Audited, revision-checked operations then canceled drain, resumed discovery,
and resumed dispatch in that order. Controls advanced from revision 19 to 22
and are now discovery open, dispatch open, and drain `normal`.
- After reopening, the active production worker contacted the server and
received new DockerHub work. Lifecycle preflight, strict runtime health, and
host-agent/Caddy/X-UI service checks remained healthy.
## Production Architecture Corrections
- Real candidate validation found that private writable runtime configuration
could not also be the root-owned immutable package authority. Worker package
manifests now use the separate read-only `/data/worker-packages` authority,
while initialization, lock, and PostgreSQL password state use private files
directly beneath `/data`.
- PostgreSQL maintenance and offline migration use the discovery-only server
authority profile and therefore do not require the worker-only TruffleHog
executable. The offline cutover reconciled two stale leases, migrated the
production database, and established audited discovery and dispatch pauses.
- Production validation also corrected Worker API HTTP concurrency from one to
eight while retaining the real assignment cap of one.
- DockerHub discovery now configures account metadata without creating scanner
Docker directories or requiring scanner runtime initialization. Scanner
execution keeps the original full-runtime guard and Docker config behavior.
## Task 10.4 Production Evidence
- The active thin-v6 runtime image is
`sha256:dd55500ee2f947c0088a57061d1689025653fc0b08ed7fb22baea6816fb93270`
under tag `truf-local:runtime`; the managed edge image is
`sha256:6ec35cae4f4cf4bcd2bb1fe427b9cd2f16f8b5c2577442eae0941d806d362529`
under tag `truf-local:edge`.
- Canonical health reports Supervisor `ACTIVE`, PostgreSQL `READY`, and healthy
DockerHub, GitLab, HuggingFace, janitor, projector, ingester, and Worker API
processes. Lifecycle attestation confirms host-network runtime with no
published ports and a loopback-only edge.
- Root-owned protocol-2 Windows and Linux manifests advertise only GitLab,
DockerHub, and HuggingFace. Candidate initialization passed against the real
production volume before cutover.
- Public checks after rollback returned 401 for an invalid Worker API token,
401 for the unauthenticated protected admin route, and 404 for an unrelated
Truf path. Host Caddy, X-UI, and the Truf host agent remained active.
- Shared-host restart validation exposed a real Docker Compose identity nuance:
`network_mode: service:runtime` changes the edge Compose config hash whenever
runtime is recreated. Lifecycle now attests all fixed edge metadata before
observing and pinning each forward or rollback edge hash; it never weakens the
fixed topology checks.
- Strict runtime-health command failures are normalized to lifecycle health
failures. This made the intentional rollback drill reconcile with the exact
`health_check_failed` safe category rather than generic `apply_failed`.
- Root-only evidence for two diagnostic failed holds remains preserved under
`/opt/truf-remote-server/staging/failed-hold-recovery-20260922` and
`/opt/truf-remote-server/staging/failed-hold-recovery-v2-20260922`. Both holds
were cleared only after strict health, identity, and evidence checks.
- The exact pre-v4 unit, previous runtime image and configuration, verified
custom-format PostgreSQL backup, failed-hold evidence, and reconciled operation
results remain available for rollback and audit. No X-UI or unrelated host
service was modified.
### Task 10.4 Verification
- The final focused host-agent matrix passed 107 tests with fourteen platform
skips. Edge deployment and container-runtime pytest coverage passed 221 tests
with one platform skip.
- Runtime-document coverage passed 22 tests and worker-package coverage passed
15 tests. Changed lifecycle and drill scripts compile successfully.
- Both standalone base+edge and shared-host base+edge Compose projections passed
`config --quiet` with non-secret synthetic validation values; required edge
variables still fail closed when omitted.
- Strict OpenSpec validation and diff checks passed after production evidence
was reconciled and controls were reopened.
## Task 10.5 Production Evidence
- A single immutable DockerHub digest was processed independently by the
packaged Windows worker and the hardened WSL worker with assignment caps and
parallelism fixed at one. Both reservations produced accepted protocol-2
bundles and acknowledged ingestion/projection records.
- A completed assignment replay returned the original accepted receipt without
rescanning. A separate claim-only assignment expired at its real wall-clock
deadline, refunded queue capacity and attempts, and replayed the same durable
expiry receipt over the public Worker API.
- An audited drain advanced from `draining` to `drained` with zero live remote
assignments and zero pre-commit bundles. Audited cancel restored `normal`
while both explicit gates remained paused.
- Temporary canary devices were revoked, their token file was overwritten and
removed, the normal 7200-second assignment TTL was restored, and no unresolved
canary assignment remained.
## Task 10.6 Production Evidence
- Only after the DockerHub canary gates passed, audited operations enabled
production discovery and dispatch and created one production WSL worker user
and device with an active assignment cap of one.
- Real production cycles completed successfully for GitLab with `gl_1`, public
HuggingFace with `hf_1`, and DockerHub with its real account pool. The final
DockerHub cycle reported eleven available accounts and exited with code zero.
- The active server and both protocol-2 compatibility profiles contain exactly
`gitlab`, `dockerhub`, and `huggingface`; GitHub remains excluded.
- Current controls are revision 22 with discovery and dispatch open and drain
state `normal`. The production WSL worker is running detached with
parallelism one; its audited user is enabled, its device is not revoked, and
it contacted the server after the task-10.4 rollback gates reopened.
- Rollback evidence retains the previous image, exact units, previous active
configuration, and verified database dump. The v4 unit and image identities
were checked before and after activation.
## Managed Files Production Extension (2026-09-22)
- Production Files now exposes exactly three configured logical roots:
`runtime-logs`, `runtime-keychecks`, and `runtime-results`. All are list/read
only. Logs and keychecks retain the 64 MiB per-file limit; only the fixed
result root permits 256 MiB files.
- The result root exposes only active `scan_results.jsonl` and
`found_secrets.jsonl` projections and their exact six-digit generations.
Internal locks, databases, ledgers, scan errors, malformed generations,
recovery files, and temporary/quarantine directories remain unavailable.
- The deployment paused discovery and dispatch and allowed the existing remote
DockerHub assignment to resolve naturally. Drain reached `drained` with zero
blockers before any runtime or configuration activation; no assignment was
cancelled.
- Runtime image `sha256:3b4d6e19e29e85a32b75d64265d75e71100d3f7f00221d21de544256dadd25cf`
was activated with a fail-safe Compose transition. The preceding image is
retained as `truf-local:runtime-pre-managed-files-v1`.
- Official restart operation `1978f770-c4a5-537d-b474-c3c9428cb79c`
completed `succeeded/succeeded`, reconciled without a safe category or failed
hold, and proved the full fixed lifecycle on the new immutable image before
the root configuration changed.
- Official Preview/Save operation `87b78ff6-d8eb-5fb4-a897-83b52c3697e5`
changed only the managed-root mapping. Apply operation
`7a572971-08d2-5e51-be78-4fb7039a93d4` completed
`succeeded/succeeded`, reconciled without rollback or failed hold. The active
configuration identity is
`e48797689ab0ee7328d21cf5c23d0ffedb979dba492bf49d1da3afe4b075cf5b`.
- Live runtime traversal verified all three exact policies, an empty keycheck
listing, two allowlisted active result projections, stable snapshot
length/hash, one-snapshot serialization, permit reuse, safe rejection of an
internal result artifact, and denied result mutation.
- Live Worker API HTML rendered all three root IDs and the read-only keycheck
and result pages. A permitted 146366-byte projection download matched its
exact source SHA-256 and length through the streaming route; an internal
result name returned 404. No file contents were emitted during validation.
- Final audited controls are revision 56 with discovery and dispatch open and
drain `normal`. Canonical Worker API plus discovery-producer strict health and
lifecycle preflight pass; failed hold is absent; host agent, host Caddy, and
X-UI are active. Public checks remain 401 for invalid Worker authentication,
401 for unauthenticated admin access, and 404 for an unrelated path.