Skip to content

Add PowerForge server recovery CLI#341

Merged
PrzemyslawKlys merged 4 commits intomainfrom
codex/powerforge-server-recovery
May 4, 2026
Merged

Add PowerForge server recovery CLI#341
PrzemyslawKlys merged 4 commits intomainfrom
codex/powerforge-server-recovery

Conversation

@PrzemyslawKlys
Copy link
Copy Markdown
Member

Summary

  • add powerforge-web server recovery actions for plan, inspect, capture, deploy dry-run/run, verify, bootstrap-plan, and restore-secrets-plan
  • add the server recovery manifest schema and source-generated JSON models
  • document the recovery workflow, encrypted capture behavior, GitHub backup target policy, and Evotec reference usage

Validation

  • dotnet build C:\Support\GitHub\PSPublishModule\PowerForge.Web.Cli\PowerForge.Web.Cli.csproj -c Release --no-restore
  • Test-Json -Path C:\Support\GitHub\Website\deploy\linux\evotec.serverrecovery.json -SchemaFile C:\Support\GitHub\PSPublishModule\Schemas\powerforge.web.serverrecovery.schema.json
  • PowerForge.Web.Cli.exe server plan --manifest C:\Support\GitHub\Website\deploy\linux\evotec.serverrecovery.json --output json
  • PowerForge.Web.Cli.exe server deploy --manifest C:\Support\GitHub\Website\deploy\linux\evotec.serverrecovery.json --dry-run --fail-on-failure --output json

@claude
Copy link
Copy Markdown

claude Bot commented May 3, 2026

Code Review — PR #341: Add PowerForge server recovery CLI

This is a substantial addition (~2,500 lines). The overall architecture is clear and the design doc is well-written. Below is feedback organized by severity.


Critical / Bugs

1. Process I/O deadlock in RunProcessCaptureText
Reading stdout to end before calling WaitForExit is the classic deadlock pattern: if the child process writes enough to stderr to fill the OS pipe buffer, the child blocks writing stderr, ReadToEnd() on stdout never returns, and the call hangs indefinitely. Fix: read stdout and stderr concurrently (async tasks or separate threads), then WaitForExit.

2. HasExactLine and HasLine are identical (Inspect.cs)
Both methods split, trim, and compare case-insensitively. The intended distinction between an "exact" line match and a "contains" match is lost — one of them is dead code. The HasLine version should use Contains rather than ==.

3. Stale error message in HandleServer
When subArgs.Length == 0 the error text says "Supported action: plan." — this is a copy-paste artifact from before the full action set was wired up. Should enumerate all seven supported actions.

4. Logic inversion in RestoreSecretsPlan.cs

if (!manifest.BackupTarget?.Encryption?.Equals("age", ...) == true)

When Encryption is null, the nullable-bool negation yields null, and null == true is false, so the warning is silently suppressed. The intent was clearly "warn if encryption is not 'age'", but a missing encryption value skips the warning entirely. Use an explicit null check instead:

if (manifest.BackupTarget?.Encryption is not null && 
    !manifest.BackupTarget.Encryption.Equals("age", StringComparison.OrdinalIgnoreCase))

5. Hardcoded Evotec-specific symlink paths in HandleServerInspect
The inspect handler unconditionally runs readlink -f /var/www/evotec/xyz/current && readlink -f /var/www/evotec/pl/current against every manifest. This will fail or report false drift for any non-Evotec server. This content belongs in the manifest, not the engine.

6. Schema / model mismatches

  • Target.architecture defined in the schema has no corresponding Architecture property in PowerForgeServerTarget.
  • notes (array of strings) defined at the schema root has no corresponding property in PowerForgeServerRecoveryManifest. Both are silently discarded during deserialisation.

Security

7. No validation of NamedCommand.Command content before remote execution
BuildDeployCommand passes command.Command verbatim to the remote shell via ExecuteRemote. Since commands are treated as shell scripts by design, whoever controls the manifest JSON can execute arbitrary shell on the remote server. This is an intentional trust boundary, but it is documented nowhere. A clear note in the design doc (and ideally in the CLI --help output) that the manifest file must be treated as trusted input would prevent surprises.

8. tar -xzf ... -C / in the generated restore script
Extracting a decrypted archive directly to / is dangerous if the archive is ever malformed or tampered with. The POWERFORGE_RESTORE_SECRETS_CONFIRM=YES gate handles accidental runs, but there is no path-safety check on archive contents. Consider using --strip-components or listing archive contents first and validating paths before extracting.

9. SanitizeFileName does not strip / on Linux
Path.GetInvalidFileNameChars() does not include / on Linux (only Windows). If manifest.Name contains a forward slash the resulting sanitised name will traverse directories unexpectedly.


Schema Quality

10. Target has no required fields
A manifest with "target": {} passes schema validation but throws InvalidOperationException at runtime in BuildServerSshTarget. Add "oneOf": [{"required": ["sshAlias"]}, {"required": ["host"]}] (or simply require at least one of the two).

11. Missing constraints

  • VerifyUrl.url has no format: uri — any string passes.
  • BackupTarget.type and BackupTarget.encryption are free strings with no enum — a typo like "Age" silently breaks recovery.
  • BackupRetention.keepLatest and keepDays allow minimum: 0 meaning "keep nothing", which is almost certainly unintentional (minimum: 1 or conditional requirement would be safer).
  • Secret.capture enum only has ["exclude", "encrypted"] but the design doc describes both plain and encrypted capture sets — is "plain" intentionally excluded?

Code Quality

12. No error handling in LoadServerRecoveryManifest
A missing file, malformed JSON, or type mismatch produces a raw .NET stack trace. Wrapping the File.ReadAllText + JsonSerializer.Deserialize call in a try/catch and emitting a structured CLI error would significantly improve operator experience.

13. No SSH connection timeout in ExecuteRemote
A hung SSH connection (network partition, unresponsive server) will block the process indefinitely. Passing -o ConnectTimeout=30 to the SSH command would make the tool much more robust.

14. chmod heuristic in WriteRestoreSecretsScript is fragile
The heuristic hard-codes 600 for paths containing "letsencrypt/credentials" or ".env" and 640 for everything else. Any new secret type will silently get 640. The restoreMode field already exists in the model — it should be used to encode the intended permissions explicitly rather than inferring them from path substrings.

15. One SSH connection per inspect check
HandleServerInspect opens roughly 15–20 separate SSH connections (one per check). Batching the checks into a single ssh call (or using SSH multiplexing via ControlMaster) would be meaningfully faster and less noisy in SSH logs.

16. WriteRestoreChecklist hardcodes Evotec repo names
Server.cs emits "Clone or update the **Website and PSPublishModule** repositories" — Evotec-specific content in the reusable engine layer. This contradicts the design goal of keeping site-specific content out of the engine. The checklist entries should come from the manifest.

17. Cloudflare detection via Server header is unreliable
Verify.cs checks Server: cloudflare but Cloudflare has been phasing this out. The CF-Ray response header is a more reliable signal.

18. Unrelated changes bundled in the PR
The PowerForge.Web.Backlog.md renaming of "CodeGlyphX" → neutral names is unrelated to server recovery. Minor, but makes the diff harder to review.


Summary

The feature is well thought-out and the design document is excellent. The main items to address before merging are: the process I/O deadlock (item 1), the Evotec-specific content in the engine (items 5, 16), the logic inversion bug (item 4), the schema/model mismatches (item 6), and wrapping manifest loading in a try/catch (item 12). The security note about manifest trust (item 7) and the tar -C / concern (item 8) are also worth addressing with at minimum a documentation note.

@claude
Copy link
Copy Markdown

claude Bot commented May 3, 2026

Code Review — PR #341: Add PowerForge server recovery CLI

This is a substantial addition (~2,540 lines across 9 new files). The architecture is well-structured and the security intent is clearly thought-through. Comments below are ordered by severity.


Critical / Correctness

1. PowerForgeServerSecret.Capture default is string.Empty, not "exclude"

public string Capture { get; set; } = string.Empty; will silently deserialize a missing field as "". All comparison code uses secret.Capture.Equals("encrypted", ...), which means a missing capture field will be treated as neither "exclude" nor "encrypted" — a silent no-op rather than a safe default. The schema enforces ["exclude","encrypted"] but C# has no runtime enforcement.

Fix: change the default to "exclude" or make the property nullable and handle null explicitly.

2. Unhandled exceptions from BuildServerSshTarget() and BuildRemoteTarScript() crash the process

Both throw InvalidOperationException on bad manifest data (missing host, disallowed capture path characters). None of the callers catch these. A bad manifest produces an unhandled exception and stack trace instead of a structured JSON error — inconsistent with the rest of the CLI's error model.

Fix: wrap the main handler body (or the individual calls) in a try/catch InvalidOperationException and emit a structured failure result.

3. HandleServerCapture() always exits 0 even when captures fail

Capture command failures are collected as warnings but do not affect the exit code. Unlike deploy and verify, there is no --fail-on-failure flag, so CI pipelines cannot reliably detect a failed capture without parsing JSON output.

Fix: add a --fail-on-failure flag to capture consistent with deploy/verify, or make capture failures always exit non-zero.


Schema / Model Consistency

4. NamedCommand.Id is nullable in C# but required in the schema

string? Id in the model vs "required": ["id", "command"] + "minLength": 1 in the schema. A manifest bypassing schema validation (or loaded programmatically) with a null id will produce command.out.txt-style output paths without any warning.

Fix: make Id non-nullable (string Id) and set a sensible default, or validate after deserialization.

5. manifest.Apache?.ValidateCommand / manifest.Packages?.ApacheModules ?? manifest.Apache?.Modules fallback repeated in three places

The dual-source fallback (Packages.ApacheModules ?? Apache.Modules) is copy-pasted in HandleServerPlan, HandleServerInspect, and BuildBootstrapPlanSteps. If the merge semantics change (e.g., both lists should be combined rather than one taking priority), three sites need updating.

Fix: add a computed helper, e.g. IReadOnlyList<string> ResolveApacheModules(PowerForgeServerRecoveryManifest m).


Error Handling

6. No SSH command timeout beyond ConnectTimeout=30

ConnectTimeout=30 covers the TCP handshake, not the command runtime. A hung tar or apt list command will block the CLI indefinitely.

Fix: propagate a timeout (perhaps via --timeout-seconds flag or a manifest field) and pass it to ProcessStartInfo / WaitForExit(timeout).

7. copyTask.GetAwaiter().GetResult() in CaptureEncryptedRemoteTarArchive()

This surfaces AggregateException on failure, not the inner exception. The call-site catch block does not exist, so this exception is unhandled.

Fix: either await in an async context or use .Result inside a try/catch that unwraps the inner exception.


Code Quality

8. "validate""plan" alias is undocumented

The dispatcher maps "validate" silently to HandleServerPlan. Neither the usage strings in WebCliHelpers.cs nor Docs/PowerForge.Web.ServerRecovery.md mention this alias. A user who types server validate expecting a schema-validation-only operation will get the full plan output.

Fix: either document the alias explicitly, or remove it and add a dedicated validate handler that only validates the manifest and exits.

9. new HttpClient() per URL in VerifyUrl()

Standard socket exhaustion anti-pattern. For a small CLI tool with a handful of URLs this is unlikely to cause issues, but it is worth noting. Consider passing an HttpClient instance from the caller.

10. ProcessResult in ServerRecoveryModels.cs

ProcessResult is a process-execution DTO, not a domain model. It would be more at home in WebCliCommandHandlers.Server.cs or a shared helpers file, and its Success computed property (ExitCode == 0) means it implicitly depends on the process model rather than being a pure data type.


Security

Overall the security posture is good. Specific positive notes:

  • ShellQuote() uses the correct POSIX '"'"' escaping pattern.
  • BuildRemoteTarScript() whitelists path characters before building the remote script.
  • The restore-secrets script checks for path-traversal before extraction.
  • age recipient (a public key) is correctly stored in the manifest; private keys come from the environment only.

One minor concern: BuildServerSshTarget() produces user@host from manifest fields that are passed as an SSH positional argument. At the C# level this is safe (ArgumentList, not a shell string), but a host value like example.com -o ProxyCommand=... could inject SSH options. Adding a basic hostname/username character allowlist (similar to the capture path check) would harden this.


Documentation

Docs/PowerForge.Web.ServerRecovery.md is well-written and honest about the "proposed" vs "implemented" split. One suggestion: call out the validate alias and the --fail-on-failure gap (once addressed) so operators know the current limitations.


Summary

Area Verdict
Architecture ✅ Well-partitioned, consistent file-per-feature pattern
Security intent ✅ Thoughtful — quoting, allowlists, no secrets in manifests
Error model ⚠️ Inconsistent — capture doesn't follow deploy/verify conventions
Schema/model parity ⚠️ Two mismatches (Capture default, Id nullability)
Documentation ✅ Clear and honest about scope
Test coverage ℹ️ No automated tests visible; manual validation steps are listed in the PR description

The most important fixes before merge are items 1–3 above. The rest are lower-priority quality improvements.

@claude
Copy link
Copy Markdown

claude Bot commented May 4, 2026

Code Review — PR #341: Add PowerForge server recovery CLI

Overall: This is a well-structured, security-conscious addition of a server recovery subsystem. The architecture is clean and the secret-handling design is thoughtful. Below is detailed feedback organized by area.


What this PR does

Adds powerforge-web server subcommands (inspect, plan, capture, deploy, verify, bootstrap-plan, restore-secrets-plan) backed by a JSON manifest schema, a full set of source-generated C# models, and comprehensive documentation. The scope is large (~2,570 additions) but the logic is well-partitioned into separate partial-class files per command.


Strengths

  • Security design is solid. The manifest-trust warning in the docs is explicit and correct: command fields in the manifest are treated as shell scripts executed over SSH. Plain vs. encrypted capture split is well-reasoned, and the restore-secrets.sh generation enforces path-traversal rejection and requires an explicit env-var confirmation before extracting to /.
  • Shell quoting is correct. ShellQuote uses the standard POSIX single-quote + escape approach; BuildSshArguments passes the shell command through sh -lc with proper quoting, avoiding injection from manifest-supplied paths.
  • Capture path validation. BuildRemoteTarScript rejects paths with characters outside a safe allowlist before building the remote tar command — good defence against unexpected manifest content.
  • set -Eeuo pipefail in all generated scripts; LF normalization on Windows hosts. Solid.
  • Deduplication in AddStep prevents the same bootstrap command from being emitted twice when manifest sections overlap (e.g. Apache modules appearing in both packages.apacheModules and apache.modules).

Issues and suggestions

Security / correctness

  1. apt-get install without version pins (BootstrapPlan.cs line ~772)
    The generated bootstrap script emits apt-get install -y <packages> from the manifest. Without pinned versions this is non-deterministic. Consider at least documenting this in the generated markdown and adding a warning when no version constraint is present in the package name.

  2. sudo -n in server inspect may silently degrade checks.
    Commands like sudo -n ufw status numbered and sudo -n sshd -T will return a non-zero exit code if the SSH user lacks passwordless sudo. The check is then reported as a connectivity/config failure rather than a "missing sudo permission" failure. Consider adding an early sudo -n true probe and recording the result as a sudo.available check so users know why downstream checks are failing.

  3. age -d decryption target path in restore-secrets.sh
    The script decrypts the archive with age -d -o "$tmp_dir/secrets.tar.gz". If the age process fails (wrong key, corrupt archive) the output file may be partially written. The script continues to tar -tzf it, which will silently succeed on a truncated gzip with some implementations. Adding age -d ... && tar -tzf ... (already using set -e, so this is mostly a clarity point) would make the failure path easier to diagnose.

  4. HttpClient is instantiated per URL check (VerifyUrl, Server.Verify.cs line ~1708).
    For single-use CLI tools this is acceptable, but if a manifest defines many verify URLs the socket exhaustion risk is real. Consider a single HttpClient instance (or HttpClientFactory) shared across URL checks within one invocation.

  5. File.Copy(manifestPath, ...) during capture (Server.cs line ~1921) copies the raw manifest — including any inline recipient key — to the output directory before the encryption step. If the output directory ends up in Git (e.g., via backupTarget), the recipient public key leaks in plain text. Public keys are not secrets, but if the manifest were ever extended to allow an inline private key this becomes a footgun. A comment noting that the manifest copy is intentionally plain would remove ambiguity.

Code quality

  1. BuildServerSshTarget throws InvalidOperationException but callers wrap only HandleServer (the dispatch entry point), not the individual handlers. HandleServerPlan calls BuildServerSshTarget indirectly — but HandleServerPlan does not use it. The other handlers (inspect, capture, deploy, verify) do call it and they are covered by the outer try/catch in HandleServer. This is correct today, but the coupling is subtle. Consider making BuildServerSshTarget return null + a warning string instead of throwing, so callers can produce a proper structured error rather than a caught exception message.

  2. ProcessResult.Success => ExitCode == 0 is defined in ServerRecoveryModels.cs but ProcessResult is a general internal type. If it is ever reused outside this subsystem the ExitCode == 0 convention may not hold (e.g., some tools use exit code 2 for "success with warnings"). The current scope is fine, but renaming the property to ExitedClean or making it explicit in the doc would reduce future confusion.

  3. RunProcessCaptureText / RunProcessCaptureBinary use .GetAwaiter().GetResult() on async I/O (Server.cs lines ~2252–2274). This is a known pattern for synchronous CLI tools but it can deadlock on certain stream buffer configurations when both stdout and stderr fill simultaneously. The standard fix (ReadToEnd called on two tasks before WaitForExit, then awaited) is already applied correctly here — just worth confirming that WaitForExit is called after both async reads are started, which it is. ✓

  4. SanitizeFileName strips / and \ (Server.cs line ~2338–2344). This is correct for a file-name component. However, ResolveCaptureOutputPath calls Path.GetFullPath(Path.Combine(..., name, timestamp)) where name comes from SanitizeFileName(manifest.Name). A manifest name of ../../etc would survive the sanitizer as ..-..-etc and produce a benign path, which is fine. But it is worth a test or an assertion if automated manifest generation is ever added.

Schema

  1. secret.capture enum is not enforced in the schema.
    The C# model defaults Capture to "exclude" and the code checks "encrypted" via StringComparison.OrdinalIgnoreCase, but the JSON schema (powerforge.web.serverrecovery.schema.json) does not define an enum constraint on capture. Consider adding "enum": ["exclude", "plain", "encrypted"] to the Secret definition to catch typos at manifest validation time.

  2. backupTarget.retention is defined in both the schema and the C# models but is not referenced anywhere in the CLI logic. This is fine if it is forward-reserved, but a comment or a // reserved note in the model would prevent "dead code" confusion later.

Documentation

  1. PowerForge.Web.ServerRecovery.md is dated 2026-05-03 in its header. That date is in the future relative to normal release cadence conventions. If the date is intentional (the doc was drafted in advance), it is fine, but it may confuse contributors expecting the "last updated" field to match the commit date.

  2. server validate is documented as an alias for server plan in the usage help, but the doc (ServerRecovery.md) only mentions server plan. It would be clearer if the doc explicitly noted the alias.


Minor / nitpicks

  • HasLine and HasExactLine split on \r\n but also accept \n-only via StringSplitOptions.RemoveEmptyEntries. This is correct for SSH output that may mix line endings.
  • The Preview(string) 500-character truncation is reasonable; a note that binary output from SSH commands is not guarded here (it would just appear as garbled text in OutputPreview) might be worth adding to the docs.
  • In WebCliCommandHandlers.Server.cs, SanitizeFileName and ShellQuote are private statics on the partial class. If other command handlers (e.g., future cloudflare extensions) need these utilities, they should be promoted to WebCliHelpers. For now, the scope is appropriate.

Summary

The implementation is solid and the security model is well thought-out. The main actionable items are:

  1. Add secret.capture enum constraint to the JSON schema.
  2. Add an early sudo.available probe to server inspect.
  3. Document or warn about unpinned apt packages in bootstrap plans.
  4. Consider a shared HttpClient for multi-URL verification.

The rest are low-priority clarity improvements.

@claude
Copy link
Copy Markdown

claude Bot commented May 4, 2026

Code Review — PR #341: Add PowerForge server recovery CLI

Overview

This PR introduces a comprehensive powerforge-web server recovery subsystem: a JSON manifest schema, source-generated C# data models, and CLI handlers for inspect, plan, capture, deploy, verify, bootstrap-plan, and restore-secrets-plan. The documentation is thorough and the overall architecture is sound. Below is a detailed pass over code quality, security, and gaps.


What is Well Done

  • File organisation: Each sub-command gets its own partial class file (Server.Inspect.cs, Server.Deploy.cs), consistent with the existing codebase convention.
  • Shell-quoting: ShellQuote() correctly escapes single-quoted shell arguments, and all SSH argument lists use ArgumentList.Add() (safe from process-level injection).
  • Archive path validation in BuildRemoteTarScript uses a character allowlist, not a blocklist.
  • set -Eeuo pipefail and the trap cleanup in generated scripts are security-conscious defaults.
  • Path-traversal guard in the restore script rejects absolute paths and .. sequences before extraction.
  • Source-generated JSON: All new types are correctly registered in PowerForgeWebCliJsonContext.

Security Concerns

1. SSH host key verification is not enforced

BuildSshArguments passes -o ConnectTimeout=30 but no host-key policy. On a first-time connection to a fresh recovery host, SSH will either prompt interactively (breaking automation) or silently accept depending on the user's ssh_config. For a recovery tool that runs capture and deploy commands against production secrets, a MITM on first connect is a real risk. Consider documenting that the SSH alias must be pre-configured with a known-hosts entry, or surfacing an explicit --accept-new-host-key flag.

2. sudo -n assumes passwordless sudo on the target

HandleServerInspect and several capture commands issue sudo -n <command>. If passwordless sudo is not configured, these silently fail and are reported as drift. The manifest schema and ServerRecovery.md should call this out as an explicit prerequisite, or make it the very first inspect check so the failure is obvious.

3. Glob characters allowed in capture paths

BuildRemoteTarScript allows *, ?, [, ] in paths. These expand via sh -lc on the remote host. This is probably intentional for paths like /etc/apache2/sites-enabled/*.conf, but a code comment would prevent a future "hardening" pass from removing them and breaking valid manifests.

4. Hardcoded chmod mode inference in WriteRestoreSecretsScript

The mode for restored secret files is inferred from path string matching (letsencrypt/credentials, .env). This will produce wrong permissions for any path that does not match those substrings. A mode property on the Secret schema (already present on ManagedPath) would make this explicit and correct.


Bugs / Correctness

5. No process execution timeout

RunProcessCaptureText and RunProcessCaptureBinary call process.WaitForExit() with no timeout. -o ConnectTimeout=30 only covers the TCP handshake; a hung tar, stalled pipe, or unresponsive sudo on the remote side will block the CLI forever. Consider WaitForExit(TimeSpan) or passing -o ServerAliveInterval=15 -o ServerAliveCountMax=3 through to SSH.

6. HandleServer catches only InvalidOperationException

SocketException, Win32Exception (when ssh or age is not in PATH), and IOException all propagate as raw stack traces. A broader top-level catch with a user-friendly message would improve the experience for the most common failure modes.

7. capture --output flag inconsistency

HandleServerCapture accepts --out and --output-dir but not --output. The bootstrap-plan and restore-secrets-plan handlers accept all three. This will confuse users who try the same flag across sub-commands.

8. Capture.exclude is defined but never read

The JSON schema defines capture.exclude, the C# model exposes PowerForgeServerCapture.Exclude, but the capture handler never reads it. Either implement the exclusion filter or mark the field as reserved in the schema description.


Performance / Design

9. HttpClient created per URL

A new HttpClient is instantiated for every URL in VerifyUrl. For a verify pass with many URLs this causes repeated socket allocation/teardown. Passing a single shared HttpClient from HandleServerVerify to VerifyUrl would be cleaner and more efficient.

10. Duplicated output path resolution logic

ResolveBootstrapPlanOutputPath and ResolveRestoreSecretsPlanOutputPath are nearly identical. A shared ResolveServerStateOutputPath(manifest, subDir) helper would remove the duplication.

11. Silent deduplication in AddStep

AddStep silently drops a step when an identical command is already in plannedCommands. If the same command appears in both the auto-generated bootstrap steps and the manifest's bootstrap.commands, users will see a shorter plan with no explanation. Adding a warning on deduplication would make the behaviour transparent.


Test Coverage

No tests are added for this feature. The following helpers are high-value candidates for unit tests:

  • ShellQuote() with edge cases: empty string, embedded single quotes, $, newlines, backslashes.
  • SanitizeFileName() with invalid filename chars, slashes, backslashes.
  • BuildRemoteTarScript() path validation allowlist and the "no target paths" exception path.
  • BuildBootstrapPlanSteps() step ordering, deduplication behaviour, missing-repo-URL warning.
  • BuildServerRecoveryStages() stages reflecting which manifest sections are populated.

Minor Notes

  • The notes manifest field is schema-validated and model-mapped but never surfaced in CLI output. Either print it in plan / inspect or note it as reserved in the docs.
  • Bootstrap step ordering: Apache modules are enabled after bootstrap.commands run. If any bootstrap command requires a module to be loaded first, the plan will fail in practice. Documenting the fixed stage order in ServerRecovery.md would help operators write correct manifests.

Summary

The feature is coherent, well-structured, and the security-sensitive paths (shell quoting, archive path traversal, temp dir cleanup) are handled carefully. Main actionable items before shipping:

Priority Item
High Document or enforce SSH host key verification
High Add a process execution timeout to WaitForExit calls
Medium Broaden exception handling in HandleServer
Medium Replace hardcoded chmod mode inference with a mode field on Secret
Medium Fix capture --output flag inconsistency
Low Remove or implement Capture.exclude
Low Add unit tests for quoting / sanitization / plan-building helpers

Generated with Claude Code

@PrzemyslawKlys PrzemyslawKlys merged commit 8bad0c4 into main May 4, 2026
6 checks passed
@PrzemyslawKlys PrzemyslawKlys deleted the codex/powerforge-server-recovery branch May 4, 2026 15:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant