# 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/TOKEN` → `ShareConfig.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 = ` - [ ] 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 - [x] 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` --- - [x] 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()`: ```typescript 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:268` — `r.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` --- - [x] 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): ```go // 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): ```go 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: ```go // 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: ```go pskLine := fmt.Sprintf("PresharedKey = %s", device.PresharedKey) if device.DisablePresharedKey || device.PresharedKey == "" { pskLine = "" } ``` Then change the format string: ```go 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: ```go 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` --- - [x] 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: ```go 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` --- - [x] 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): ```go // 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): ```go 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` --- - [x] 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): ```go 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): ```go 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` --- - [x] 7. Add PSK disable toggle to AddPeerModal.vue **What to do**: In `apps/dashboard-ui/src/components/AddPeerModal.vue`: **7a. Add reactive state**: ```typescript const disablePresharedKey = ref(false) ``` **7b. Add checkbox to template** (after the AllowInternet field): ```vue
``` **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` --- - [x] 8. Add PSK disable toggle to DeviceDetail.vue **What to do**: In `apps/dashboard-ui/src/views/DeviceDetail.vue`: **8a. Add reactive state**: ```typescript const disablePresharedKey = ref(false) ``` **8b. Load current state** from device data in `onMounted` or `watch`: ```typescript disablePresharedKey.value = device.disable_preshared_key || false ``` **8c. Add toggle to the device edit form**: ```vue
``` **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` --- - [x] 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: ```typescript const defaultDisablePresharedKey = ref(false) ``` **9b. Load from server data**: ```typescript defaultDisablePresharedKey.value = editingServer.default_disable_preshared_key || false ``` **9c. Add toggle to server edit form** (near peer defaults section): ```vue
``` **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` --- - [x] 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`: ```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: ```go return fmt.Sprintf(`private_key=%s public_key=%s endpoint=%s allowed_ip=%s`, ...) ``` To: ```go 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 - [x] F1. **Plan Compliance Audit** — `oracle` 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` - [x] F2. **Code Quality + Build Tests** — `unspecified-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` - [x] F3. **Real Manual QA** — `unspecified-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` - [x] F4. **Scope Fidelity Check** — `deep` 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 ```bash 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