diff --git a/docs/release-control/v6/internal/subsystems/agent-lifecycle.md b/docs/release-control/v6/internal/subsystems/agent-lifecycle.md index 80bfbb8d9..774494fd2 100644 --- a/docs/release-control/v6/internal/subsystems/agent-lifecycle.md +++ b/docs/release-control/v6/internal/subsystems/agent-lifecycle.md @@ -124,6 +124,7 @@ management, and fleet control surfaces. 7. Preserve canonical token-lifecycle reads in shared `internal/api/` auth/security helpers so lifecycle-adjacent setup and install flows do not revoke a displayed relay pairing token after `lastUsedAt` proves that an already paired device is actively depending on that credential. 8. Preserve backend-owned Pulse Mobile relay runtime credential minting in those same shared `internal/api/` auth/security helpers so lifecycle-adjacent setup and install flows reuse the canonical mobile token route instead of reintroducing wildcard or browser-authored runtime token bundles. 9. Preserve the dedicated backend-owned `relay:mobile:access` capability and its governed backward-compatible route inventory plus the shared helper call sites around it, so lifecycle-adjacent setup and install flows do not widen the mobile device credential back into general AI chat/execute scope ownership. +10. Preserve shipped security-doc guidance in shared lifecycle setup helpers so `internal/api/config_setup_handlers.go` and adjacent install/setup runtime paths point operators at the running build's local security documentation route rather than GitHub `main` links. ## Forbidden Paths @@ -1417,6 +1418,11 @@ Proxmox install commands in `internal/api/config_setup_handlers.go` and configured, the canonical agent-install-command API must return tokenless install transport and must not persist a new API token record just because an operator opened a backend-driven install surface. +That same backend-owned setup/install boundary also owns shipped security-doc +guidance in runtime responses and logs: `internal/api/config_setup_handlers.go` +and adjacent lifecycle setup helpers must not point operators at GitHub +`main` for security instructions that the running build already serves +locally, and should use the shipped `/docs/SECURITY.md` path instead. The same optional-auth continuity must hold after install as well: Unified Agent runtime startup may not reject a blank token unless enrollment is explicitly enabled, agent report transport must omit auth headers when no token is diff --git a/docs/release-control/v6/internal/subsystems/api-contracts.md b/docs/release-control/v6/internal/subsystems/api-contracts.md index cfb32a45c..65079aaf6 100644 --- a/docs/release-control/v6/internal/subsystems/api-contracts.md +++ b/docs/release-control/v6/internal/subsystems/api-contracts.md @@ -184,6 +184,7 @@ Own canonical runtime payload shapes between backend and frontend. 17. Governed frontend API clients open-coding `if (!response.ok) { if (isAPIResponseStatus(...)) throw new Error(...) }` status-to-user-message branches instead of using canonical shared custom-status error helpers 18. Monitoring command-trigger clients open-coding `parseOptionalAPIResponse(response, { success: true }, ...)` success-envelope fallbacks instead of using a canonical shared success-envelope helper 19. Governed frontend API clients open-coding `try/catch` wrappers around `apiFetchJSON(...)` just to map `402` or `404` into `[]`, `{ plans: [] }`, or `null` instead of using canonical shared API-error-status fallback helpers +20. Backend config/settings handlers pointing operator guidance at GitHub `main` docs when the running build already ships that guidance locally under `/docs/` ## Completion Obligations diff --git a/docs/release-control/v6/internal/subsystems/storage-recovery.md b/docs/release-control/v6/internal/subsystems/storage-recovery.md index 4d3fb1feb..dc311637e 100644 --- a/docs/release-control/v6/internal/subsystems/storage-recovery.md +++ b/docs/release-control/v6/internal/subsystems/storage-recovery.md @@ -77,6 +77,7 @@ querying, and the operator-facing storage health presentation layer. 16. Preserve optional-auth tokenless behavior in those same shared backend install-command helpers so adjacent transport surfaces do not implicitly persist API tokens and flip auth-configured state when an operator only requested a Proxmox install command on a token-optional Pulse instance. 17. Preserve backend-owned Pulse Mobile relay runtime credential minting in those same shared `internal/api/` auth/security helpers so storage- and recovery-adjacent transport surfaces do not inherit browser-authored wildcard token bundles when they depend on the canonical security helper layer. 18. Preserve the dedicated backend-owned `relay:mobile:access` capability and its governed backward-compatible route inventory plus the shared helper call sites around it, so storage- and recovery-adjacent transport surfaces do not treat the mobile relay credential as a general AI scope bundle. +19. Preserve shipped local security-doc guidance in shared `internal/api/` config/setup helpers so storage- and recovery-adjacent transport surfaces do not reintroduce GitHub `main` security links when the running build already serves its own local security documentation route. ## Forbidden Paths diff --git a/internal/api/config_handlers_temperature_ssh_test.go b/internal/api/config_handlers_temperature_ssh_test.go index ced9434ff..6d685a336 100644 --- a/internal/api/config_handlers_temperature_ssh_test.go +++ b/internal/api/config_handlers_temperature_ssh_test.go @@ -87,5 +87,11 @@ func TestHandleVerifyTemperatureSSH(t *testing.T) { if !bytes.Contains(rec.Body.Bytes(), []byte("invalid-host-name-for-test")) { t.Error("expected affected node in failure list") } + if !bytes.Contains(rec.Body.Bytes(), []byte("/docs/SECURITY.md")) { + t.Error("expected shipped security doc reference in failure guidance") + } + if bytes.Contains(rec.Body.Bytes(), []byte("github.com/rcourtman/Pulse/blob/main/SECURITY.md")) { + t.Error("expected failure guidance to avoid GitHub main security doc links") + } }) } diff --git a/internal/api/config_setup_handlers.go b/internal/api/config_setup_handlers.go index 1a9e4fb89..47e8f0b22 100644 --- a/internal/api/config_setup_handlers.go +++ b/internal/api/config_setup_handlers.go @@ -837,7 +837,7 @@ func (h *ConfigHandlers) getOrGenerateSSHKeys() SSHKeyPair { if isContainer && !devModeAllowSSH { log.Error().Msg("SECURITY BLOCK: SSH key generation disabled in containerized deployments") log.Error().Msg("Temperature monitoring via SSH is disabled in containerized deployments") - log.Error().Msg("See: https://github.com/rcourtman/Pulse/blob/main/SECURITY.md#critical-security-notice-for-container-deployments") + log.Error().Msg("See: " + shippedSecurityContainerNoticeDocAnchor) log.Error().Msg("To test SSH keys in dev/lab only: PULSE_DEV_ALLOW_CONTAINER_SSH=true (NEVER in production!)") return SSHKeyPair{} } diff --git a/internal/api/config_system_handlers.go b/internal/api/config_system_handlers.go index 580c0b5cb..d65a63d33 100644 --- a/internal/api/config_system_handlers.go +++ b/internal/api/config_system_handlers.go @@ -152,7 +152,7 @@ func (h *ConfigHandlers) handleVerifyTemperatureSSH(w http.ResponseWriter, r *ht response.WriteString(fmt.Sprintf(" • %s\n", node)) } response.WriteString("\n") - response.WriteString("See: https://github.com/rcourtman/Pulse/blob/main/SECURITY.md for detailed SSH configuration options.\n") + response.WriteString("See: " + shippedSecurityDocPath + " for detailed SSH configuration options.\n") } w.Header().Set("Content-Type", "text/plain") diff --git a/internal/api/contract_test.go b/internal/api/contract_test.go index c6a93c3ec..6e4e5aa19 100644 --- a/internal/api/contract_test.go +++ b/internal/api/contract_test.go @@ -5708,6 +5708,15 @@ func TestContract_EmbeddedFrontendWarningUsesCanonicalDevEntrypoints(t *testing. } } +func TestContract_ShippedSecurityDocReferencesStayLocal(t *testing.T) { + if shippedSecurityDocPath != "/docs/SECURITY.md" { + t.Fatalf("expected shipped security doc path, got %q", shippedSecurityDocPath) + } + if shippedSecurityContainerNoticeDocAnchor != "/docs/SECURITY.md#critical-security-notice-for-container-deployments" { + t.Fatalf("expected shipped security container notice path, got %q", shippedSecurityContainerNoticeDocAnchor) + } +} + func mustStreamEvent(t *testing.T, eventType string, data interface{}) chat.StreamEvent { t.Helper() diff --git a/internal/api/docs_links.go b/internal/api/docs_links.go new file mode 100644 index 000000000..ffdb47112 --- /dev/null +++ b/internal/api/docs_links.go @@ -0,0 +1,6 @@ +package api + +const ( + shippedSecurityDocPath = "/docs/SECURITY.md" + shippedSecurityContainerNoticeDocAnchor = shippedSecurityDocPath + "#critical-security-notice-for-container-deployments" +)