Files
Nexus-Guard-Suite/.sisyphus/plans/sharelink-presharedkey-fixes.md
T
datadunia 9c30fbd506
NexusGuard CI / server-core-test (push) Failing after 33s
NexusGuard CI / device-agent-test (push) Failing after 28s
NexusGuard CI / dashboard-ui-build (push) Failing after 29s
chore: add new plan docs, update submodule refs
2026-05-27 02:44:12 +07:00

39 KiB

Share Link Fix + PresharedKey Disable Feature

TL;DR

Quick Summary: Fix share link "always expired" bug (URL mismatch) + add peer-level PresharedKey disable toggle with server default setting + fix device-agent dropped PresharedKey.

Deliverables:

  • Share link fix: ShareConfig.vue — use fetch() at root path, not Axios
  • PSK disable: Device model DisablePresharedKey bool, WgServer model DefaultDisablePresharedKey bool + all creation/generation paths + frontend toggles
  • Agent PSK: WireGuardConfig.PresharedKey field + UAPI emission

Estimated Effort: Medium (8 files backend, 4 files frontend, 2 files agent) Parallel Execution: YES — 3 waves Critical Path: Model migration → Backend handlers → Frontend toggles → Agent structs → Final QA


Context

Original Request

  1. Bug: Share config link selalu expired — recipient always sees "Link Expired or Invalid"
  2. Feature: Peer dapat disable PresharedKey; server default disable PSK untuk peer baru

Metis Analysis Key Findings

  • Share bug root cause: URL mismatch — ShareConfig.vue calls api.get('/share/:token') which adds /api/v1 prefix via Axios baseURL, but server route is at root GET /share/:token
  • Share bug secondary concern: Server returns generic 404 for both expired AND invalid — error message doesn't distinguish
  • PSK disable scope: 3 creation paths (peer, device, provisioning) + 3 config generators (peer config, share config, provisioning response) + agent UAPI
  • Precedence: device.DisablePresharedKey > server.DefaultDisablePresharedKey > false (keep PSK)
  • Migration: GORM AutoMigrate with default:false tags; existing rows get false
  • Agent: Missing PresharedKey field in WireGuardConfig struct; needs to handle empty gracefully

Current State

  • Share link: PeerConfigModal.vue creates share at admin dashboard → generates URL like /share/TOKEN → recipient opens → SPA at /share/TOKENShareConfig.vue calls api.get('/share/TOKEN') → WRONG URL → 404 → "Expired"
  • PresharedKey: Always generated via wgtypes.GenerateKey() in all 3 creation paths. Always included in config text. No toggle to disable. Device-agent WireGuardConfig struct doesn't parse it.
  • WireGuard kernel: wgmanager_linux.go already handles empty PresharedKey correctly (zero-value key = no PSK)

Work Objectives

Core Objective

  1. Fix share link so recipients can actually retrieve the config
  2. Allow admin to disable PresharedKey per-peer and set server-wide default

Concrete Deliverables

  • apps/dashboard-ui/src/views/ShareConfig.vue — use fetch() instead of Axios
  • apps/server-core/internal/models/models.go — add DisablePresharedKey to Device, DefaultDisablePresharedKey to WgServer
  • apps/server-core/api/peers.go — conditional PSK in CreatePeer + getDeviceConfig
  • apps/server-core/api/devices.go — DisablePresharedKey in UpdateDeviceRequest + handler
  • apps/server-core/api/share.go — conditional PSK in share config
  • apps/server-core/api/provisioning.go — conditional PSK generation + payload
  • apps/dashboard-ui/src/components/AddPeerModal.vue — PSK toggle
  • apps/dashboard-ui/src/views/DeviceDetail.vue — PSK toggle
  • apps/dashboard-ui/src/views/Servers.vue — default PSK setting
  • apps/device-agent/internal/client/provisioning.go — add PresharedKey field
  • apps/device-agent/internal/tunnel/wireguard.go — conditional preshared_key in UAPI

Definition of Done

  • Share link: curl GET /share/{VALID_TOKEN} → 200 with config_text
  • Share link: curl GET /share/{EXPIRED_TOKEN} → 404 with error message
  • Share link: Open share URL in browser → config displays, no "Expired" error
  • PSK: Create device with PSK enabled → config_text includes PresharedKey = <key>
  • PSK: Create device with PSK disabled → config_text has NO PresharedKey line
  • PSK: Toggle PSK on existing device → updates correctly
  • PSK: Server default disable PSK → new devices inherit
  • PSK: Per-device toggle overrides server default
  • Agent: Provision device with PSK → agent applies preshared_key via UAPI
  • Agent: Provision device without PSK → agent does NOT emit preshared_key
  • npm run build passes (dashboard-ui)
  • go build ./... passes (server-core + device-agent)

Must Have

  • Share link URL mismatch fixed — recipient can download config
  • Admin can disable PSK per-peer at creation/editing
  • Server can set default "disable PSK" for all new peers on that server
  • Device-agent receives and applies PresharedKey when present
  • Device-agent handles missing/empty PresharedKey gracefully

Must NOT Have (Guardrails)

  • Do NOT refactor Axios api instance or VITE_API_BASE_URL
  • Do NOT change server routing structure (main.go route definitions)
  • Do NOT touch shared/crypto/encryptor.go
  • Do NOT add PSK rotation, expiry, or custom/user-defined PSK values
  • Do NOT add bulk tools for mass-updating existing devices
  • Do NOT add new provisioning protocols or agent restart mechanisms
  • Do NOT retroactively apply server default to existing devices
  • Do NOT change how existing tunnels work mid-session

Verification Strategy

ZERO HUMAN INTERVENTION — ALL verification is agent-executed.

Test Decision

  • Infrastructure exists: Yes (Go tests + test DB + miniredis)
  • Automated tests: Tests-after (add/update tests after implementation)
  • Framework: Go testing + miniredis + httptest
  • Agent-Executed QA: curl for API, fetch for share link, npm run build, go build

QA Policy

Every task MUST include agent-executed QA scenarios.

  • API/Backend: curl — specific HTTP methods, paths, request bodies, expected status codes + response fields
  • Frontend: npm run build + grep for expected patterns
  • Agent: grep for struct fields + build verification
  • Evidence: Saved to .sisyphus/evidence/task-{N}-{scenario-slug}.txt

Execution Strategy

Parallel Execution Waves

Wave 1 (Foundation — 2 parallel tasks):
├── Task 1: Model migration (Device + WgServer fields) [quick]
├── Task 2: Share link fix (ShareConfig.vue) [quick]

Wave 2 (Backend — 4 parallel tasks, blocked by Task 1):
├── Task 3: peers.go — conditional PSK in CreatePeer + getDeviceConfig [quick]
├── Task 4: share.go — conditional PSK in share config [quick]
├── Task 5: provisioning.go — conditional PSK gen + payload [quick]
├── Task 6: devices.go — DisablePresharedKey in update handler [quick]

Wave 3 (Frontend + Agent — 4 parallel tasks):
├── Task 7: AddPeerModal.vue — PSK disable toggle [visual-engineering]
├── Task 8: DeviceDetail.vue — PSK disable toggle [visual-engineering]
├── Task 9: Servers.vue — default PSK disable setting [visual-engineering]
├── Task 10: Device-agent — PresharedKey struct + UAPI [quick]

Wave FINAL (Parallel reviews):
├── Task F1: Plan compliance audit (oracle)
├── Task F2: Code quality + build tests (unspecified-high)
├── Task F3: Real manual QA (unspecified-high)
├── Task F4: Scope fidelity check (deep)

TODOs

  • 1. Database model migration — add DisablePresharedKey fields

    What to do:

    • In apps/server-core/internal/models/models.go:
      1. Add to Device struct: DisablePresharedKey bool \json:"disable_preshared_key" gorm:"default:false"``
      2. Add to WgServer struct: DefaultDisablePresharedKey bool \json:"default_disable_preshared_key" gorm:"default:false"``
    • main_dev.go already runs GORM AutoMigrate with -tags dev. The default:false tag ensures existing rows get false (not NULL).
    • Note: GORM default:false on bool works — existing rows will backfill correctly.

    Must NOT do:

    • Do NOT run ALTER TABLE manually — AutoMigrate handles this
    • Do NOT touch models_test.go or migrations.go

    Recommended Agent Profile:

    • Category: quick
      • Reason: 2 lines added, zero logic change, pure schema

    Parallelization:

    • Can Run In Parallel: YES
    • Parallel Group: Wave 1 (with Task 2)
    • Blocks: Tasks 3-9, F1-F4
    • Blocked By: None

    References:

    • apps/server-core/internal/models/models.go:57-84 — Device struct (add field near line 80)
    • apps/server-core/internal/models/models.go:25-48 — WgServer struct (add field near line 45)

    Acceptance Criteria:

    • go build ./... passes
    • Grep confirms new fields exist in model

    QA Scenarios:

    Scenario: Device model has DisablePresharedKey
      Tool: Bash (grep)
      Steps:
        1. grep -n "DisablePresharedKey" apps/server-core/internal/models/models.go
      Expected Result: Match found with gorm tag "default:false"
      Evidence: .sisyphus/evidence/task-1-model-field.txt
    
    Scenario: WgServer model has DefaultDisablePresharedKey
      Tool: Bash (grep)
      Steps:
        1. grep -n "DefaultDisablePresharedKey" apps/server-core/internal/models/models.go
      Expected Result: Match found with gorm tag "default:false"
      Evidence: .sisyphus/evidence/task-1-server-default.txt
    
    Scenario: Build passes
      Tool: Bash
      Steps:
        1. cd apps/server-core && go build ./...
      Expected Result: Exit 0
      Evidence: .sisyphus/evidence/task-1-build.txt
    

    Commit: YES (with Tasks 3-6)

    • Message: feat(model): add DisablePresharedKey flags to Device + WgServer

  • 2. Fix share link URL mismatch in ShareConfig.vue

    What to do:

    • In apps/dashboard-ui/src/views/ShareConfig.vue:
      1. Remove import { api } from '../services/api' (line 36)
      2. Change load() function (lines 43-52) to use fetch() instead of api.get():
      const load = async () => {
        try {
          const res = await fetch(`/share/${route.params.token}`)
          if (!res.ok) {
            throw new Error(`HTTP ${res.status}`)
          }
          const data = await res.json()
          configText.value = data.config_text
        } catch (err) {
          error.value = true
        } finally {
          loading.value = false
        }
      }
      
    • Why: The api Axios instance has baseURL = VITE_API_BASE_URL which includes /api/v1 prefix. Share endpoint is at root GET /share/:token. Using fetch() hits the origin domain correctly.

    Must NOT do:

    • Do NOT import or use api (avoid JWT auth header on public route)
    • Do NOT change router config or route definitions
    • Do NOT modify server main.go routes
    • Do NOT modify api.ts (Axios config)

    Recommended Agent Profile:

    • Category: quick
      • Reason: Single file, ~15 lines change, straightforward

    Parallelization:

    • Can Run In Parallel: YES
    • Parallel Group: Wave 1 (with Task 1)
    • Blocks: F1-F4
    • Blocked By: None

    References:

    • apps/dashboard-ui/src/views/ShareConfig.vue:33-52 — entire script section. Line 45 is the api.get call to replace.
    • apps/dashboard-ui/src/services/api.ts:4-5 — baseURL = VITE_API_BASE_URL (contains /api/v1). Confirms why api.get is wrong.
    • apps/server-core/main.go:268r.GET("/share/:token", shareHandler.GetShare) — root level, not under /api/v1.

    Acceptance Criteria:

    • npm run build passes
    • Grep confirms no api.get( in ShareConfig.vue
    • Grep confirms fetch( is used for share endpoint

    QA Scenarios:

    Scenario: ShareConfig no longer uses api.get
      Tool: Bash (grep)
      Steps:
        1. grep -n "api\." apps/dashboard-ui/src/views/ShareConfig.vue
      Expected Result: No matches (api not imported)
      Evidence: .sisyphus/evidence/task-2-no-api.txt
    
    Scenario: ShareConfig uses fetch instead
      Tool: Bash (grep)
      Steps:
        1. grep -n "fetch(" apps/dashboard-ui/src/views/ShareConfig.vue
      Expected Result: Match found in load() function
      Evidence: .sisyphus/evidence/task-2-fetch.txt
    
    Scenario: Build passes
      Tool: Bash
      Steps:
        1. cd apps/dashboard-ui && npm run build
      Expected Result: Exit 0
      Evidence: .sisyphus/evidence/task-2-build.txt
    

    Commit: YES (separate)

    • Message: fix(ui): use fetch() for share endpoint to fix URL mismatch /api/v1 prefix bug

  • 3. Conditionally generate + output PresharedKey in peers.go

    What to do: Changes in apps/server-core/api/peers.go:

    3a. CreatePeer() — conditional PSK generation (lines 76-80):

    // OLD:
    psk, err := wgtypes.GenerateKey()
    if err != nil {
        c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to generate preshared key"})
        return
    }
    
    // NEW: Generate PSK only if not disabled
    var pskStr string
    if !req.DisablePresharedKey {
        psk, err := wgtypes.GenerateKey()
        if err != nil {
            c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to generate preshared key"})
            return
        }
        pskStr = psk.String()
    }
    

    Also add DisablePresharedKey to CreatePeerRequest struct (line 30-34):

    type CreatePeerRequest struct {
        Name                string `json:"name" binding:"required"`
        WgServerID          string `json:"wg_server_id" binding:"required"`
        AllowInternet       bool   `json:"allow_internet"`
        DisablePresharedKey bool   `json:"disable_preshared_key"`
    }
    

    And in the Device struct creation (line 94-107), change:

    // OLD:
    PresharedKey:        psk.String(),
    
    // NEW:
    DisablePresharedKey: req.DisablePresharedKey,
    PresharedKey:        pskStr,
    

    3b. getDeviceConfig() — conditional PSK in config text (lines 226-246): Change the configText format string to conditionally include PresharedKey:

    pskLine := fmt.Sprintf("PresharedKey = %s", device.PresharedKey)
    if device.DisablePresharedKey || device.PresharedKey == "" {
        pskLine = ""
    }
    

    Then change the format string:

    configText := fmt.Sprintf(`[Interface]
    PrivateKey = %s
    Address = %s/%s
    DNS = %s%s
    
    [Peer]
    PublicKey = %s
    %s
    AllowedIPs = %s
    Endpoint = %s%s`,
        device.PrivateKey,
        *device.InternalIP,
        addrPrefix,
        dns,
        mtuLine,
        wgServer.PublicKey,
        pskLine,
        allowedIPs,
        wgServer.PublicEndpoint,
        keepaliveLine,
    )
    

    Note: If pskLine is empty, the blank line with %s will produce \n\n — need to handle carefully. Better approach: build config text with a conditional block.

    Better approach: Build config string with conditional parts:

    var pskBlock string
    if device.PresharedKey != "" && !device.DisablePresharedKey {
        pskBlock = fmt.Sprintf("PresharedKey = %s\n", device.PresharedKey)
    }
    
    configText := fmt.Sprintf(`[Interface]
    PrivateKey = %s
    Address = %s/%s
    DNS = %s%s
    
    [Peer]
    PublicKey = %s
    %sAllowedIPs = %s
    Endpoint = %s%s`,
        device.PrivateKey,
        *device.InternalIP,
        addrPrefix,
        dns,
        mtuLine,
        wgServer.PublicKey,
        pskBlock,
        allowedIPs,
        wgServer.PublicEndpoint,
        keepaliveLine,
    )
    

    This way if pskBlock is empty, no blank line is inserted.

    3c. UpdateConfig() — handle PresharedKey validation (lines 311-319): Currently rejects PresharedKey changes. Keep this validation — users should use the toggle, not edit config manually.

    Must NOT do:

    • Do NOT touch UpdateConfig() validation logic
    • Do NOT remove the PresharedKey change rejection (users should use toggle)
    • Do NOT touch wg_test.go

    Parallelization:

    • Can Run In Parallel: YES (with Tasks 4, 5, 6)
    • Parallel Group: Wave 2
    • Blocks: Tasks 7-9, F1-F4
    • Blocked By: Task 1

    References:

    • apps/server-core/api/peers.go:30-34 — CreatePeerRequest struct
    • apps/server-core/api/peers.go:76-80 — PSK generation in CreatePeer
    • apps/server-core/api/peers.go:226-246 — getDeviceConfig config text generation
    • apps/server-core/api/peers.go:311-319 — UpdateConfig PresharedKey validation (keep as-is)

    Acceptance Criteria:

    • go build ./... passes
    • CreatePeer with disable_preshared_key=false → PSK in response
    • CreatePeer with disable_preshared_key=true → no PSK in response
    • getDeviceConfig omits PresharedKey line when device has DisablePresharedKey=true

    QA Scenarios:

    Scenario: CreatePeerRequest has DisablePresharedKey field
      Tool: Bash (grep)
      Steps:
        1. grep -n "DisablePresharedKey" apps/server-core/api/peers.go
      Expected Result: 2+ matches (struct + usage)
      Evidence: .sisyphus/evidence/task-3-request-struct.txt
    
    Scenario: CreatePeer conditionally generates PSK
      Tool: Bash (grep)
      Steps:
        1. grep -A5 "if !req.DisablePresharedKey" apps/server-core/api/peers.go
      Expected Result: Shows GenerateKey() inside the conditional
      Evidence: .sisyphus/evidence/task-3-conditional-gen.txt
    
    Scenario: getDeviceConfig conditionally includes PSK
      Tool: Bash (grep)
      Steps:
        1. grep -B2 -A3 "pskBlock\|PresharedKey =" apps/server-core/api/peers.go | head -20
      Expected Result: Shows pskBlock built conditionally, used in format string
      Evidence: .sisyphus/evidence/task-3-config-output.txt
    
    Scenario: Build passes
      Tool: Bash
      Steps:
        1. cd apps/server-core && go build ./...
      Expected Result: Exit 0
      Evidence: .sisyphus/evidence/task-3-build.txt
    

    Commit: YES (with tasks 1, 4, 5, 6)

    • Message: feat(api): conditional PresharedKey generation and config output

  • 4. Conditionally include PresharedKey in share config (share.go)

    What to do:

    • In apps/server-core/api/share.go, the CreateShare() function generates config text at lines 71-88.
    • Replace the hardcoded PresharedKey = %s with conditional inclusion:
    var pskBlock string
    if device.PresharedKey != "" && !device.DisablePresharedKey {
        pskBlock = fmt.Sprintf("PresharedKey = %s\n", device.PresharedKey)
    }
    
    configText := fmt.Sprintf(`[Interface]
    PrivateKey = %s
    Address = %s/%s
    DNS = 1.1.1.1
    
    [Peer]
    PublicKey = %s
    %sAllowedIPs = %s
    Endpoint = %s`,
        device.PrivateKey,
        *device.InternalIP,
        addrPrefix,
        wgServer.PublicKey,
        pskBlock,
        allowedIPs,
        wgServer.PublicEndpoint,
    )
    

    Must NOT do:

    • Do NOT change the existing config structure (Interface, Peer sections)
    • Do NOT touch share_test.go

    Parallelization:

    • Can Run In Parallel: YES (with Tasks 3, 5, 6)
    • Parallel Group: Wave 2
    • Blocks: F1-F4
    • Blocked By: Task 1

    References:

    • apps/server-core/api/share.go:71-88 — current config generation (lines 78 = PresharedKey)
    • apps/server-core/api/peers.go:226-246 — same pattern to follow for conditional PSK block

    Acceptance Criteria:

    • go build ./... passes
    • Share config omits PresharedKey when device has DisablePresharedKey=true
    • Share config includes PresharedKey when device has DisablePresharedKey=false

    QA Scenarios:

    Scenario: Share config has conditional PSK
      Tool: Bash (grep)
      Steps:
        1. grep -n "pskBlock\|DisablePresharedKey" apps/server-core/api/share.go
      Expected Result: Matches showing conditional PSK logic
      Evidence: .sisyphus/evidence/task-4-share-psk.txt
    
    Scenario: Build passes
      Tool: Bash
      Steps:
        1. cd apps/server-core && go build ./...
      Expected Result: Exit 0
      Evidence: .sisyphus/evidence/task-4-build.txt
    

    Commit: YES (with tasks 1, 3, 5, 6)

    • Message: feat(api): conditional PresharedKey in share config

  • 5. Conditionally generate + output PresharedKey in provisioning.go

    What to do: Changes in apps/server-core/api/provisioning.go:

    5a. Conditional PSK generation (lines 105-110):

    // OLD:
    psk, err := wgtypes.GenerateKey()
    if err != nil {
        tx.Rollback()
        c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to generate preshared key"})
        return
    }
    ...
    device.PresharedKey = psk.String()
    
    // NEW:
    var pskStr string
    if !device.DisablePresharedKey {
        psk, err := wgtypes.GenerateKey()
        if err != nil {
            tx.Rollback()
            c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to generate preshared key"})
            return
        }
        pskStr = psk.String()
    }
    ...
    device.PresharedKey = pskStr
    

    5b. Conditional PSK in ConfigPayload (lines 138-145):

    payload := ConfigPayload{
        PrivateKey:   priv.String(),
        InternalIP:   *device.InternalIP,
        ServerPub:    wgServer.PublicKey,
        Endpoint:     wgServer.PublicEndpoint,
        DNS:          "1.1.1.1",
    }
    if pskStr != "" {
        payload.PresharedKey = pskStr
    }
    

    Note: ConfigPayload.PresharedKey is a string — if not set, it serializes as "preshared_key":"". The device-agent already ignores unknown fields silently. This is fine.

    Must NOT do:

    • Do NOT touch the provisioning test file (tests need updating but out of scope for this task)
    • Do NOT touch ConfigPayload struct (leave as-is, empty string = no PSK)

    Parallelization:

    • Can Run In Parallel: YES (with Tasks 3, 4, 6)
    • Parallel Group: Wave 2
    • Blocks: F1-F4
    • Blocked By: Task 1

    References:

    • apps/server-core/api/provisioning.go:37-44 — ConfigPayload struct (PresharedKey field already exists)
    • apps/server-core/api/provisioning.go:105-114 — PSK generation block
    • apps/server-core/api/provisioning.go:138-145 — ConfigPayload construction

    Acceptance Criteria:

    • go build ./... passes
    • Provisioning with DisablePresharedKey=true → no PSK in ConfigPayload
    • Provisioning with DisablePresharedKey=false → PSK in ConfigPayload

    QA Scenarios:

    Scenario: Provisioning conditionally generates PSK
      Tool: Bash (grep)
      Steps:
        1. grep -B1 -A5 "if !device.DisablePresharedKey" apps/server-core/api/provisioning.go
      Expected Result: Shows conditional block with GenerateKey()
      Evidence: .sisyphus/evidence/task-5-conditional-gen.txt
    
    Scenario: ConfigPayload conditionally includes PSK
      Tool: Bash (grep)
      Steps:
        1. grep -A5 "if pskStr" apps/server-core/api/provisioning.go
      Expected Result: Shows the conditional payload.PresharedKey assignment
      Evidence: .sisyphus/evidence/task-5-payload.txt
    
    Scenario: Build passes
      Tool: Bash
      Steps:
        1. cd apps/server-core && go build ./...
      Expected Result: Exit 0
      Evidence: .sisyphus/evidence/task-5-build.txt
    

    Commit: YES (with tasks 1, 3, 4, 6)

    • Message: feat(api): conditional PresharedKey in provisioning flow

  • 6. Handle DisablePresharedKey in device Update handler (devices.go)

    What to do: Changes in apps/server-core/api/devices.go:

    6a. Add to UpdateDeviceRequest (lines 166-174):

    type UpdateDeviceRequest struct {
        Name                string  `json:"name"`
        AllowInternet       *bool   `json:"allow_internet"`
        EndpointAllowedIPs  *string `json:"endpoint_allowed_ips"`
        DNS                 *string `json:"dns"`
        MTU                 *int    `json:"mtu"`
        PersistentKeepalive *int    `json:"persistent_keepalive"`
        Notes               *string `json:"notes"`
        DisablePresharedKey *bool   `json:"disable_preshared_key"`
    }
    

    6b. Handle in Update handler (between lines 192-213):

    if req.DisablePresharedKey != nil {
        updates["disable_preshared_key"] = *req.DisablePresharedKey
        // If re-enabling PSK and no PSK exists, generate one
        if !*req.DisablePresharedKey && device.PresharedKey == "" {
            psk, err := wgtypes.GenerateKey()
            if err == nil {
                updates["preshared_key"] = psk.String()
            }
        }
        // If disabling PSK, clear the stored key
        if *req.DisablePresharedKey {
            updates["preshared_key"] = ""
        }
    }
    

    Must NOT do:

    • Do NOT touch Create() — that's the peer creation path (handled by peers.go)
    • Do NOT touch List(), Get(), Delete() handlers
    • Do NOT generate PSK outside the re-enable scenario

    Parallelization:

    • Can Run In Parallel: YES (with Tasks 3, 4, 5)
    • Parallel Group: Wave 2
    • Blocks: Tasks 7-9, F1-F4
    • Blocked By: Task 1

    References:

    • apps/server-core/api/devices.go:166-174 — UpdateDeviceRequest struct
    • apps/server-core/api/devices.go:176-235 — Update handler (lines 192-213 are the updates section)

    Acceptance Criteria:

    • go build ./... passes
    • PUT /devices/:id with disable_preshared_key=true → device.PresharedKey cleared
    • PUT /devices/:id with disable_preshared_key=false on device with PSK → no change to PSK
    • PUT /devices/:id with disable_preshared_key=false on device without PSK → new PSK generated

    QA Scenarios:

    Scenario: UpdateDeviceRequest has DisablePresharedKey
      Tool: Bash (grep)
      Steps:
        1. grep -n "DisablePresharedKey" apps/server-core/api/devices.go
      Expected Result: 2+ matches (struct + handler)
      Evidence: .sisyphus/evidence/task-6-request-field.txt
    
    Scenario: Update handler handles PSK toggle
      Tool: Bash (grep)
      Steps:
        1. grep -B1 -A6 "if req.DisablePresharedKey" apps/server-core/api/devices.go
      Expected Result: Shows the conditional block with PSK generation/clearing
      Evidence: .sisyphus/evidence/task-6-toggle-logic.txt
    
    Scenario: Build passes
      Tool: Bash
      Steps:
        1. cd apps/server-core && go build ./...
      Expected Result: Exit 0
      Evidence: .sisyphus/evidence/task-6-build.txt
    

    Commit: YES (with tasks 1, 3, 4, 5)

    • Message: feat(api): handle DisablePresharedKey in device update handler

  • 7. Add PSK disable toggle to AddPeerModal.vue

    What to do: In apps/dashboard-ui/src/components/AddPeerModal.vue:

    7a. Add reactive state:

    const disablePresharedKey = ref(false)
    

    7b. Add checkbox to template (after the AllowInternet field):

    <div class="flex items-center space-x-3 mt-3">
      <input type="checkbox" id="disable-psk" v-model="disablePresharedKey"
             class="w-4 h-4 rounded border-white/20 bg-white/5 text-cyan-600 focus:ring-cyan-500" />
      <label for="disable-psk" class="text-sm text-gray-300">
        Disable PresharedKey
        <span class="text-xs text-gray-500 ml-1">(reduces connection security)</span>
      </label>
    </div>
    

    7c. Include in API request (in the submit handler): Pass disable_preshared_key: disablePresharedKey.value in the createPeer payload.

    Must NOT do:

    • Do NOT remove the existing AllowInternet toggle
    • Do NOT add custom PSK input (only yes/no toggle)
    • Do NOT change the existing API response handling

    Parallelization:

    • Can Run In Parallel: YES (with Tasks 8, 9, 10)
    • Parallel Group: Wave 3
    • Blocks: F1-F4
    • Blocked By: Tasks 1, 3

    References:

    • apps/dashboard-ui/src/components/AddPeerModal.vue — full component (read first, add field + state + API passthrough)
    • apps/dashboard-ui/src/views/DeviceDetail.vue — similar toggle pattern for reference

    Acceptance Criteria:

    • npm run build passes
    • Toggle exists in template
    • Toggle is passed in API request

    QA Scenarios:

    Scenario: Toggle exists in template
      Tool: Bash (grep)
      Steps:
        1. grep -n "disable-psk\|DisablePresharedKey\|disable_preshared_key" apps/dashboard-ui/src/components/AddPeerModal.vue
      Expected Result: 2+ matches (template + script)
      Evidence: .sisyphus/evidence/task-7-toggle-exists.txt
    
    Scenario: Build passes
      Tool: Bash
      Steps:
        1. cd apps/dashboard-ui && npm run build
      Expected Result: Exit 0
      Evidence: .sisyphus/evidence/task-7-build.txt
    

    Commit: YES (with tasks 8, 9)

    • Message: feat(ui): add PresharedKey disable toggle to peer creation form

  • 8. Add PSK disable toggle to DeviceDetail.vue

    What to do: In apps/dashboard-ui/src/views/DeviceDetail.vue:

    8a. Add reactive state:

    const disablePresharedKey = ref(false)
    

    8b. Load current state from device data in onMounted or watch:

    disablePresharedKey.value = device.disable_preshared_key || false
    

    8c. Add toggle to the device edit form:

    <div class="flex items-center space-x-3 mt-3">
      <input type="checkbox" id="device-disable-psk" v-model="disablePresharedKey"
             class="w-4 h-4 rounded border-white/20 bg-white/5 text-cyan-600 focus:ring-cyan-500" />
      <label for="device-disable-psk" class="text-sm text-gray-300">
        Disable PresharedKey
        <span class="text-xs text-gray-500 ml-1">(reduces connection security)</span>
      </label>
    </div>
    

    8d. Include in update payload: Pass disable_preshared_key: disablePresharedKey.value in the device update request.

    Must NOT do:

    • Do NOT change the existing edit form layout or other fields
    • Do NOT add custom PSK input

    Parallelization:

    • Can Run In Parallel: YES (with Tasks 7, 9, 10)
    • Parallel Group: Wave 3
    • Blocks: F1-F4
    • Blocked By: Tasks 1, 6

    References:

    • apps/dashboard-ui/src/views/DeviceDetail.vue — read expected edit form section
    • apps/server-core/api/devices.go:166-174 — UpdateDeviceRequest struct (for field name)

    Acceptance Criteria:

    • npm run build passes
    • Toggle in device edit form, bound to device.disable_preshared_key
    • Toggle sent in update API request

    QA Scenarios:

    Scenario: Toggle exists in DeviceDetail
      Tool: Bash (grep)
      Steps:
        1. grep -n "disable_preshared_key\|DisablePresharedKey" apps/dashboard-ui/src/views/DeviceDetail.vue
      Expected Result: 2+ matches (template + script)
      Evidence: .sisyphus/evidence/task-8-toggle-exists.txt
    
    Scenario: Build passes
      Tool: Bash
      Steps:
        1. cd apps/dashboard-ui && npm run build
      Expected Result: Exit 0
      Evidence: .sisyphus/evidence/task-8-build.txt
    

    Commit: YES (with tasks 7, 9)

    • Message: feat(ui): add PresharedKey disable toggle to device detail edit form

  • 9. Add default PSK disable setting to Servers.vue

    What to do: In apps/dashboard-ui/src/views/Servers.vue:

    9a. Add reactive state in the server edit form:

    const defaultDisablePresharedKey = ref(false)
    

    9b. Load from server data:

    defaultDisablePresharedKey.value = editingServer.default_disable_preshared_key || false
    

    9c. Add toggle to server edit form (near peer defaults section):

    <div class="flex items-center space-x-3 mt-3">
      <input type="checkbox" id="server-default-disable-psk" v-model="defaultDisablePresharedKey"
             class="w-4 h-4 rounded border-white/20 bg-white/5 text-cyan-600 focus:ring-cyan-500" />
      <label for="server-default-disable-psk" class="text-sm text-gray-300">
        Disable PresharedKey for new peers by default
        <span class="text-xs text-gray-500 ml-1">(can be overridden per-peer)</span>
      </label>
    </div>
    

    9d. Include in update payload: Pass default_disable_preshared_key: defaultDisablePresharedKey.value in the server update request.

    Must NOT do:

    • Do NOT change existing peer default fields (DNS, MTU, Keepalive, AllowedIPs)
    • Do NOT add any other default settings

    Parallelization:

    • Can Run In Parallel: YES (with Tasks 7, 8, 10)
    • Parallel Group: Wave 3
    • Blocks: F1-F4
    • Blocked By: Tasks 1, 3

    References:

    • apps/dashboard-ui/src/views/Servers.vue — read the server edit form (peer defaults section)
    • apps/server-core/internal/models/models.go:45 — WgServer.DefaultDisablePresharedKey field

    Acceptance Criteria:

    • npm run build passes
    • Toggle exists in server edit form
    • Toggle sent in server update request

    QA Scenarios:

    Scenario: Default toggle exists in Servers.vue
      Tool: Bash (grep)
      Steps:
        1. grep -n "default_disable_preshared_key\|DefaultDisablePresharedKey" apps/dashboard-ui/src/views/Servers.vue
      Expected Result: 2+ matches (template + script)
      Evidence: .sisyphus/evidence/task-9-toggle-exists.txt
    
    Scenario: Build passes
      Tool: Bash
      Steps:
        1. cd apps/dashboard-ui && npm run build
      Result: Exit 0
      Evidence: .sisyphus/evidence/task-9-build.txt
    

    Commit: YES (with tasks 7, 8)

    • Message: feat(ui): add default PresharedKey disable setting to server edit form

  • 10. Fix device-agent PresharedKey gap (provisioning.go + wireguard.go)

    What to do: Changes in apps/device-agent/:

    10a. Add PresharedKey to WireGuardConfig struct in apps/device-agent/internal/client/provisioning.go:

    type WireGuardConfig struct {
        PrivateKey  string `json:"private_key"`
        PresharedKey string `json:"preshared_key"`
        InternalIP  string `json:"internal_ip"`
        ServerPub   string `json:"server_pub"`
        Endpoint    string `json:"endpoint"`
        DNS         string `json:"dns"`
    }
    

    10b. Update ConvertToUAPI() in apps/device-agent/internal/tunnel/wireguard.go: Change from:

    return fmt.Sprintf(`private_key=%s
    public_key=%s
    endpoint=%s
    allowed_ip=%s`, ...)
    

    To:

    pskLine := ""
    if presharedKey != "" {
        pskLine = fmt.Sprintf("preshared_key=%s\n", presharedKey)
    }
    return fmt.Sprintf(`private_key=%s
    public_key=%s
    %sendpoint=%s
    allowed_ip=%s`, privateKey, publicKey, pskLine, endpoint, allowedIP)
    

    10c. Update function signature and callers:

    • Change ConvertToUAPI signature to accept presharedKey string
    • In main.go where it's called, pass cfg.PresharedKey

    Must NOT do:

    • Do NOT touch shared/crypto/encryptor.go (known debt, keep duplicated)
    • Do NOT change the provisioning function flow (decrypt, parse, apply — stay the same)

    Parallelization:

    • Can Run In Parallel: YES (with Tasks 7, 8, 9)
    • Parallel Group: Wave 3
    • Blocks: F1-F4
    • Blocked By: Task 1 (conceptual dependency — agent reads Device model changes but doesn't block on compile)

    References:

    • apps/device-agent/internal/client/provisioning.go:28-34 — WireGuardConfig struct (add PresharedKey)
    • apps/device-agent/internal/tunnel/wireguard.go:83-97 — ConvertToUAPI function
    • apps/device-agent/main.go:38 — caller of ConvertToUAPI
    • apps/server-core/api/provisioning.go:138-145 — ConfigPayload struct (already has PresharedKey, confirmed)

    Acceptance Criteria:

    • go build ./... passes (device-agent)
    • WireGuardConfig has PresharedKey field
    • ConvertToUAPI conditionally emits preshared_key when non-empty
    • ConvertToUAPI omits preshared_key when empty

    QA Scenarios:

    Scenario: WireGuardConfig has PresharedKey field
      Tool: Bash (grep)
      Steps:
        1. grep -n "PresharedKey\|preshared_key" apps/device-agent/internal/client/provisioning.go
      Expected Result: Shows PresharedKey string field with json tag
      Evidence: .sisyphus/evidence/task-10-struct-field.txt
    
    Scenario: ConvertToUAPI accepts presharedKey param
      Tool: Bash (grep)
      Steps:
        1. grep -n "func ConvertToUAPI" apps/device-agent/internal/tunnel/wireguard.go
      Expected Result: Shows function signature with presharedKey string param
      Evidence: .sisyphus/evidence/task-10-uapi-sig.txt
    
    Scenario: ConvertToUAPI conditionally emits preshared_key
      Tool: Bash (grep)
      Steps:
        1. grep -B1 -A3 "if presharedKey" apps/device-agent/internal/tunnel/wireguard.go
      Expected Result: Shows conditional pskLine building
      Evidence: .sisyphus/evidence/task-10-uapi-conditional.txt
    
    Scenario: Build passes
      Tool: Bash
      Steps:
        1. cd apps/device-agent && go build ./...
      Expected Result: Exit 0
      Evidence: .sisyphus/evidence/task-10-build.txt
    

    Commit: YES (separate, agent is independent submodule)

    • Message: feat(agent): add PresharedKey support to WireGuard config and UAPI

Final Verification Wave

  • F1. Plan Compliance Auditoracle Read the plan end-to-end. For each Must Have: verify implementation exists. For each Must NOT Have: search codebase for forbidden patterns (refactored Axios, crypto/encryptor.go changes, etc.). Check evidence files exist in .sisyphus/evidence/. Compare deliverables against plan. Output: Must Have [N/N] | Must NOT Have [N/N] | Tasks [N/N] | VERDICT: APPROVE/REJECT

  • F2. Code Quality + Build Testsunspecified-high Run go build ./... on ALL 3 submodules (server-core, dashboard-ui, device-agent). Run npm run build on dashboard-ui. Check for: unused imports, commented-out code, AI slop (excessive comments, over-abstraction). Output: Build [PASS/FAIL] | Lint [PASS/FAIL] | Files [N clean/N issues] | VERDICT

  • F3. Real Manual QAunspecified-high Execute these end-to-end scenarios:

    1. Share link fix: Call POST /api/v1/devices/:id/share → get token → curl GET /share/TOKEN → verify 200 with config_text
    2. PSK creation: POST /api/v1/peers with disable_preshared_key=false → response has PresharedKey. POST with true → no PresharedKey
    3. PSK config: GET /devices/:id/config for both PSK states → verify correct format
    4. PSK toggle: PUT /devices/:id with disable_preshared_key=true → GET device → verify field changed
    5. Server default: Update server setting → create peer on that server → verify inheritance
    6. Share config PSK: Create share for device with PSK disabled → verify share config omits PSK
    7. Agent build: go build ./... in device-agent → verify WireGuardConfig has PresharedKey field Save evidence to .sisyphus/evidence/final-qa/. Output: Scenarios [N/N pass] | Integration [N/N] | VERDICT
  • F4. Scope Fidelity Checkdeep Read the actual diffs for all changed files. Verify 1:1 with spec. No missing, no scope creep. Check Must NOT do compliance. Detect cross-task contamination. Output: Tasks [N/N compliant] | Contamination [CLEAN/N issues] | VERDICT


Commit Strategy

  • Task 2 (standalone): fix(ui): use fetch() for share endpoint to fix /api/v1 prefix bug
  • Task 1 + 3-6: feat: conditional PresharedKey across model, creation, config, share, provisioning, device update
  • Tasks 7-9: feat(ui): add PresharedKey disable toggles to AddPeerModal, DeviceDetail, Servers
  • Task 10 (standalone agent): feat(agent): add PresharedKey support to WireGuard config and UAPI
  • Parent repo: feat: submodule refs for share link fix + PresharedKey disable feature

Success Criteria

Verification Commands

cd apps/server-core && go build ./...
cd apps/dashboard-ui && npm run build
cd apps/device-agent && go build ./...
curl -v http://localhost:8080/share/{VALID_TOKEN}
curl -v http://localhost:8080/api/v1/peers -X POST -d '{"name":"test","wg_server_id":"...","disable_preshared_key":true}'

Final Checklist

  • All "Must Have" present
  • All "Must NOT Have" absent
  • All builds pass
  • Share link: valid token returns 200 with config
  • Share link: expired token returns 404 with error
  • PSK disabled: no PresharedKey in any generated config
  • PSK enabled (default): PresharedKey present in all generated configs
  • Agent: builds with PresharedKey field in WireGuardConfig
  • Agent: ConvertToUAPI conditionally emits preshared_key