From 1ca93316335f276cf1cbbfe287c70c7c694692c6 Mon Sep 17 00:00:00 2001 From: Marvin von Rappard Date: Wed, 23 Sep 2026 21:54:26 +0200 Subject: [PATCH 1/3] cloud: re-sync the proto copy with the cloud's source of truth Brings cloud/proto back to byte-identical with the control plane's copy: the tailnet probe cadence constant (read only by the cloud) takes the cloud's 120s value, ValidateHTTPPath is exported as it is upstream, and the doc.go and constant comments are reconciled so they read true in both repositories. No wire shape changes. --- AGENTS.md | 1 + cloud/proto/doc.go | 27 ++++++++++++++++----------- cloud/proto/messages.go | 23 ++++++++++++----------- cloud/proto/validate.go | 8 ++++++-- 4 files changed, 35 insertions(+), 24 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index a0f519d..0308adf 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -42,3 +42,4 @@ When adding a new feature, include the relevant docs page update and at least on - Do not run tests, start the software, start the dev server, start Docker Compose, or execute migrations unless explicitly asked by the developer. - Do not read entire translation files. Make targeted reads and edits only. - Do not add yourself as a co-author in commits. +- `cloud/proto` is a verbatim copy of the DockTail Cloud wire contract, not code owned here. Never edit it on its own: every change is made identically in the control plane's copy (the source of truth) so the two stay byte-identical, and every wire addition is `omitempty` and backward-compatible. Keep it stdlib-only, and never add exec, deploy, shell, or command message types. diff --git a/cloud/proto/doc.go b/cloud/proto/doc.go index 74a99bc..395b11e 100644 --- a/cloud/proto/doc.go +++ b/cloud/proto/doc.go @@ -1,17 +1,22 @@ -// Package proto defines the wire contract between the DockTail agent (this repo, -// AGPL) and DockTail Cloud's proprietary agent-plane. +// Package proto defines the wire contract between the DockTail agent (OSS, +// AGPL, github.com/marvinvr/docktail) and the DockTail Cloud agent-plane +// (proprietary). // // It is intentionally dependency-free (stdlib only) so it stays a clean // licensing firewall: the AGPL agent and the proprietary cloud both import an -// identical, neutral set of types without either contaminating the other. +// identical, neutral set of types without either contaminating the other. The +// intended end-state is a standalone Apache-2.0 module +// (github.com/marvinvr/docktail-proto) that both sides import. // -// NOTE: this is currently a verbatim copy of docktail-cloud/proto, kept in sync -// by hand. The intended end-state (see that repo's PLAN.md) is a single shared -// Apache-2.0 module (github.com/marvinvr/docktail-proto) that both sides import; -// until that module exists, the two copies MUST be kept byte-identical on the -// wire (same JSON field names and message shapes). +// NOTE: until that module exists, this package lives as two hand-synced copies +// — proto/ in the DockTail Cloud repo (the source of truth) and cloud/proto in +// the agent repo — that MUST stay byte-identical. Every change lands in both, +// and every field added to the wire is omitempty and backward-compatible. The +// cloud's CI diffs its copy against a pinned agent commit. // -// Transport: outbound-only WSS, JSON messages, 30s heartbeat, jittered -// reconnect. The protocol is metadata-only — there are no exec, deploy, or shell -// message types, by design and verifiable in this open source. +// Transport: outbound-only WSS, JSON messages, 30s heartbeat (doubles as +// liveness), jittered reconnect. Every frame is an [Envelope] carrying a +// typed payload. The protocol is metadata-only: there are no exec, deploy, +// or shell message types, on purpose — the non-goals are enforced +// structurally and verifiable in the open agent source. package proto diff --git a/cloud/proto/messages.go b/cloud/proto/messages.go index 70bfa4d..2e4cadf 100644 --- a/cloud/proto/messages.go +++ b/cloud/proto/messages.go @@ -527,9 +527,9 @@ const ( // the customer's own Tailscale API quota from a buggy or hostile cloud. MinTailnetProbeIntervalMS int64 = 60_000 // DefaultTailnetProbeIntervalMS is the cloud's polling cadence per - // (workspace, tailnet). Service state changes on human timescales (an admin - // approving a service), so this is deliberately slow. - DefaultTailnetProbeIntervalMS int64 = 300_000 + // (workspace, tailnet). Only locally-up, container-backed service names are + // included in each sweep, keeping the API load bounded at this cadence. + DefaultTailnetProbeIntervalMS int64 = 120_000 ) // --------------------------------------------------------------------------- @@ -562,11 +562,12 @@ const ( ClassServiceMissing = "service_missing" // no such service definition in the tailnet at all // Public-vantage classes. The cloud produces these from its own HTTPS probe of - // a Funnel exposure (see docs/public-vantage.md). They are namespaced rather - // than reusing the transport classes above because the classification is also - // the incident's, and a bare `timeout` could not say WHICH layer timed out: - // the recovery rules ask "is this outage currently explained by the public - // vantage?" and must never mistake a local probe failure for a Funnel one. + // a Funnel exposure (see DockTail Cloud docs/vantages.md). They are + // namespaced rather than reusing the transport classes above because the + // classification is also the incident's, and a bare `timeout` could not say + // WHICH layer timed out: the recovery rules ask "is this outage currently + // explained by the public vantage?" and must never mistake a local probe + // failure for a Funnel one. ClassPublicDNS = "public_dns" // the funnel hostname does not resolve to a public address ClassPublicTimeout = "public_timeout" // no answer from the funnel within the probe budget ClassPublicRefused = "public_refused" // the funnel ingress refused the connection @@ -575,7 +576,7 @@ const ( // Deprecated: ClassServe was emitted by the removed `tailscale serve` vantage. // Recognized so pre-existing incidents and stored rows still render; never - // produced. See docs/prober.md. + // produced. See DockTail Cloud docs/vantages.md. ClassServe = "serve" ) @@ -702,7 +703,7 @@ const MaxFilesystems = 16 // Log capture modes — the workspace default ([LogConfig.Mode]) and per-service // overrides ([LogConfig.Overrides]) both use these. const ( - LogModeIncident = "incident" // capture the tail on a down-signal event (what the cloud sends unless the workspace turned capture off) - LogModeOff = "off" // never capture; also the fail-closed value for an empty or invalid mode + LogModeIncident = "incident" // capture the tail on a down-signal event (what the cloud sends for a workspace that never set this) + LogModeOff = "off" // never capture; also the fail-closed value for an empty, invalid, or reserved mode (see [SafeLogMode]) LogModeContinuous = "continuous" // reserved: rolling capture, not yet implemented ) diff --git a/cloud/proto/validate.go b/cloud/proto/validate.go index 5c51ab8..9d611ae 100644 --- a/cloud/proto/validate.go +++ b/cloud/proto/validate.go @@ -24,7 +24,7 @@ func ValidateCheckConfig(cfg CheckConfig) error { return fmt.Errorf("invalid expected HTTP status") } if cfg.Kind == "http" { - if err := validateHTTPPath(cfg.Path); err != nil { + if err := ValidateHTTPPath(cfg.Path); err != nil { return err } } @@ -89,7 +89,11 @@ func SafeLogMode(mode string) string { return LogModeOff } -func validateHTTPPath(path string) error { +// ValidateHTTPPath bounds a relative HTTP request path. It is the one rule for +// every path the cloud puts on the wire or dials itself: the check config it +// sends an agent, and the Funnel path the public vantage requests. An empty path +// is valid and means "/". +func ValidateHTTPPath(path string) error { if path == "" { return nil } From 195024583cca5d3396d019aa6550989196f31972 Mon Sep 17 00:00:00 2001 From: Marvin von Rappard Date: Wed, 23 Sep 2026 22:00:02 +0200 Subject: [PATCH 2/3] cloud: carry the http_status class in the proto copy The control plane's copy gained ClassHTTPStatus; mirror the identical lines so this copy stays byte-identical with it. --- cloud/proto/messages.go | 1 + cloud/proto/schema.json | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/cloud/proto/messages.go b/cloud/proto/messages.go index 2e4cadf..6f568f9 100644 --- a/cloud/proto/messages.go +++ b/cloud/proto/messages.go @@ -551,6 +551,7 @@ const ( ClassRefused = "refused" ClassTLS = "tls" ClassHTTP5xx = "http_5xx" + ClassHTTPStatus = "http_status" // HTTP answered, but not with the configured expect_status (and not a 5xx) ClassACLBlocked = "acl_blocked" // reserved for the deferred Control-API ACL audit; not produced by the tailnet vantage ClassContainer = "container" // local down -> container problem diff --git a/cloud/proto/schema.json b/cloud/proto/schema.json index 4725638..7709187 100644 --- a/cloud/proto/schema.json +++ b/cloud/proto/schema.json @@ -134,7 +134,7 @@ "ok": { "type": "boolean" }, "latency_ms": { "type": "integer" }, "status_code": { "type": "integer" }, - "class": { "type": "string", "enum": ["dns", "timeout", "refused", "tls", "http_5xx", "acl_blocked", "serve", "container"] }, + "class": { "type": "string", "enum": ["dns", "timeout", "refused", "tls", "http_5xx", "http_status", "acl_blocked", "serve", "container"] }, "error": { "type": "string" }, "checked_at": { "type": "integer" } } From 71134e4d16c7c0dee3f30777fb924cdca83c8615 Mon Sep 17 00:00:00 2001 From: Marvin von Rappard Date: Wed, 30 Sep 2026 21:27:43 +0200 Subject: [PATCH 3/3] fix(lint): suppress gosec G703 false positive on the operator-set host root Newer golangci-lint ships gosec's taint-based G703, which flags the DOCKTAIL_HOST_ROOT stat in resolveHostRoot. The path is operator-configured on purpose and only stat-ed, so this is not a traversal. --- cloud/hostfs_linux.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cloud/hostfs_linux.go b/cloud/hostfs_linux.go index b37c14b..91ad606 100644 --- a/cloud/hostfs_linux.go +++ b/cloud/hostfs_linux.go @@ -121,7 +121,7 @@ func resolveHostRoot() string { if root == "" || root == "/" { return "" } - if _, err := os.Stat(filepath.Join(root, "proc", "1", "mounts")); err != nil { + if _, err := os.Stat(filepath.Join(root, "proc", "1", "mounts")); err != nil { //nolint:gosec // G703: the operator names the host root via DOCKTAIL_HOST_ROOT on purpose; only stat-ed return "" } return root