Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion docs/audits/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ Currently-actionable findings, pass-scoped. Earlier passes (1–3) are largely i
| P4-SEC-04 | P1 | PRs #54/#57 (2026-07-03) | Pre-enrollment bootstrap window: local pairing plane fully closed. Part 1 (PR #54): full 256-bit device fingerprint (`internal/devicekeys/fingerprint.go`, golden-vector-pinned) binding signing key + age recipient, compare-and-confirm gate on `devices approve`/`enroll --approve` before any DB write, `SECU-05` keyless-placeholder refusal. Part 2 (PR #57): one-paste `devstrap-pair1:` pairing codes (`devices pairing-code`, `init --join --code [--fingerprint]`, `devices enroll --code --approve --fingerprint`), unauthenticated-by-design blob with integrity from the derived-fingerprint ceremony; control-char rejection; carried fields surfaced at confirm time. Proven live on R2 (dogfood run 3). Remaining (tracked separately): authenticated snapshot exchange, remote trust propagation. |
| P4-SEC-07 (rotation) | P2 | PR #56 (2026-07-03) | Periodic (non-revoke) WCK rotation — the last open remainder: `keys rotate` (pure Rotate, wrap-first atomic, no revoke side effects) + age-triggered auto-rotation in `sync` (`keys.rotate_max_age` default 90d, `0` disables; post-pull/pre-push with same-cycle grant push; storm suppression via pulled-grant age reset), doctor `workspace key age`. Forward exposure only; documented-not-built: old-epoch containment, keychain-slot growth. Proven live on R2 (dogfood run 3: epoch 1→2 converged in one sync per device). With the foundation (PR #25) and pairing (PRs #48–#50) rows above, P4-SEC-07 is fully shipped. |
| P4-GIT-03 | P1 | slices 1–6 (2026-07-05, PRs #107/#121–#124/#125/#127 + read-confine) | OS-enforced agent sandbox, fully shipped. macOS Seatbelt + Linux bubblewrap + Linux Landlock fallback confine writes to the worktree/tmp, deny credential reads (full-fidelity backends), and deny policy-scoped network; a pure-Go seccomp syscall denylist (mount/kexec/ptrace/keyring/io_uring/legacy-escape → EPERM) rides both Linux backends; unsigned local `sandbox.violation` telemetry records backend/mode/limitations per run and persists tagged macOS Seatbelt denials, surfaced by `agent show`/`doctor`; and tighter read confinement (`--read-confine auto\|on\|off`, default-on for the readonly policy, `--read-allow` extras) restricts reads to the worktree/tmp, OS toolchain roots, and $HOME build caches on all three backends — which also gives the Landlock fallback a credential-read boundary it otherwise lacks. `--sandbox auto\|off\|require` gates the whole thing; `require` refuses to launch when a requested confinement cannot be enforced. Documented-not-built follow-ups (not `P4-GIT-03` remainders): Linux runtime denial detection, macOS `mach-lookup` tightening, and containerization (spec/14 roadmap). |
| P4-PROD-04 | P2 | `feat/p4-prod-04-service-install` | `devstrap service install\|uninstall\|status` wraps `run-loop` in a per-user launchd LaunchAgent (macOS, `com.devstrap.run-loop`, modern `bootstrap`/`bootout`/`print` verbs) or systemd `--user` service (Linux, `devstrap-run-loop.service`) so the workspace converges unattended. The OS branch lives entirely behind `internal/platform` (`ServiceSpec`/`ServiceManager` enriched; `service_launchd.go`/`service_systemd.go` untagged + golden-tested, `service_darwin.go`/`service_linux.go` PATH-shim-tested). Install refuses an unconfigured hub and an ephemeral `$TMPDIR`/`go-build` exec path, writes the plist/unit atomically at `0600`, restarts on failure throttled to 30s, and bakes no secret (adapters add only `PATH`); the systemd path advises `loginctl enable-linger` when lingering is off and never auto-downgrades keychain custody (`P6-XP-04`). `doctor` folds in an optional service check. Docs: `spec/13` §service, `spec/05`/`spec/06` shipped installers, `spec/16` test rows. (Pass-4 finding — does not count toward the Pass-6 total.) |
| P6-SYNC-01 | P1 | PR #30 | Per-event `event_verification_failure` quarantine + approve-time replay; one bad signed event no longer wedges the batch. Open residual: synced `device.revoked` trust propagation (revoke is still local-only). |
| P6-SEC-01 | P1 | PRs #31/#33/#34 | (a) grant carriers verified before WCK ingestion; (b/c) `(epoch, kid)`-addressed custody, overwrite refusal, grant-preferred `PushKey`, replay-time grant ingestion. Fully shipped. |
| P6-SEC-02 | P2 | PRs #32/#33 | Founder/join split (`init --join`, pull-before-push, founder gate) + `(epoch, kid)` keying; a joiner never self-mints or loses pre-approval events. Open residuals: `P6-SEC-03` truncate wedge, `P4-SEC-04` part 2 (one-paste pairing code; fingerprint confirmation shipped). |
Expand Down Expand Up @@ -167,7 +168,7 @@ Currently-actionable findings, pass-scoped. Earlier passes (1–3) are largely i
| P4-QUAL-04 | P2 | Enforce coverage in CI + add a Windows build _(narrowed 2026-07-05: the coverage half shipped — ci.yml generates `coverage.out` and gates a 50% `go tool cover -func` floor, closing `P5-QUAL-04`; remaining open scope is the Windows build only — `internal/platform` build tags are already windows-clean, known runtime risk: the `syscall.Signal(0)` liveness probe in `internal/cli/repo_lock.go`)_ |
| P4-QUAL-05 | — | **Shipped v0.1.1 (2026-07-05)** — moved to _Recently shipped_ above. |
| P4-QUAL-07 | P3 | Enable resource/context-leak linters (`bodyclose`, `sqlclosecheck`, `contextcheck`, …) |
| P4-PROD-04 | P2 | `devstrap service install` (LaunchAgent/systemd unit wrapping `run-loop`) |
| P4-PROD-04 | | **Shipped 2026-07-06** (`feat/p4-prod-04-service-install`) — `devstrap service install\|uninstall\|status` (LaunchAgent/systemd unit wrapping `run-loop`) — moved to _Recently shipped_ above. |
| P4-PROD-05 | — | **Shipped v0.1.0 (2026-07-05)** — moved to _Recently shipped_ above. |

### Pass 3 (2026-06-28 cloud-sync) — residual
Expand Down
31 changes: 31 additions & 0 deletions internal/cli/doctor.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package cli
import (
"context"
"encoding/json"
"errors"
"fmt"
"io"
"os"
Expand Down Expand Up @@ -240,9 +241,39 @@ func runDoctorChecks(ctx context.Context, opts *options) []checkResult {
results = append(results, checkResult{Name: "state database", Status: checkError, Detail: err.Error()})
}
results = append(results, checkRepoLocks(paths.Home)...)
results = append(results, checkService(ctx, opts)...)
return results
}

// checkService reports the background run-loop service's health (P4-PROD-04).
// It is entirely optional: an unsupported platform/session omits the check, a
// not-installed service reports ok (with the install hint), a running service
// reports ok, and an installed-but-stopped service warns with an inspection
// remedy.
func checkService(ctx context.Context, opts *options) []checkResult {
mgr := serviceBackend()
label := mgr.DefaultLabel()
status, err := mgr.Status(ctx, label)
Comment on lines +253 to +256

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check if the chosen service label is persisted anywhere (config/state) for doctor to read back.
rg -nP '\bservice\.label\b|serviceLabel|SavedLabel' --type=go

Repository: Reederey87/DevStrap

Length of output: 157


🏁 Script executed:

#!/bin/bash
set -euo pipefail

ast-grep outline internal/cli/doctor.go --view expanded
printf '\n---\n'
ast-grep outline internal/cli/service.go --view expanded
printf '\n---\n'
rg -n "DefaultLabel|label|install|doctor|Status\(" internal/cli internal -g '*.go'
printf '\n--- service backend files ---\n'
rg -n "type .*Backend|func .*DefaultLabel|func .*Status|label" internal -g '*.go'

Repository: Reederey87/DevStrap

Length of output: 50375


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- internal/cli/doctor.go (240-275) ---'
sed -n '240,275p' internal/cli/doctor.go
echo
echo '--- internal/cli/service.go (36-200) ---'
sed -n '36,200p' internal/cli/service.go
echo
echo '--- service manager interface ---'
rg -n "type ServiceManager interface|DefaultLabel|Status\(" internal -g '*.go'

Repository: Reederey87/DevStrap

Length of output: 12833


doctor should accept the service label used at install time
checkService always probes mgr.DefaultLabel(), so a devstrap service install --label <custom> can still be reported as “not installed” and the reinstall hint points at a second default-labeled instance. Add a --label path here, or persist the chosen label, so doctor checks the same unit name as install and status.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/cli/doctor.go` around lines 253 - 256, `checkService` is always
using `mgr.DefaultLabel()`, so `doctor` can miss a service installed with a
custom label. Update the `checkService` flow in `doctor.go` to accept and use
the same label passed at install time (or read the persisted label) instead of
hardcoding the default. Make sure the label used by `checkService` matches the
one used by the install/status path so the status check and reinstall hint
target the correct unit name.

if err != nil {
if errors.Is(err, platform.ErrUnsupported) {
return nil
}
return []checkResult{{Name: "run-loop service", Status: checkWarn, Detail: err.Error()}}
}
if !status.Installed {
return []checkResult{{Name: "run-loop service", Status: checkOK, Detail: "not installed (optional; `devstrap service install` for unattended sync)"}}
}
if status.Running {
return []checkResult{{Name: "run-loop service", Status: checkOK, Detail: fmt.Sprintf("installed and running (%s)", status.Detail)}}
}
return []checkResult{{
Name: "run-loop service",
Status: checkWarn,
Detail: fmt.Sprintf("installed but not running (%s)", status.Detail),
Remedy: fmt.Sprintf("inspect launchctl print / journalctl --user -u %s; reinstall with devstrap service install", label),
}}
}

func checkAgentRunSweep(ctx context.Context, opts *options, store *state.Store) []checkResult {
_ = opts
reconciled, stillRunning, err := sweepStaleAgentRuns(ctx, store)
Expand Down
26 changes: 26 additions & 0 deletions internal/cli/doctor_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@ package cli
import (
"context"
"path/filepath"
"strings"
"testing"

"github.com/Reederey87/DevStrap/internal/platform"
"github.com/Reederey87/DevStrap/internal/state"
"github.com/spf13/viper"
)
Expand Down Expand Up @@ -159,3 +161,27 @@ func TestCheckHubHealthWorkspaceIDRowFileHub(t *testing.T) {
t.Fatalf("checkHubHealth results = %+v, want workspace id row", results)
}
}

func TestDoctorWarnsWhenServiceInstalledButStopped(t *testing.T) {
f := &fakeServiceManager{
labelVal: "fake.run-loop",
statusVal: platform.ServiceStatus{Installed: true, Running: false, Detail: "not loaded", UnitPath: "/x/fake.plist"},
}
withFakeService(t, f)

v := viper.New()
v.Set("home", t.TempDir())
opts := &options{v: v}

results := checkService(context.Background(), opts)
if len(results) != 1 {
t.Fatalf("checkService results = %+v, want exactly one", results)
}
got := results[0]
if got.Name != "run-loop service" || got.Status != checkWarn {
t.Fatalf("service check = %+v, want a warning row", got)
}
if !strings.Contains(got.Remedy, "journalctl --user -u fake.run-loop") {
t.Errorf("remedy = %q, want the inspection hint", got.Remedy)
}
}
1 change: 1 addition & 0 deletions internal/cli/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ func NewRootCommand(stdout, stderr io.Writer) *cobra.Command {
cmd.AddCommand(newEnvCommand(stdout, opts))
cmd.AddCommand(newRunCommand(stdout, opts))
cmd.AddCommand(newRunLoopCommand(stdout, opts))
cmd.AddCommand(newServiceCommand(stdout, opts))
cmd.AddCommand(newAgentCommand(stdout, opts))
cmd.AddCommand(newDevicesCommand(stdout, opts))
cmd.AddCommand(newConflictsCommand(stdout, opts))
Expand Down
7 changes: 7 additions & 0 deletions internal/cli/run_loop.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,13 @@ import (
// or log consumer must be able to notice a persistently failing loop; swallowing
// every error forever hides outages. A single transient failure resets on the
// next success.
//
// When run under `devstrap service` (P4-PROD-04), a non-zero exit here trips
// the supervisor's restart, throttled by the 30s ThrottleInterval (launchd) /
// RestartSec (systemd) baked into the service spec — so a persistently failing
// loop backs off rather than hot-looping. Keep that delay comfortably shorter
// than a typical --interval so a genuinely transient failure still recovers
// promptly.
const runLoopMaxConsecutiveFailures = 5

// newRunLoopCommand implements `devstrap run-loop` (XP-02): a portable
Expand Down
Loading
Loading