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— usefetch()at root path, not Axios- PSK disable: Device model
DisablePresharedKey bool, WgServer modelDefaultDisablePresharedKey bool+ all creation/generation paths + frontend toggles- Agent PSK:
WireGuardConfig.PresharedKeyfield + UAPI emissionEstimated 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
- Bug: Share config link selalu expired — recipient always sees "Link Expired or Invalid"
- Feature: Peer dapat disable PresharedKey; server default disable PSK untuk peer baru
Metis Analysis Key Findings
- Share bug root cause: URL mismatch —
ShareConfig.vuecallsapi.get('/share/:token')which adds/api/v1prefix via Axios baseURL, but server route is at rootGET /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:falsetags; existing rows getfalse - Agent: Missing
PresharedKeyfield inWireGuardConfigstruct; needs to handle empty gracefully
Current State
- Share link:
PeerConfigModal.vuecreates share at admin dashboard → generates URL like/share/TOKEN→ recipient opens → SPA at/share/TOKEN→ShareConfig.vuecallsapi.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-agentWireGuardConfigstruct doesn't parse it. - WireGuard kernel:
wgmanager_linux.goalready handles empty PresharedKey correctly (zero-value key = no PSK)
Work Objectives
Core Objective
- Fix share link so recipients can actually retrieve the config
- Allow admin to disable PresharedKey per-peer and set server-wide default
Concrete Deliverables
apps/dashboard-ui/src/views/ShareConfig.vue— usefetch()instead of Axiosapps/server-core/internal/models/models.go— addDisablePresharedKeyto Device,DefaultDisablePresharedKeyto WgServerapps/server-core/api/peers.go— conditional PSK in CreatePeer + getDeviceConfigapps/server-core/api/devices.go— DisablePresharedKey in UpdateDeviceRequest + handlerapps/server-core/api/share.go— conditional PSK in share configapps/server-core/api/provisioning.go— conditional PSK generation + payloadapps/dashboard-ui/src/components/AddPeerModal.vue— PSK toggleapps/dashboard-ui/src/views/DeviceDetail.vue— PSK toggleapps/dashboard-ui/src/views/Servers.vue— default PSK settingapps/device-agent/internal/client/provisioning.go— add PresharedKey fieldapps/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 buildpasses (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
apiinstance orVITE_API_BASE_URL - Do NOT change server routing structure (
main.goroute 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:- Add to
Devicestruct:DisablePresharedKey bool \json:"disable_preshared_key" gorm:"default:false"`` - Add to
WgServerstruct:DefaultDisablePresharedKey bool \json:"default_disable_preshared_key" gorm:"default:false"``
- Add to
main_dev.goalready runs GORM AutoMigrate with-tags dev. Thedefault:falsetag ensures existing rows getfalse(not NULL).- Note: GORM
default:falseon bool works — existing rows will backfill correctly.
Must NOT do:
- Do NOT run
ALTER TABLEmanually — AutoMigrate handles this - Do NOT touch
models_test.goormigrations.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.txtCommit: YES (with Tasks 3-6)
- Message:
feat(model): add DisablePresharedKey flags to Device + WgServer
- In
-
2. Fix share link URL mismatch in ShareConfig.vue
What to do:
- In
apps/dashboard-ui/src/views/ShareConfig.vue:- Remove
import { api } from '../services/api'(line 36) - Change
load()function (lines 43-52) to usefetch()instead ofapi.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 } } - Remove
- Why: The
apiAxios instance hasbaseURL = VITE_API_BASE_URLwhich includes/api/v1prefix. Share endpoint is at rootGET /share/:token. Usingfetch()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.goroutes - 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:268—r.GET("/share/:token", shareHandler.GetShare)— root level, not under /api/v1.
Acceptance Criteria:
npm run buildpasses- 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.txtCommit: YES (separate)
- Message:
fix(ui): use fetch() for share endpoint to fix URL mismatch /api/v1 prefix bug
- In
-
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
DisablePresharedKeytoCreatePeerRequeststruct (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
pskLineis empty, the blank line with%swill 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 structapps/server-core/api/peers.go:76-80— PSK generation in CreatePeerapps/server-core/api/peers.go:226-246— getDeviceConfig config text generationapps/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.txtCommit: YES (with tasks 1, 4, 5, 6)
- Message:
feat(api): conditional PresharedKey generation and config output
- Do NOT touch
-
4. Conditionally include PresharedKey in share config (share.go)
What to do:
- In
apps/server-core/api/share.go, theCreateShare()function generates config text at lines 71-88. - Replace the hardcoded
PresharedKey = %swith 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.txtCommit: YES (with tasks 1, 3, 5, 6)
- Message:
feat(api): conditional PresharedKey in share config
- In
-
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 = pskStr5b. 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.PresharedKeyis 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
ConfigPayloadstruct (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 blockapps/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.txtCommit: 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 structapps/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.txtCommit: YES (with tasks 1, 3, 4, 5)
- Message:
feat(api): handle DisablePresharedKey in device update handler
- Do NOT touch
-
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.valuein 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.txtCommit: 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
onMountedorwatch:disablePresharedKey.value = device.disable_preshared_key || false8c. 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.valuein 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 sectionapps/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.txtCommit: 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 || false9c. 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.valuein 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.txtCommit: 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
ConvertToUAPIsignature to acceptpresharedKey string - In
main.gowhere it's called, passcfg.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 functionapps/device-agent/main.go:38— caller of ConvertToUAPIapps/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.txtCommit: YES (separate, agent is independent submodule)
- Message:
feat(agent): add PresharedKey support to WireGuard config and UAPI
- Change
Final Verification Wave
-
F1. Plan Compliance Audit —
oracleRead 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 Tests —
unspecified-highRungo build ./...on ALL 3 submodules (server-core, dashboard-ui, device-agent). Runnpm run buildon 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 QA —
unspecified-highExecute these end-to-end scenarios:- Share link fix: Call POST /api/v1/devices/:id/share → get token → curl GET /share/TOKEN → verify 200 with config_text
- PSK creation: POST /api/v1/peers with disable_preshared_key=false → response has PresharedKey. POST with true → no PresharedKey
- PSK config: GET /devices/:id/config for both PSK states → verify correct format
- PSK toggle: PUT /devices/:id with disable_preshared_key=true → GET device → verify field changed
- Server default: Update server setting → create peer on that server → verify inheritance
- Share config PSK: Create share for device with PSK disabled → verify share config omits PSK
- 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 Check —
deepRead 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