diff --git a/dag/extdeps/sudo/nopasswd_execute_probe_check_op.dag b/dag/extdeps/sudo/nopasswd_execute_probe_check_op.dag index d4dd49f2584..dbc4cc19906 100644 --- a/dag/extdeps/sudo/nopasswd_execute_probe_check_op.dag +++ b/dag/extdeps/sudo/nopasswd_execute_probe_check_op.dag @@ -90,6 +90,11 @@ fn sudo_nopasswd_execute_probe_shell_condition(probe: SudoNopasswdExecuteProbeSh // binary not yet installed would read as a grant miss (the reason this probe kind exists). data sudo_nopasswd_grant_list_tag: String = "NOPASSWD:" +// THE LAST STAGE READS THE WHOLE LISTING. `grep -q` exits at its first match while sed is still +// writing, and under pipefail the writer's SIGPIPE (141) fails the pipeline -- so a grant that IS +// listed early in a long listing read as absent, every time once the listing outgrew the pipe +// buffer (srv1, 2026-10-10: ~13 KB after the gunbc-ghrunner drop-in, 20/20 runs exit 141). `grep -Fx` +// with stdout discarded consumes all of it and still answers by its exit status alone. fn sudo_nopasswd_grant_list_shell_condition(probe: SudoNopasswdGrantListProbeShape) -> Node { let tag = sudo_nopasswd_grant_list_tag bash_build_pipe( @@ -103,7 +108,10 @@ fn sudo_nopasswd_grant_list_shell_condition(probe: SudoNopasswdGrantListProbeSha left: bash_build_command_from_lits(lits: ["sed", "-n", concat(concat("s/^.*", tag), "//p")]), right: bash_build_pipe( left: bash_build_command_from_lits(lits: ["sed", "s/^[[:space:]]*//;s/[[:space:]]*$//"]), - right: bash_build_command_from_lits(lits: ["grep", "-Fqx", "--", probe as String]) + right: bash_build_with_redir( + inner: bash_build_command_from_lits(lits: ["grep", "-Fx", "--", probe as String]), + redir: ^bash_production_fold_redir_stdout_null + ) ) ) ) diff --git a/dag/gunbc/live_deploy/spec.dag b/dag/gunbc/live_deploy/spec.dag index f1dc7a6a515..085c6e69c1f 100644 --- a/dag/gunbc/live_deploy/spec.dag +++ b/dag/gunbc/live_deploy/spec.dag @@ -49,6 +49,7 @@ import gunbc.roadmap_dashboard_instance { HostDashboardInstance, dashboard_instance_provider_state_root, dashboard_instance_tailnet_door_socket, + dashboard_instance_tailnet_door_directory, } import extdeps.http.server { HttpServerListenConfig } import gunbc.auth.approval_broker_endpoint { approval_broker_listen_port, approval_broker_listen_host, approval_broker_confirm_front_door } @@ -1334,7 +1335,18 @@ fn deployment_spec_for( ), names: names, instance_id: instance.instance_id, - directories: dashboard_instance_directory_demands(instance: instance), + directories: concat(dashboard_instance_directory_demands(instance: instance), deployment_tailnet_door_directories(instance: instance, names: names)), + } +} + +// THE DOOR'S DIRECTORY IS A DEPLOYMENT DEMAND, NOT AN INSTANCE ONE: only a deployment whose serve +// route targets a unix socket runs a door (deployment_tailnet_door_socket), and only the spec knows +// the route. It joins the instance's own list here, so the one provider ensures and reads it back +// before the door unit binds; a deployment routed to a port demands nothing. +fn deployment_tailnet_door_directories(instance: HostDashboardInstance, names: DeploymentNames) -> List { + match deployment_tailnet_door_socket(names: names) { + Present { value: _ } => [dashboard_instance_tailnet_door_directory(instance: instance)] + Absent => [] } } diff --git a/dag/gunbc/roadmap/dashboard_instance_directories.dag b/dag/gunbc/roadmap/dashboard_instance_directories.dag index cb85a7794ec..c1798d2848d 100644 --- a/dag/gunbc/roadmap/dashboard_instance_directories.dag +++ b/dag/gunbc/roadmap/dashboard_instance_directories.dag @@ -26,6 +26,11 @@ import gunbc.fabric_storage_placement { fabric_storage_directory_demands } // writes, at which point this list stops being authored. Until then, adding a directory writer means // declaring its demand in its own module and adding one row here. // +// A DEPLOYMENT'S OWN DEMANDS ARE NOT INSTANCE CONSUMERS: the tailnet door's directory exists only +// where a deployment's serve route targets a unix socket, which the instance does not know, so +// gunbc.live_deploy.spec deployment_spec_for appends it (deployment_tailnet_door_directories) to this +// list for the live_deploy route. The dashboard apply runs no door and reads this list alone. +// // It lives above the consumers rather than in gunbc.roadmap_dashboard_instance because every consumer // imports the instance type from there; composing them there would close a cycle. fn dashboard_instance_directory_demands(instance: HostDashboardInstance) -> List { diff --git a/dag/gunbc/roadmap/roadmap_dashboard_instance.dag b/dag/gunbc/roadmap/roadmap_dashboard_instance.dag index 15f25d1841b..6f328f5324e 100644 --- a/dag/gunbc/roadmap/roadmap_dashboard_instance.dag +++ b/dag/gunbc/roadmap/roadmap_dashboard_instance.dag @@ -316,11 +316,20 @@ fn dashboard_instance_child(root: FilePath, child: String) -> FilePath { join([root as String, "/", child], "") as FilePath } // THE INSTANCE'S TAILNET DOOR: the unix socket its door unit serves on and tailscale serve proxies -// to. It sits in the instance root, which the service principal owns; who may open it is not what -// the door relies on -- the handler is handed the kernel-attested peer of every connection -// (`gunbc serve --unix-socket`, peer_user) and only tailscaled's is the proxy. +// to. It sits in a directory of its own under the instance root, which the deployment ensures owned +// by the service principal: the instance root itself need not be the principal's to write (srv1's +// /opt/gunbc is root-owned 0755 and the deploy re-asserts it), so a socket bound directly in it +// could not be created and readiness, which requires the door, could never hold (srv1 precondition +// sweep, 2026-10-10) -- the defect roadmap_event_carrier_directory answered for the event worktree. +// Who may open the socket is not what the door relies on -- the handler is handed the +// kernel-attested peer of every connection (`gunbc serve --unix-socket`, peer_user) and only +// tailscaled's is the proxy. +fn dashboard_instance_tailnet_door_directory(instance: HostDashboardInstance) -> OwnedDirectoryTarget { + dashboard_instance_shared_directory(instance: instance, member: "tailnet-door" as NonEmptyStr, child: "tailnet-door") +} + fn dashboard_instance_tailnet_door_socket(instance: HostDashboardInstance) -> NonEmptyStr { - dashboard_instance_child(root: instance.instance_root, child: "tailnet-door.sock") as String as NonEmptyStr + join([owned_directory_path(t: dashboard_instance_tailnet_door_directory(instance: instance)) as String, "/door.sock"], "") as NonEmptyStr } fn dashboard_instance_provider_state_root(instance: HostDashboardInstance) -> FilePath {