12 KiB
ADDED Requirements
Requirement: Shared strict document validation
Configuration and secrets preview, startup, and privileged apply SHALL use the same side-effect-free validator for bounded UTF-8 YAML, duplicate keys, mapping roots, strict scalar types and bounds, known keys, exact core profile, auth-pool schemas, unique entry names, reference integrity, package capabilities, and deployment paths.
Scenario: Valid configuration is previewed
- WHEN an operator submits a candidate satisfying the complete schema
- THEN preview returns normalized validation success and a bounded diff without activating the candidate
Scenario: Duplicate or unknown key is submitted
- WHEN a candidate contains a duplicate mapping key or unsupported field
- THEN validation fails before staging or restart and identifies the field without echoing secret values
Scenario: Secret reference is invalid
- WHEN configuration selects an auth-pool entry that does not exist in the candidate secrets
- THEN combined validation fails and neither document becomes active
Requirement: Separate immutable package authority
Worker package manifests SHALL resolve only beneath a separate root-owned, non-writable package authority, while runtime-generated initialization state, locks, and PostgreSQL credentials SHALL remain in the private writable data volume rather than the active config/secrets bind.
Scenario: Package manifest uses the active-document directory
- WHEN configuration references a package manifest beneath the config/secrets authority or outside the fixed package root
- THEN validation fails before startup or privileged apply
Scenario: Runtime initializes with active documents mounted read-only
- WHEN the runtime initializes or restarts with the active config/secrets directory mounted read-only
- THEN its initialization marker, singleton lock, and generated PostgreSQL password remain writable only through fixed private data paths
Requirement: Candidate revisions and compare-and-swap
The system SHALL stage validated candidate revisions separately from active files and SHALL require expected active and candidate SHA-256 identities when saving or applying them.
Scenario: Candidate is saved
- WHEN an operator saves valid bytes against the current active hash
- THEN the candidate is durably staged with a new hash and the active file is unchanged
Scenario: Active file changed through SSH
- WHEN the active hash differs from the expected hash submitted by a stale page
- THEN save or apply fails without replacing either active file
Scenario: Candidate changed concurrently
- WHEN the supplied candidate hash is no longer current
- THEN apply is rejected before host lifecycle changes begin
Requirement: Plaintext secrets editing without persistence leakage
The protected secrets page SHALL allow authorized operators to view and edit the complete plaintext YAML document while responses remain no-store and secret values remain absent from browser persistence, application logs, diffs outside that page, operation status, and audit records.
Scenario: Operator opens secrets page
- WHEN an authenticated operator requests the dedicated secrets editor
- THEN the current document is rendered in a server-side form over the protected no-store response
Scenario: Secrets candidate is validated
- WHEN an operator previews or saves changed secrets
- THEN validation results name safe field paths and hashes but do not repeat credential values
Scenario: Operator leaves the page
- WHEN the browser navigates to another admin page
- THEN the application has written no secret value to local storage, session storage, service workers, or client-side application state
Requirement: Closed host-agent protocol
The privileged host operations agent SHALL accept only a canonical operation UUID, one fixed action enum, expected active hashes, and expected candidate hashes from the authorized runtime peer over a local Unix socket. Deployment profile is root-installed host policy and SHALL NOT be added to that request.
Scenario: Valid apply request arrives
- WHEN the authorized runtime UID submits an exact valid request
- THEN the agent verifies the persisted operation and hashes before acquiring the singleton apply lock
Scenario: Request includes path or command data
- WHEN a request includes a service name, path, shell text, environment, Docker argument, or unknown field
- THEN the agent rejects it before any privileged action
Scenario: Request attempts topology selection
- WHEN a request includes a profile, ingress port, upstream, Caddy path, unit, service, command, environment, Compose file, or Compose argument
- THEN the agent rejects it before reading candidate documents or stopping the deployment
Scenario: Unauthorized local peer connects
- WHEN a process with an unapproved peer identity uses the socket
- THEN the agent rejects the request regardless of its JSON body
Requirement: Coordinated apply with health verification
For config, secrets, or combined apply, the host agent SHALL revalidate fixed candidate files, verify compare-and-swap hashes, create byte-identical backups, stop the fixed Truf deployment, atomically replace active files, recreate the exact root-selected runtime and edge profile, attest its fixed topology, and wait for required health before reporting success. In shared-host mode it SHALL NOT stop, restart, reload, reconfigure, or remove host Caddy, X-UI, or another unrelated service.
Scenario: Combined apply succeeds
- WHEN both candidates validate and the restarted deployment reaches Supervisor ACTIVE, PostgreSQL READY, Worker API, ingester, and projector health
- THEN the operation completes successfully with resulting hashes and retained rollback evidence
Scenario: Validation changes between preview and apply
- WHEN host-side revalidation or hash verification differs from the accepted candidate operation
- THEN the agent aborts before stopping the healthy runtime
Scenario: Apply is requested while another is active
- WHEN the singleton operation lock is held
- THEN the second request is rejected or remains queued without overlapping lifecycle mutations
Scenario: Shared-host profile is healthy
- WHEN shared-host apply recreates a host-network runtime with no published ports and an edge bound only to fixed loopback, and all runtime and edge health checks pass
- THEN the operation succeeds without a host-Caddy or X-UI lifecycle action
Scenario: Shared-host topology drifts
- WHEN runtime publishes a port, edge binds a non-loopback address, profile metadata changes, or Compose labels, mounts, network, capabilities, or Caddyfile differ from the fixed profile
- THEN the agent rejects the deployment before candidate replacement or reports failed health without claiming success
Scenario: Shared-host rollback succeeds
- WHEN candidate health fails and the previous Truf runtime and edge are restored
- THEN rollback completes exactly once without changing host Caddy, X-UI, or another unrelated service
Requirement: Automatic rollback and failed hold
If the new deployment fails its bounded health check, the agent SHALL restore byte-identical backups and verify the previous deployment; if rollback also fails, it SHALL stop retrying and retain a failed-hold state and all evidence for SSH recovery.
Scenario: New configuration fails startup
- WHEN the restarted runtime cannot reach required health within the deadline
- THEN the agent restores the prior active files and restarts the previous deployment
Scenario: Rollback succeeds
- WHEN the restored deployment reaches required health
- THEN the operation records rolled-back status and safe failure category without claiming apply success
Scenario: Rollback fails
- WHEN neither the candidate nor restored deployment becomes healthy
- THEN the agent enters failed hold, performs no replacement loop or forced authority release, and preserves backups and diagnostics
Requirement: Logical managed roots
The generic file page SHALL address only configured logical roots with explicit read, create/replace, and delete permissions, and SHALL never accept an absolute root from a client.
Scenario: Operator lists a managed runtime root
- WHEN a valid logical root ID for logs, keycheck projections, or result projections and a canonical relative directory are requested
- THEN the service returns a bounded read-only listing of permitted regular files and directories under that root
Scenario: Operator downloads rotated result projections
- WHEN the operator requests a permitted active or rotated result projection within its configured file-size bound
- THEN the service returns that regular single-link file without granting mutation access or exposing the backing runtime path
Scenario: Result projection names are allowlisted
- WHEN the result-projection root is listed or read
- THEN only active
scan_results.jsonlandfound_secrets.jsonlfiles and their exact six-digit generation names are visible, while locks, databases, ledgers, scan errors, temporary/quarantine directories, malformed generations, and recovery artifacts remain unavailable
Scenario: Large result projection is downloaded
- WHEN an allowed result projection is within the larger result-root byte bound
- THEN the service copies and hashes an unchanged source revision into an anonymous same-volume snapshot using bounded chunks, permits only one such snapshot at a time, streams the snapshot with bounded memory, and releases the snapshot and concurrency slot after response completion or failure
Scenario: Operator requests excluded storage
- WHEN a request targets application code, config/secrets through the generic page, PostgreSQL storage, sockets, host-agent metadata, raw result bundles, or an unknown root
- THEN the service rejects it without revealing host paths or existence details
Requirement: Descriptor-safe path containment
Managed-file traversal and mutation SHALL use a retained root directory descriptor, component-wise no-follow operations, canonical relative components, and regular single-link file checks.
Scenario: Relative traversal is attempted
- WHEN a path contains an empty, dot, dot-dot, absolute, drive-qualified, backslash, NUL, over-depth, or over-length component
- THEN the request is rejected before filesystem access outside the retained root
Scenario: Symlink is swapped during access
- WHEN a path component becomes a symlink between validation and open
- THEN no-follow descriptor traversal fails without accessing the link target
Scenario: Hardlink or special file is targeted
- WHEN the final object is multi-linked or is not a regular file
- THEN view, download, replace, and delete are rejected
Requirement: Durable bounded file mutation
An allowed managed-file create or replace SHALL use an exclusive same-directory temporary regular file, bounded bytes, fsync, atomic descriptor-relative replacement, directory fsync, and final ownership/type/mode verification.
Scenario: File replacement succeeds
- WHEN an authorized bounded replacement is submitted against the current file hash
- THEN readers observe either the complete old file or complete new file and audit records the safe before/after hashes
Scenario: File is concurrently changed
- WHEN the current file hash differs from the expected hash
- THEN replacement fails without overwriting the concurrent change
Scenario: Upload exceeds the root limit
- WHEN submitted bytes exceed the configured bounded file size
- THEN the service rejects and removes temporary data without changing the target
Requirement: Content-free operational audit
Configuration, secrets, and managed-file operations SHALL record actor, typed action, logical target, timestamps, result, safe category, byte counts where applicable, and before/after hashes, but SHALL NOT record file contents or credentials.
Scenario: Managed file is deleted
- WHEN an allowed delete succeeds against the expected hash
- THEN audit records the logical root/path and previous hash without retaining deleted content
Scenario: Secrets apply fails
- WHEN a secrets operation fails validation, startup, or rollback
- THEN status and audit expose only the bounded failure category and document hashes