Add named config, hosting layouts and credential lifecycle to allod site #167

Merged
vnprc merged 18 commits from agent/site-config-path into master 2026-09-09 20:42:17 +01:00
Member

allod site gains three things at once: a --config <path> option that selects one explicit rclone configuration, a compile-time hosting layout chosen by the deployment package, and credential lifecycle commands (check, config show, config update, config replace).
Named configs let activation-managed hosting credentials survive machine recreation; deploy and setup read and write the same path, and rotation during a build may hand sync newer contents.
The hosting layout is fixed when the binary is built: directadmin by default, public-html when the deployment sets a linker flag. No flag, environment variable, or site.toml key can move the destructive sync target.
The lifecycle commands verify a credential against the server without building or publishing, show its safe fields, update one field in place, or replace all three; a rejected FTP login reports username-or-password because the protocol cannot say which.
Without --config, without the linker flag, and for existing deploy and config invocations, behavior and generated rclone arguments are unchanged; config --force still works as the compatibility spelling of replace.
Sabotage tests cover option swallowing, unsafe terminal text, rotation, FIFOs, symlinks, unreadable files, a sabotaged layout table, and duplicate credential stanzas; the full tagged and untagged Go matrix and nix flake check pass on the merged tree.
Rollback is to omit --config, drop the linker flag and rebuild, and keep using the existing per-user rclone configuration, which remains valid.

This branch carries the work of three stacked PRs, now targeting master alone: #167 (named config path), #168 (deployment-selected hosting layout, merged into this branch) and #169 (credential lifecycle, merged into #168's branch). The review findings raised on all three are carried unchanged. Master's copy of #166 is byte-identical to the base this branch was built on, so the catch-up merge took the branch side of the four cmd/allod/site*.go files and master's side of everything else.

Closes allod/tools#164
Closes allod/tools#165
Closes allod/tools#163

Issues #163 and #165 already show as closed because their PRs merged into this stack's branches rather than into master; their code lands only when this PR merges.

Contract

Named config path

  • --config <path> and --config=<path> work on allod site deploy, allod site check, and every allod site config form, in any order with their other options. A separated value beginning with - is rejected so it cannot swallow another option; use --config=<path> for a relative path that itself begins with a dash. An explicit path overrides RCLONE_CONFIG, rclone's reported path, and the XDG/HOME fallback.
  • Read-only access is nonblocking, requires a regular opened descriptor, and follows a final symlink so activation-managed credentials work. Mutating access refuses final symlinks, FIFOs, directories, devices, and sockets before prompting; the atomic writer checks the destination type again before rename.
  • Every config value that reaches the terminal must be valid UTF-8 with no Unicode control or line/paragraph separator; unsafe values cause no prompt, command, write, or raw output.
  • Failure remedies carry the selected Config: path so no message recommends a default-config command for a named config.

Deployment-selected hosting layout

  • A site-enabled binary has exactly one hosting profile compiled in. siteHostingProfileName defaults to directadmin; a deployment selects public-html with ldflags = [ "-X main.siteHostingProfileName=public-html" ] in its buildGoModule.
  • public-html targets shared:public_html/<domain> with no filter file. directadmin keeps the exact prior destination, exclusion filter, and rclone argv.
  • The built-in table is validated before any effect: unique valid names, exactly one <domain> placeholder, a safe relative docroot with no empty, ., .., or control segments, and single-line exclusion-only filter rules. An invalid compiled value or table fails before credential checking, building, or syncing.

Credential lifecycle

  • allod site check [--config <path>] reuses the deploy preflight's read-only lsd shared: probe and failure reporter; it never resolves a site repository, builds, syncs, verifies HTTPS, or publishes.
  • allod site config show prints Config, Remote, Type, Host, and User only, rejects missing or duplicate required keys, non-FTP remotes, and configurations without explicit TLS, and never prints the password field.
  • allod site config update {host|user|password} takes no value in argv, asks one terminal question, and replaces only that field's byte range. Both rclone key = value and key: value separators work; other bytes, directives, line endings, and stanzas survive unchanged. Update requires an existing shared stanza.
  • allod site config replace is the discoverable full replacement; allod site config --force remains its compatibility spelling; bare allod site config keeps create-and-refuse-existing behavior. Replacement refuses duplicate shared stanzas before prompting or writing.
  • A credential rejected through a symlink, explicit or via rclone's default resolution, directs the owner to its source credential rather than an impossible mutation of the symlink.

Risk

  • Deploy reopens the selected path for preflight and for sync, with the site build between them. A rotation in that interval changes what sync reads; this is documented and tested, not presented as snapshot identity. Another same-account process could still replace the destination between the writer's final type check and rename, but that process already holds the credential and this CLI is not a same-user security boundary.
  • The compiled profile changes a destructive sync destination. Only validated built-in profiles are accepted, and public-html's empty exclusion list assumes ACME is served elsewhere.
  • Lifecycle commands write a machine-wide, plaintext-equivalent rclone configuration. Writes stay atomic at mode 0600 and the password reaches rclone only on stdin.

Validation

Run on the merged tree at the catch-up merge commit:

  • gofmt -l clean on cmd and internal
  • nix develop -c bash -c 'go vet ./... && go test -count=1 ./... && go vet -tags site ./... && go test -count=1 -tags site ./...'
  • nix build .#checks.x86_64-linux.go-checks .#checks.x86_64-linux.allod-parity --no-link, with build logs showing both Go matrices and all 704 parity tests passing
  • nix flake check passes in full. The allod-parity expectation failure that the three PRs previously had to disclose is fixed on master.
  • Focused sabotage tests: separated --config swallowing an option, invalid UTF-8, U+0085, U+2028 and U+2029 at every boundary, credential rotation between preflight and sync, FIFO, symlink, non-regular, missing and unreadable config paths, an invalid compiled layout and a sabotaged layout table observing zero remote, build, sync and verification calls, duplicate shared stanzas through both replace spellings, and mixed =/: byte-preserving updates.
`allod site` gains three things at once: a `--config <path>` option that selects one explicit rclone configuration, a compile-time hosting layout chosen by the deployment package, and credential lifecycle commands (`check`, `config show`, `config update`, `config replace`). Named configs let activation-managed hosting credentials survive machine recreation; deploy and setup read and write the same path, and rotation during a build may hand sync newer contents. The hosting layout is fixed when the binary is built: `directadmin` by default, `public-html` when the deployment sets a linker flag. No flag, environment variable, or `site.toml` key can move the destructive sync target. The lifecycle commands verify a credential against the server without building or publishing, show its safe fields, update one field in place, or replace all three; a rejected FTP login reports username-or-password because the protocol cannot say which. Without `--config`, without the linker flag, and for existing `deploy` and `config` invocations, behavior and generated rclone arguments are unchanged; `config --force` still works as the compatibility spelling of `replace`. Sabotage tests cover option swallowing, unsafe terminal text, rotation, FIFOs, symlinks, unreadable files, a sabotaged layout table, and duplicate credential stanzas; the full tagged and untagged Go matrix and `nix flake check` pass on the merged tree. Rollback is to omit `--config`, drop the linker flag and rebuild, and keep using the existing per-user rclone configuration, which remains valid. This branch carries the work of three stacked PRs, now targeting master alone: #167 (named config path), #168 (deployment-selected hosting layout, merged into this branch) and #169 (credential lifecycle, merged into #168's branch). The review findings raised on all three are carried unchanged. Master's copy of #166 is byte-identical to the base this branch was built on, so the catch-up merge took the branch side of the four `cmd/allod/site*.go` files and master's side of everything else. Closes allod/tools#164 Closes allod/tools#165 Closes allod/tools#163 Issues #163 and #165 already show as closed because their PRs merged into this stack's branches rather than into master; their code lands only when this PR merges. ## Contract ### Named config path - `--config <path>` and `--config=<path>` work on `allod site deploy`, `allod site check`, and every `allod site config` form, in any order with their other options. A separated value beginning with `-` is rejected so it cannot swallow another option; use `--config=<path>` for a relative path that itself begins with a dash. An explicit path overrides `RCLONE_CONFIG`, rclone's reported path, and the XDG/HOME fallback. - Read-only access is nonblocking, requires a regular opened descriptor, and follows a final symlink so activation-managed credentials work. Mutating access refuses final symlinks, FIFOs, directories, devices, and sockets before prompting; the atomic writer checks the destination type again before rename. - Every config value that reaches the terminal must be valid UTF-8 with no Unicode control or line/paragraph separator; unsafe values cause no prompt, command, write, or raw output. - Failure remedies carry the selected `Config:` path so no message recommends a default-config command for a named config. ### Deployment-selected hosting layout - A site-enabled binary has exactly one hosting profile compiled in. `siteHostingProfileName` defaults to `directadmin`; a deployment selects `public-html` with `ldflags = [ "-X main.siteHostingProfileName=public-html" ]` in its `buildGoModule`. - `public-html` targets `shared:public_html/<domain>` with no filter file. `directadmin` keeps the exact prior destination, exclusion filter, and rclone argv. - The built-in table is validated before any effect: unique valid names, exactly one `<domain>` placeholder, a safe relative docroot with no empty, `.`, `..`, or control segments, and single-line exclusion-only filter rules. An invalid compiled value or table fails before credential checking, building, or syncing. ### Credential lifecycle - `allod site check [--config <path>]` reuses the deploy preflight's read-only `lsd shared:` probe and failure reporter; it never resolves a site repository, builds, syncs, verifies HTTPS, or publishes. - `allod site config show` prints Config, Remote, Type, Host, and User only, rejects missing or duplicate required keys, non-FTP remotes, and configurations without explicit TLS, and never prints the password field. - `allod site config update {host|user|password}` takes no value in argv, asks one terminal question, and replaces only that field's byte range. Both rclone `key = value` and `key: value` separators work; other bytes, directives, line endings, and stanzas survive unchanged. Update requires an existing `shared` stanza. - `allod site config replace` is the discoverable full replacement; `allod site config --force` remains its compatibility spelling; bare `allod site config` keeps create-and-refuse-existing behavior. Replacement refuses duplicate `shared` stanzas before prompting or writing. - A credential rejected through a symlink, explicit or via rclone's default resolution, directs the owner to its source credential rather than an impossible mutation of the symlink. ## Risk - Deploy reopens the selected path for preflight and for sync, with the site build between them. A rotation in that interval changes what sync reads; this is documented and tested, not presented as snapshot identity. Another same-account process could still replace the destination between the writer's final type check and rename, but that process already holds the credential and this CLI is not a same-user security boundary. - The compiled profile changes a destructive sync destination. Only validated built-in profiles are accepted, and `public-html`'s empty exclusion list assumes ACME is served elsewhere. - Lifecycle commands write a machine-wide, plaintext-equivalent rclone configuration. Writes stay atomic at mode 0600 and the password reaches rclone only on stdin. ## Validation Run on the merged tree at the catch-up merge commit: - `gofmt -l` clean on `cmd` and `internal` - `nix develop -c bash -c 'go vet ./... && go test -count=1 ./... && go vet -tags site ./... && go test -count=1 -tags site ./...'` - `nix build .#checks.x86_64-linux.go-checks .#checks.x86_64-linux.allod-parity --no-link`, with build logs showing both Go matrices and all 704 parity tests passing - `nix flake check` passes in full. The `allod-parity` expectation failure that the three PRs previously had to disclose is fixed on master. - Focused sabotage tests: separated `--config` swallowing an option, invalid UTF-8, U+0085, U+2028 and U+2029 at every boundary, credential rotation between preflight and sync, FIFO, symlink, non-regular, missing and unreadable config paths, an invalid compiled layout and a sabotaged layout table observing zero remote, build, sync and verification calls, duplicate `shared` stanzas through both replace spellings, and mixed `=`/`:` byte-preserving updates.
Author
Member

Independent review found three code/interface issues and one claim that needs narrowing. Please address these additively before merge; the full tagged/untagged Go test and vet suites otherwise pass.

  • P1: separated --config accepts another option as its value. site config --config --force can write a credential to a file named --force, while deploy --config --dry-run can swallow the dry-run flag. Reject option-looking separated values and keep --config=<path> as the spelling for a leading-dash path.
  • P2: named-config failures currently recommend default-config commands. Propagate #166’s path-aware reporter so every remedy preserves the selected Config path.
  • P2: deploy reopens the named path for preflight and sync, with a build between them. The PR must not imply it pins one file version; either establish snapshot semantics or state that it uses the same path and that rotation/replacement during a deploy can change what sync reads.
  • P2: mutating site config can block on a FIFO and atomically rename over a symlink. Define and enforce file-type semantics before lifecycle commands build on this: regular files are mutable, non-regular files fail before prompting, and activation-managed symlinks should be read-only rather than silently severed.
Independent review found three code/interface issues and one claim that needs narrowing. Please address these additively before merge; the full tagged/untagged Go test and vet suites otherwise pass. - P1: separated `--config` accepts another option as its value. `site config --config --force` can write a credential to a file named `--force`, while `deploy --config --dry-run` can swallow the dry-run flag. Reject option-looking separated values and keep `--config=<path>` as the spelling for a leading-dash path. - P2: named-config failures currently recommend default-config commands. Propagate #166’s path-aware reporter so every remedy preserves the selected Config path. - P2: deploy reopens the named path for preflight and sync, with a build between them. The PR must not imply it pins one file version; either establish snapshot semantics or state that it uses the same path and that rotation/replacement during a deploy can change what sync reads. - P2: mutating `site config` can block on a FIFO and atomically rename over a symlink. Define and enforce file-type semantics before lifecycle commands build on this: regular files are mutable, non-regular files fail before prompting, and activation-managed symlinks should be read-only rather than silently severed.
Author
Member

Addressed additively in a6deff1 after merging the updated preflight base at b705cb0.

  • Separated --config now rejects an option-looking value; --config=<path> remains the explicit escape for a leading-dash path.
  • The base's path-aware reporter now carries the selected config through missing-remote, unreadable, rejected-credential, and unknown-check remedies.
  • Usage and the PR body now promise one path, not one content snapshot. A rotation test proves sync can read a newer generation after the build.
  • Read-only access uses a nonblocking open plus descriptor stat, follows symlinks to regular activation targets, and rejects FIFOs and other non-regular targets.
  • Mutating config access uses O_NOFOLLOW, refuses symlinks/non-regular files before prompting, and rechecks the destination before atomic rename.

The full tagged and untagged Go test and vet matrix passes. The PR body records the prior unrelated nix flake check failure precisely and does not claim that gate passed.

Addressed additively in `a6deff1` after merging the updated preflight base at `b705cb0`. - Separated `--config` now rejects an option-looking value; `--config=<path>` remains the explicit escape for a leading-dash path. - The base's path-aware reporter now carries the selected config through missing-remote, unreadable, rejected-credential, and unknown-check remedies. - Usage and the PR body now promise one path, not one content snapshot. A rotation test proves sync can read a newer generation after the build. - Read-only access uses a nonblocking open plus descriptor stat, follows symlinks to regular activation targets, and rejects FIFOs and other non-regular targets. - Mutating config access uses `O_NOFOLLOW`, refuses symlinks/non-regular files before prompting, and rechecks the destination before atomic rename. The full tagged and untagged Go test and vet matrix passes. The PR body records the prior unrelated `nix flake check` failure precisely and does not claim that gate passed.
Author
Member

Scoped stability review confirms the four prior findings are resolved and found one remaining low-severity output-safety mismatch.

  • P3: --config promises a printable single-line path but currently reuses validConfigValue, which rejects only ASCII C0/DEL. Invalid UTF-8, C1 controls, and U+2028/U+2029 can survive validation and are later printed raw in Config: and error lines. Strengthen the shared printable-value check (or consistently quote path output) and add sabotage cases for invalid UTF-8, a C1 control, and Unicode line/paragraph separators.
Scoped stability review confirms the four prior findings are resolved and found one remaining low-severity output-safety mismatch. - P3: `--config` promises a printable single-line path but currently reuses `validConfigValue`, which rejects only ASCII C0/DEL. Invalid UTF-8, C1 controls, and U+2028/U+2029 can survive validation and are later printed raw in `Config:` and error lines. Strengthen the shared printable-value check (or consistently quote path output) and add sabotage cases for invalid UTF-8, a C1 control, and Unicode line/paragraph separators.
Author
Member

Addressed additively in 4c212e2.

  • validConfigValue now requires valid UTF-8 and rejects every Unicode control plus U+2028/U+2029, while keeping ordinary printable Unicode valid.
  • The same validator still governs config paths, hosts, users, and rclone's obscured password output.
  • Obscured output now trims only CR/LF framing, so a C1 control or Unicode separator cannot disappear before validation.
  • Exact sabotage cases cover invalid UTF-8, U+0085, U+2028, and U+2029 at the option boundary, host/user boundary, and obscured-output boundary. Unsafe paths cause no prompt or command, unsafe values cause no config write, and hostile bytes never reach output.

Focused tests and the full tagged/untagged Go test and vet matrix pass. The PR Validation section now names this coverage.

Addressed additively in `4c212e2`. - `validConfigValue` now requires valid UTF-8 and rejects every Unicode control plus U+2028/U+2029, while keeping ordinary printable Unicode valid. - The same validator still governs config paths, hosts, users, and rclone's obscured password output. - Obscured output now trims only CR/LF framing, so a C1 control or Unicode separator cannot disappear before validation. - Exact sabotage cases cover invalid UTF-8, U+0085, U+2028, and U+2029 at the option boundary, host/user boundary, and obscured-output boundary. Unsafe paths cause no prompt or command, unsafe values cause no config write, and hostile bytes never reach output. Focused tests and the full tagged/untagged Go test and vet matrix pass. The PR Validation section now names this coverage.
vnprc changed target branch from agent/site-auth-preflight to master 2026-09-09 20:11:53 +01:00
vnprc approved these changes 2026-09-09 20:13:22 +01:00
Dismissed
Contributor

Owner asked for this branch to be rebased after #166 merged to master and #168 and #169 merged down into this branch. Recording what the rebase found, and why it is not pushed here yet.

This branch now carries three PRs, not one. #169 merged into agent/hosting-layout-profile and #168 merged into this branch, so agent/site-config-path holds the work of #167, #168 and #169 and targets master alone. The title and body still describe only the named-config-path change.

A literal rebase is not available. The forge refuses it: force-push is not permitted on branch 'agent/site-config-path'. Add commits instead. Replaying the eleven commits is also not a mechanical operation, because each PR in the stack was written against a different base, #168 branched from cf384e8 before either #166 or #167 existed, and the integration merges that reconciled them are exactly what a linear rebase discards. Attempting it stops on the first commit with conflicts in site.go and site_test.go.

The equivalent merge is deterministic and was verified. Master carries #166 under different hashes from the forge's rebase-merge, and git diff 4215e60 b705cb0 across all four site sources is empty, so master's copy of #166 is byte-identical to this branch's. That makes the resolution mechanical: the four conflicted files take this branch's version, which is that same #166 base plus the #167, #168 and #169 work, and every other file takes master's side. README.md auto-merges, keeping both sides.

The result was checked against an independently constructed clean rebase of this branch onto master, built as three tree snapshots one per originating PR. The two trees are identical, and both are byte-identical to the reviewed tip a61b3df across site.go, site_config.go, site_test.go and site_config_test.go. No new code was written; the review findings raised on all three PRs are carried unchanged.

Validation on the merged tree. gofmt clean, go vet and go test pass both tagged and untagged, nix build .#checks.x86_64-linux.go-checks .#packages.x86_64-linux.allod builds, and nix flake check now passes in full. The allod-parity baseline red that all four PRs had to disclose is gone: master's f7c57b8 fixed the section-prompt expectation.

Why it is not pushed. A vnprc agent has no write access to allod/*; the push is refused by the pre-receive hook. allod patch receive cannot carry the merge either, because it exports with format-patch and refuses non-linear history. So this needs an agent on allod-dev, or the owner running the merge directly. The exact resolution is above and reproduces in four commands.

One issue-state defect worth a look. allod/tools#163 and allod/tools#165 are already marked closed. They closed when their PRs merged into this stack's branches rather than into master, so their code is still unlanded and is in this PR. If this PR were abandoned, both would be wrongly closed.

Owner asked for this branch to be rebased after `#166` merged to master and `#168` and `#169` merged down into this branch. Recording what the rebase found, and why it is not pushed here yet. **This branch now carries three PRs, not one.** `#169` merged into `agent/hosting-layout-profile` and `#168` merged into this branch, so `agent/site-config-path` holds the work of `#167`, `#168` and `#169` and targets master alone. The title and body still describe only the named-config-path change. **A literal rebase is not available.** The forge refuses it: `force-push is not permitted on branch 'agent/site-config-path'. Add commits instead.` Replaying the eleven commits is also not a mechanical operation, because each PR in the stack was written against a different base, `#168` branched from `cf384e8` before either `#166` or `#167` existed, and the integration merges that reconciled them are exactly what a linear rebase discards. Attempting it stops on the first commit with conflicts in `site.go` and `site_test.go`. **The equivalent merge is deterministic and was verified.** Master carries `#166` under different hashes from the forge's rebase-merge, and `git diff 4215e60 b705cb0` across all four site sources is empty, so master's copy of `#166` is byte-identical to this branch's. That makes the resolution mechanical: the four conflicted files take this branch's version, which is that same `#166` base plus the `#167`, `#168` and `#169` work, and every other file takes master's side. `README.md` auto-merges, keeping both sides. The result was checked against an independently constructed clean rebase of this branch onto master, built as three tree snapshots one per originating PR. The two trees are identical, and both are byte-identical to the reviewed tip `a61b3df` across `site.go`, `site_config.go`, `site_test.go` and `site_config_test.go`. No new code was written; the review findings raised on all three PRs are carried unchanged. **Validation on the merged tree.** `gofmt` clean, `go vet` and `go test` pass both tagged and untagged, `nix build .#checks.x86_64-linux.go-checks .#packages.x86_64-linux.allod` builds, and `nix flake check` now passes in full. The `allod-parity` baseline red that all four PRs had to disclose is gone: master's `f7c57b8` fixed the section-prompt expectation. **Why it is not pushed.** A `vnprc` agent has no write access to `allod/*`; the push is refused by the pre-receive hook. `allod patch receive` cannot carry the merge either, because it exports with `format-patch` and refuses non-linear history. So this needs an agent on `allod-dev`, or the owner running the merge directly. The exact resolution is above and reproduces in four commands. **One issue-state defect worth a look.** `allod/tools#163` and `allod/tools#165` are already marked closed. They closed when their PRs merged into this stack's branches rather than into master, so their code is still unlanded and is in this PR. If this PR were abandoned, both would be wrongly closed.
allod-agent changed title from Let site commands use a named rclone config to Add named config, hosting layouts and credential lifecycle to allod site 2026-09-09 20:41:01 +01:00
# Conflicts:
#	cmd/allod/site.go
#	cmd/allod/site_config.go
#	cmd/allod/site_config_test.go
#	cmd/allod/site_test.go
Author
Member

Merged origin/master into this branch as 5a97b20, reproducing the resolution above: the four cmd/allod/site*.go files take this branch's side (byte-identical to a61b3df), everything else takes master's side, and README.md auto-merged keeping both. Verified on the merged tree: gofmt clean, go vet and go test pass tagged and untagged, the go-checks and allod-parity check derivations build with passing logs, and nix flake check passes in full. Title and body now describe the whole stack (#167, #168, #169) and close #164, #165 and #163.

Merged origin/master into this branch as `5a97b20`, reproducing the resolution above: the four `cmd/allod/site*.go` files take this branch's side (byte-identical to `a61b3df`), everything else takes master's side, and `README.md` auto-merged keeping both. Verified on the merged tree: gofmt clean, go vet and go test pass tagged and untagged, the go-checks and allod-parity check derivations build with passing logs, and `nix flake check` passes in full. Title and body now describe the whole stack (#167, #168, #169) and close #164, #165 and #163.
vnprc approved these changes 2026-09-09 20:42:11 +01:00
vnprc merged commit 5a97b20548 into master 2026-09-09 20:42:17 +01:00
vnprc deleted branch agent/site-config-path 2026-09-09 20:42:17 +01:00
Sign in to join this conversation.
No description provided.