9.6 KiB
spec, related_spec, decision, created, status
| spec | related_spec | decision | created | status |
|---|---|---|---|---|
| docs/specs/native-kandev-cli/spec.md | docs/specs/integrations/github-authentication.md | docs/decisions/2026-07-31-system-service-user-continuity.md | 2026-07-31 | complete |
Fix Plan: Preserve Service Identity and Harden Managed Checkout Recovery
Overview
Release 0.83.0 began reconciling the origin URL of every Kandev-managed GitHub checkout during
Local and Worktree repository preparation. A reported Homebrew system installation then failed to
launch both old and new tasks with set repository origin: exit status 128.
The repair addresses both layers exposed by that incident:
- Preserve the established system-service account across reinstall and update commands, reject silent identity drift, and validate the service-home ownership boundary before restart.
- Make managed checkout origin reconciliation idempotent, single-pass on resume, and capable of returning credential-safe Git diagnostics instead of a bare exit code.
No database migration, frontend state, or executor protocol change is required.
Confirmed Root Cause
The reporter's system service resolved to root, while /var/lib/kandev, its managed repository,
.git, and .git/config were all owned by brewuser. Git run as root under Kandev's intentionally
sanitized configuration rejected the checkout as dubious ownership and returned status 128.
The update sequence can produce that state because service install --system currently derives
the account from the installer process: a command invoked with sudo can select SUDO_USER, while
the same command from a root login selects root. Reinstallation rewrites the service definition but
does not change the existing data owner.
Release 0.83.0 exposed the latent mismatch because repoclone.Cloner.SetOriginURL now executes
git remote set-url during repository preparation. The operation discards Git's combined output,
so the user sees only exit status 128. The resume path also prepares the primary repository in
applyResumeRepoConfig, resolves all repositories again for multi-repository configuration, and
resolves them once more for credential routing, repeating the mutation boundary.
Smallest reproductions:
- Run Git with
GIT_CONFIG_NOSYSTEM=1andGIT_CONFIG_GLOBAL=/dev/nullas root against a checkout owned by another UID: Git emitsfatal: detected dubious ownership in repositoryand exits 128. - Leave
.git/config.lockin a managed checkout and callSetOriginURL: the current application error is the same opaqueset repository origin: exit status 128.
Design
1. Durable system-service identity
- Add
--run-as <user>to nativeservice install --system; reject it for user services and all non-install actions. - Resolve the target account using ADR-2026-07-31: explicit flag, existing root-controlled managed
unit/plist, non-root
SUDO_USERon first install, otherwise an actionable failure requiring an explicit account. - Parse only a Kandev-managed service definition. A missing systemd
User=or launchd user key is the explicit root identity of an existing definition. - Treat service-owned
install.jsonas corroborating metadata, never as privileged authority. - Resolve the selected account to a UID. If the system home is missing or has another owner UID, fail before writing or restarting the service with pre-create/ownership guidance. Do not chown recursively or add Git trust exceptions.
- Persist the effective account in the generated definition and install metadata.
2. Idempotent, diagnosable origin reconciliation
- Keep the existing per-repository serialization in
repoclone.Cloner. - Inspect
remote.origin.urlunder the same sanitized Git environment. If it exactly matches the desired canonical URL, return without writing.git/config. - Never log or return the successful current URL. On failure, bound the diagnostic and redact known authentication material plus HTTP(S) URL userinfo before wrapping it.
- Classify Git's dubious-ownership message into a stable ownership-specific error that advises
checking the Kandev service account and managed checkout owner. Keep generic sanitized context
for failures such as
config.lock, missing repositories, or malformed configuration. - Continue failing closed. Do not retry with
safe.directory, a different user, or a stale origin.
3. One repository-resolution pass per resume
- Resolve all task repositories once in
buildResumeRequestand pass that result to primary repository configuration, multi-repository request construction, and GitHub credential routing. - Select the persisted session primary repository from that resolved set by repository ID. Preserve the current fallback for older sessions and resolve a persisted repository separately only when it is legitimately absent from the task attachment set.
- Ensure each attached managed repository reaches
ensureRepoLocalPathat most once per resume. Initial launch already uses one resolved set and remains behaviorally unchanged. - Preserve repository ordering, base/checkout branch metadata, repo-less tasks, inherited task environments, user-managed local checkout behavior, and credential snapshot semantics.
Tests
Native service installer
- Table-test argument validation and first-install identity precedence, including explicit root.
- Render Linux/macOS definitions with
--run-asand prove reinstall without the flag preserves the existing managed account when the current installer user differs. - Prove untrusted service metadata cannot override the root-controlled definition.
- Prove owner-UID mismatch fails before the service file write/restart boundary and reports both identities; a matching pre-created home continues.
Repository clone boundary
- Real-Git test that an already-canonical origin does not rewrite
.git/config(mtime/content or an injected command boundary), while a changed origin still updates it under the existing lock. - Classifier tests for dubious ownership, stale
config.lock, bounded output, URL-userinfo redaction, and known-secret redaction. - Preserve concurrent-update serialization and invalid-input coverage.
Resume orchestration
- Instrument repository preparation and assert one call per attached repository during resume, including a multi-repository task.
- Cover persisted session primary selection, an older-session fallback, a repo-less task, and the exceptional persisted repository not present in task attachments.
- Retain the existing managed/executor transport transitions and user-managed-local no-op tests.
Documentation
- Update
docs/public/run-as-a-service.mdwith identity preservation,--run-as, first root-shell install behavior, ownership-mismatch recovery, and the explicit no-auto-chown policy. - Update
docs/public/cli.mdwith the new flag and valid command combinations. - Update
docs/public/integrations.mdto explain actionable managed-checkout ownership errors without recommending broad Git trust overrides. - Run both public-doc validators.
Implementation Tasks
- task-01-preserve-system-service-user — done
- task-02-harden-origin-reconciliation — done
- task-03-resolve-resume-repositories-once — done
- task-04-document-service-identity-recovery — done
Tasks 01 and 02 have disjoint production boundaries and no dependency on each other. The primary conversation should execute them sequentially unless the user explicitly authorizes delegation. Task 03 depends on task 02's finalized origin-reconciliation contract. Task 04 follows all behavior tasks so public wording matches the implemented CLI and errors.
Validation Strategy
Run task-focused RED/GREEN commands first, then:
set -euo pipefail
(
cd apps/backend
go test ./internal/launcher ./internal/repoclone ./internal/orchestrator/executor -count=1
make test
make lint
)
node --test scripts/validate-public-docs.test.mjs
node scripts/validate-public-docs.mjs
Recorded Results
- Backend focused tests:
cd apps/backend && go test ./internal/launcher ./internal/repoclone ./internal/orchestrator/executor -count=1passed. - Backend verification:
cd apps/backend && make testandcd apps/backend && make lintpassed. - Public documentation:
node --test scripts/validate-public-docs.test.mjspassed 58 tests;node scripts/validate-public-docs.mjsvalidated 41 published pages. - Operator migration caveat: an existing system home owned by a different account must be reconciled explicitly before reinstall; Kandev does not recursively chown it or add Git trust exceptions.
No browser E2E is planned because the change adds no UI state or interaction. Existing backend error propagation displays the improved sanitized message in the current session error surface.
Risks and Out of Scope
- Hand-edited or drop-in-extended service definitions may be ambiguous. Preservation is limited to definitions that Kandev can identify and parse safely; ambiguity fails closed.
- UID/name changes managed outside Kandev may require operator reconciliation. Kandev will not infer equivalence from filesystem access or ACLs.
- Automatic recursive ownership repair, ACL management, Windows services, global/per-repository
safe.directorymutation, and privilege dropping for individual Git subprocesses are out of scope. - The fix does not weaken Kandev's isolated Git configuration. Ambient global/system Git config remains intentionally unavailable to repository-clone subprocesses.
- User-managed local repository origins remain outside Kandev's mutation boundary.