Skip to content

feat(dashboard): accept this machine's own tailnet origin without hand-writing dashboard.url - #1908

Merged
iamwhatever merged 1 commit into
mainfrom
feat/tailnet-origin
Aug 7, 2026
Merged

feat(dashboard): accept this machine's own tailnet origin without hand-writing dashboard.url#1908
iamwhatever merged 1 commit into
mainfrom
feat/tailnet-origin

Conversation

@CrysisDeu

@CrysisDeu CrysisDeu commented Aug 6, 2026

Copy link
Copy Markdown
Collaborator

Phase 2 of the tailnet RFC (docs/request-for-change/rfc-tailnet-dashboard-access.md §4). Off by default. Does not widen the network bind and does not touch authentication.

Problem

tailscale serve is the right way to reach the dashboard from a phone — tailnet-only, TLS, a stable MagicDNS hostname, access governed by tailnet ACLs. Phase 1 (#1761) documented it. But making it work still takes a manual step that fails obscurely when skipped:

tailscale serve --bg --https 443 http://127.0.0.1:5476
# ...then open https://desk.tail1a2b3c.ts.net and get a bare 403

The 403 comes from check_host(), the DNS-rebinding barrier. The origin allowlist is built from loopback plus dashboard.url, so a tailnet name is not in it. The response does not say which allowlist rejected the request or how to extend it — the user has to know to look up their own MagicDNS name and hand-write it into dashboard.url.

Why it matters

This is the one remote-access path that is private by construction, and the friction to reach it is "find a hostname you don't know, put it in a config file you haven't opened, and decode a 403 that names nothing". The RFC calls this out as Problem 3.

Fix (symptom → root cause → change)

The symptom is an unexplained 403. The root cause is that the origin set has no way to learn a name the local Tailscale daemon already knows. So the daemon is asked, once, at startup.

dashboard/tailnet.py (new) — a read-only interface to the daemon. Two properties carry the whole design:

1. Nothing raises. Missing binary, stopped daemon, timeout, non-zero exit, malformed JSON, unexpected schema — every path returns None. The dashboard must start on a host that has never heard of Tailscale, so this is pure enrichment: it contributes a name or contributes nothing.

2. The name is validated before it is returned. It arrives from a subprocess and its destination is the CSRF origin set and the Host barrier, so an unvalidated value is an origin-injection primitive. _valid_magicdns_name() is an allowlist, not a denylist — the question is not "does this look dangerous" but "is this provably a bare tailnet hostname":

Rejected Why
non-string, empty, >253 bytes not a hostname
anything with / : @ ? #, whitespace, backslash scheme / port / path / userinfo / injection
any uppercase MagicDNS names are lowercase; a mixed-case value did not come from where we think
evil.example.com, desk.tail.ts.net.evil.com valid hostnames, not tailnet names
-bad.tail.ts.net, ts.net malformed label / bare suffix

The second check is suffix self-consistency, not a hardcoded .ts.net: the name must sit under the tailnet's own CurrentTailnet.MagicDNSSuffix as reported by the same status output. Upstream documents that suffix as tailnet-specific and its own example is userfoo.tailscale.net, so a hardcoded list would reject legitimate tailnets and would rot as Tailscale adds suffixes. A consequence worth stating plainly: a self-hosted control plane (Headscale) on its own suffix is accepted, with no special case — test_accepts_a_self_hosted_suffix pins that. CurrentTailnet is nil when the node is not connected, which lands here as a missing suffix and is refused: no tailnet means no origin to add.

build_allowed_origins() stays a pure function. It gains a tailnet_host parameter, not a lookup — the caller owns the daemon call and the validation. The docstring says so explicitly, so the next reader does not move the subprocess into it and so nothing may pass an unvalidated value in. build_allowed_hosts() is unmodified: the Host allowlist derives from the origin set, so the barrier follows for free. That is the RFC's single-source-of-truth invariant and a test pins it.

The daemon call never runs on the event loop. resolve_tailnet_host() short-circuits before the thread hop when the feature is off, and offloads to asyncio.to_thread when it is on. tailscale status genuinely blocks while the daemon is coming up, and a multi-second inline stall would freeze every other session and can trip the loop-stall watchdog. A test asserts the probe runs on a non-main thread.

Both startup paths wiredstart_dashboard and start_api_server. The dashboard_url re-derivation inside start_dashboard also passes tailnet_host; without that it would silently drop the tailnet origin for anyone who sets both.

What is deliberately NOT here

  • The inferred opt-in. The RFC's §4 signal 2 (infer from tailscale serve status instead of a config flag) is gated on an open question — whether every widening of the origin allowlist must be explicit config. Only the explicit dashboard.tailscale.enabled flag ships, which the RFC says can land without that answer.
  • Phase 3. The session pin is still inert behind every same-host tunnel (issue Session pin is inert behind every documented tunnel (cloudflared / ngrok / tailscale serve) #1762). This PR does not change any auth decision, binding, comparison, or TTL. Phase 1's posture row and guide warning remain the honest signal until Phase 3 lands.
  • Any bind change. is_local_only() still returns True; the gateway still binds loopback. tailscale serve proxies to it.

Tests

test/test_tailnet_origin.py, 47 tests, weighted toward the security surface:

  • Validation as an injection-rejection suite — 17 rejected forms above, each named for what it represents rather than lumped into one parametrize; plus non-strings and the length cap.
  • Failure modes all return None and none raise — CLI absent, TimeoutExpired, OSError on exec, non-zero exit, and seven shapes of unusable stdout (empty, garbage, wrong top-level type, no Self, Self not a dict, no DNSName). One test specifically feeds a hostile name through otherwise-valid output: the daemon is trusted to be the daemon, not to be well-behaved.
  • Entry point — disabled never touches the daemon (asserted via assert_not_called, so the short-circuit can't rot into a wasted thread hop); enabled resolves off the event loop; enabled-but-unresolvable yields "".
  • Origin set — absent by default; contributed as https:// with no port; the Host allowlist follows; coexists with dashboard.url; the loopback floor is untouched. Absence is stated as set arithmetic (contributed - baseline == {…}), which also pins that the opt-in adds exactly one origin and no stray :port variant.
  • The config opt-in actually survives load and save (test/test_config_loader.py) — DashboardConfig is built field-by-field, so a nested section nobody wires up is dropped silently: the documented config set dashboard.tailscale.enabled true would read back False and then be rewritten to false by the next unrelated save(), leaving the feature inert while looking configured. Both halves are pinned, plus malformed-section degradation.
  • An explicit opt-in that resolves to nothing warns — debug-level silence is right for a host that never opted in, but for one that did it reproduces the same bare 403 with nothing above debug saying why. The off path is separately asserted to stay silent, which is what keeps the warning meaningful.

Manual verification

N/A on this host, and worth stating plainly: this dev desk has no Tailscale installed, so the happy path is exercised only against a mocked daemon. What that leaves unverified is the real shape of tailscale status --json on a live tailnet — the code reads Self.DNSName, and if a future CLI moved it the result is None (feature silently contributes nothing), not a crash. Everything else — the validator, every failure mode, the threading, and the origin/host wiring — is covered above.

Gates

pytest 436 passed (new file + test_dashboard_origin / test_config_loader / test_config_baseline / test_config_schema / test_spawn_audit), isort, flake8 -j 1, mypy 1.14.1 (732 files) — clean. docs-lint, brand-lint, scrub-lint green. config-baseline.json regenerated; the diff is only the two new entries.

test_spawn_audit.py requires every subprocess spawn under src/kiro_crew to route through sandboxed_spawn_argv or be listed in BENIGN_SPAWNS with a justification. dashboard/tailnet.py::_run_json is listed rather than routed, because routing it would make dashboard startup depend on sandbox availability — precisely the failure this module's "nothing raises" property exists to rule out. Being listed is not a free pass, so the spawn is hardened on both axes it could be reached through:

  • The binary is pinned, not looked up. Resolution walks a vetted absolute allowlist (/usr/bin, /usr/local/bin, /opt/homebrew/bin, the macOS app bundle, the Windows install dir) and never consults PATH. A PATH lookup made the executable attacker-selectable even though the arguments never were: ~/.local/bin is both on PATH and agent-writable, so a planted tailscale would be executed by the next gateway start, in-process, with the gateway's environment. A non-standard install is therefore not auto-derived and keeps using explicit dashboard.url.
  • The environment is scrubbed. env=sandbox.scrub_env() rather than inheriting os.environ — the repo's own scrubber, not a second policy invented here, so it cannot drift and stays cross-OS safe.

One fixed argv (status --json), 3s timeout, no shell, no cwd. TestSpawnHardening pins all of it, including that shutil.which is never called and that the passed env is non-empty (an over-scrub that strips HOME / SystemRoot breaks the macOS and Windows CLIs).

@CrysisDeu
CrysisDeu requested a review from a team as a code owner August 6, 2026 22:15
@CrysisDeu
CrysisDeu force-pushed the feat/tailnet-origin branch from dd4b0a1 to 4a92d4d Compare August 6, 2026 22:22
@CrysisDeu

Copy link
Copy Markdown
Collaborator Author

Self-correction before review, pushed as 4a92d4db (was dd4b0a1e).

The PR body admitted one unverified claim: this dev desk has no Tailscale, so "the code reads Self.DNSName" was asserted from documentation habit rather than checked. Checking it against upstream tailscale.com/ipn/ipnstate v1.102.2 confirmed the field path — and surfaced a real defect in the same read.

Status.CurrentTailnet.MagicDNSSuffix is documented as tailnet-specific, and upstream's own example is userfoo.tailscale.net — not ts.net. The first revision hardcoded a .ts.net suffix requirement, which would have rejected legitimate tailnets whose MagicDNS suffix is something else, and would rot as Tailscale adds suffixes. The "deliberate Headscale exclusion" I documented as a design choice was really a consequence of a check that was too blunt.

Replaced the hardcoded suffix with self-consistency against the suffix the daemon reports in the same status output:

  • the name must sit under CurrentTailnet.MagicDNSSuffix, so a different tailnet's name is refused (desk.other9z8y7x.ts.net against suffix tail1a2b3c.ts.net → rejected);
  • CurrentTailnet is nil until the node joins a tailnet, which lands as a missing suffix and is refused — no tailnet means no origin to add;
  • the upstream-deprecated top-level MagicDNSSuffix is read only as a fallback for an older daemon, never as the primary;
  • the suffix is normalised (strip / strip dots / lowercase) rather than trusted to arrive in the documented "no surrounding dots" shape.

This is strictly stronger than the hardcoded version — it checks "is this name on the tailnet we are actually on" instead of "does this name end in a string I guessed" — and it removes the Headscale special case rather than documenting it.

The structural half of the validator is unchanged, and it is the part that defends against origin injection: scheme, port, path, userinfo, whitespace, backslash, uppercase, malformed labels and the 253-byte cap are all still rejected.

Two other things the upstream read confirmed rather than changed:

  • PeerStatus.DNSName is documented as "the Peer's FQDN. It ends with a dot." — so the .rstrip(".") is required by the contract, not defensive guesswork.
  • Self is *PeerStatus and may be nil, which the existing isinstance(..., dict) guard already covers.

Tests grew 47 → 51 and now pin the behaviour that changed: a .tailscale.net tailnet is accepted, a self-hosted suffix is accepted, a name from a different tailnet is refused, a logged-out node yields nothing, and the deprecated-suffix fallback works. Gates re-run green: 426 pytest across the five affected files, isort, flake8 -j 1, mypy (732 files), docs-lint, brand-lint, scrub-lint.

Still unverified, and unchanged by this: no live tailnet round-trip. That needs a host with root (this one denies sudo outright — no new privileges is set) and a Tailscale account to join, so it is the owner's call rather than something I can close here.

Comment thread test/test_tailnet_origin.py Fixed
Comment thread test/test_tailnet_origin.py Fixed
@CrysisDeu
CrysisDeu force-pushed the feat/tailnet-origin branch from 4a92d4d to 5e6a507 Compare August 7, 2026 00:02
@CrysisDeu

Copy link
Copy Markdown
Collaborator Author

CodeQL disposition — 4a92d4db5e6a5070

CodeQL reported 2 high py/incomplete-url-substring-sanitization alerts, both in
test/test_tailnet_origin.py. They are not the same kind of finding, so they get
different dispositions. Neither indicates a sanitization defect in shipped code —
dashboard/tailnet.py validates the MagicDNS name structurally and against the
daemon-reported suffix before it ever reaches the origin set, and that logic is
untouched here.

Alert 575 — test/test_tailnet_origin.py:245accepted, fixed

assert not any("ts.net" in o for o in origins)

o is a single origin string, so this really is a substring test against a URL —
the exact shape the rule exists to catch. It was also a weak assertion: it only
proved that no origin contains ts.net, which is not what the test is for.

Replaced with set arithmetic, which pins strictly more and inspects no strings:

baseline = build_allowed_origins(5476, local_only=True)
contributed = build_allowed_origins(5476, local_only=True, tailnet_host=_GOOD)
assert contributed - baseline == {f"https://{_GOOD}"}

This now fails if the default set contributes anything tailnet-ish or if the
opt-in contributes a second entry (e.g. a stray :5476 variant) — two properties
the old assertion could not distinguish.

Alert 576 — test/test_tailnet_origin.py:266rebutted (code still changed, see below)

assert "https://crew.example.com" in origins

build_allowed_origins is declared -> set[str] (src/kiro_crew/dashboard/urls.py:382-388),
so in here is exact set membership, not a substring scan. The alert comes
from CodeQL losing the set[str] and treating origins as a str; there is no
incomplete sanitization to fix, and the assertion was already exact.

I am not dismissing the alert. The line is rewritten anyway, because making the
collection semantics explicit at the call site is an improvement independent of
the scanner:

assert origins.issuperset({f"https://{_GOOD}", "https://crew.example.com"})

That states "both origins are present" as one set operation and removes the
ambiguity CodeQL tripped on. If the alert re-fires on the new form, the correct
resolution is a rule-level false-positive report, not a change to the assertion's
meaning.

Verification on 5e6a5070

pytest -n0 on test_tailnet_origin.py + test_dashboard_origin.py +
test_config_loader.py + test_config_baseline.py + test_config_schema.py:
426 passed. isort --check-only 0, flake8 -j 1 0, mypy 0 (732 files),
docs-lint 0, brand gate 0, scrub-lint --no-history 0. Base is current
(merge-base == origin/main == 429cbad8). Still one commit.

Unrelated observation

On 4a92d4db this PR received only the CodeQL lanes — no CI, Build, Code
Review, GPT/Opus/Design/UX, and no PR Readiness status — and
gh run list --branch feat/tailnet-origin returned nothing. Repo-wide there were
23 queued and 5 pending runs at the time, so the likeliest explanation is Actions
scheduling rather than anything about this branch. Flagging it so the lane
coverage on 5e6a5070 gets a second look; if the reviewer lanes stay absent, this
PR has not actually been validated regardless of what the check list shows.

@github-actions github-actions Bot added the readiness: checking Automated validation is still running label Aug 7, 2026
@github-actions

github-actions Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Design Review (Fable 5) — ✅ PASS

Advisory design-level review of 09d312b981f5ac31101a4fde18d55dc175fab8fe — updated in place on each push; does not block merge.

Design-Verdict: PASS

Opt-in, validated, fail-silent enrichment at the right layer; the origin stays single-sourced and the change is fully reversible via one config flag.

Suggestions

  • Resolution runs exactly once at startup, so on boot-autostart hosts where the gateway wins the race against tailscaled, the feature silently reverts to the very 403 it exists to remove until a manual restart — a short bounded retry (or one lazy re-resolve on first failed origin check) would close that gap without changing the design.
  • start_dashboard and start_api_server each independently shell out to the daemon; resolving once and threading the result through both would avoid a second subprocess and the possibility of the two servers holding different origin sets.

[DESIGN-REVIEWED] 09d312b

@github-actions

github-actions Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

GPT 5.6 Review — ✅ no blocking findings

GPT 5.6 completed its review of 09d312b981f5ac31101a4fde18d55dc175fab8fe and found no blocking issues.

This comment is updated in place on each push.

Review details

No findings.
[GPT-REVIEWED] 09d312b

False positive or not applicable? A repository writer can comment:
/ai-review override gpt 09d312b981f5ac31101a4fde18d55dc175fab8fe: <one-sentence reason>

@github-actions

github-actions Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Opus 5 Review — ✅ no blocking findings

Reviewed 09d312b981f5ac31101a4fde18d55dc175fab8fe — this comment is updated in place on each push.

Review details

No findings.

[OPUS-REVIEWED] 09d312b

Verdict parsed from the review's SHA-scoped output markers for commit 09d312b981f5ac31101a4fde18d55dc175fab8fe.

False positive or not applicable? A repository writer can comment:
/ai-review override fable 09d312b981f5ac31101a4fde18d55dc175fab8fe: <one-sentence reason>

@github-actions github-actions Bot added readiness: action required A blocking check or review needs attention and removed readiness: checking Automated validation is still running labels Aug 7, 2026
@CrysisDeu
CrysisDeu force-pushed the feat/tailnet-origin branch from 5e6a507 to 23c7d3d Compare August 7, 2026 01:15
@CrysisDeu

Copy link
Copy Markdown
Collaborator Author

Disposition — 5e6a507023c7d3d6

Opus BLOCKING + the same GPT finding — accepted, fixed

src/kiro_crew/config/loader.py:4879 — verified exactly as reported. DashboardConfig(...)
is built field-by-field and had no tailscale=, and grep -n tailscale src/kiro_crew/config/loader.py matched only the dataclass definition. So
dashboard.tailscale.enabled in config.json was dropped on load, server.py:2937
always read False, resolve_tailnet_host always short-circuited, and no tailnet
origin was ever added — the feature this PR exists to add was inert. The second
half is worse than inert: to_dict() does asdict(self.dashboard), which
re-serializes the default, so the next save() on any dashboard write path would
rewrite the operator's true back to false.

Fixed by hydrating it in the same call:

tailscale=TailscaleConfig(
    enabled=_safe_bool(
        _safe_dict(dashboard_data.get("tailscale")).get("enabled"), False
    ),
),

_safe_dict / _safe_bool keep the module's existing contract that a malformed
config value degrades to the default rather than raising.

Regression test (test/test_config_loader.py::test_dashboard_tailscale_hydrates_and_survives_a_round_trip)
pins both halves — hydration, and _load_from_dict(enabled.to_dict()) still True,
which is the assertion that would have caught the erasure — plus malformed sections
("yes", 1, [], None, and {"enabled": "true"}) degrading to False.

Two independent reviewers reaching the same defect from different directions is the
signal here: the flag was documented and wired at the consumer, and nobody checked
the one step in between.

Backend Tests (… , 4) × 3 — accepted, fixed

test/test_spawn_audit.py::test_every_spawn_is_routed_or_allowlisted failed with
New unrouted subprocess spawn(s) found in src/kiro_crew: dashboard/tailnet.py::_run_json.
A repo security invariant this PR broke: every spawn must route through
sandboxed_spawn_argv or be listed in BENIGN_SPAWNS with a justification.

Listed, not routed, and the reasoning is written at the entry: one fixed argv
(["<tailscale>", "status", "--json"]), 3s timeout, no shell, no cwd, no
agent-influenced argument — every element is a module constant, and the binary comes
from shutil.which plus a fixed fallback tuple. Routing it would make dashboard
startup depend on sandbox availability, which is exactly the failure mode this
module's "nothing raises, the gateway still boots" property exists to rule out.
Mirrors the existing diagnostics.py::_kiro_cli_version entry.

Design Review — "startup-once + debug-only failure" — accepted, fixed

Correct as stated. Debug-level silence is right for a host that never opted in, but
the operator who set dashboard.tailscale.enabled=true and hit the boot race got
the same bare 403 this feature removes, with nothing above debug explaining it.
resolve_tailnet_host now logs at warning when enabled-but-unresolved, naming
tailscale status and the restart. Two tests: the warning fires when enabled, and
the disabled path stays silent — the second is what keeps the first meaningful.

Lazy re-resolution is left as the follow-up the review suggests; it is a behaviour
change (when to re-probe, and what to do about an origin set already handed to the
server) rather than a log-level fix, and it belongs with Phase 3's own review.

Design Review — "description ↔ diff mismatch" — accepted, description fixed

The body claimed requiring .ts.net was "a deliberate narrowing" and that Headscale
is "not auto-derived". That was stale from an earlier revision that did hardcode
the suffix; it was replaced with self-consistency against the daemon-reported
CurrentTailnet.MagicDNSSuffix after checking upstream's own example
(userfoo.tailscale.net), and test_accepts_a_self_hosted_suffix pins the opposite
of what the description said. The description now states the actual policy and calls
out self-hosted acceptance explicitly. A reviewer approving on the old text would
have been approving a narrower feature than the one that ships.

Design Review suggestion — "resolve once, not twice" — declined, with reason

start_dashboard and start_api_server do each resolve independently, so the
"reads the daemon once at startup" phrasing is loose. I am not adding a
process-global memo: it needs a test-only reset seam, and it would freeze a
negative result for the process lifetime — directly against the lazy
re-resolution the same review recommends as the follow-up. The cost being avoided is
one extra read of a local daemon, on an opt-in path, bounded at 3s. Better resolved
together with lazy re-resolution than by caching the failure first.

Not from this PR

Backend Tests (3.10, 3) / (3.12, 3), Coverage Gate,
Dependency Audit / Audit Production Dependencies and Frontend Tests fail
identically on #1905, which is a docs-only PR on the same base — so they are not
caused by either change. The backend shard-3 failures are
src/kiro_crew/sandbox.py:2690 ValueError: not enough values to unpack (expected 3, got 2) in the wrap_argv tests. Flagging rather than chasing: fixing sandbox.py
does not belong in this PR.

Verification on 23c7d3d6

pytest -n0 on test_tailnet_origin.py + test_dashboard_origin.py +
test_config_loader.py + test_config_baseline.py + test_config_schema.py +
test_spawn_audit.py: 436 passed. isort 0, flake8 -j 1 0, mypy 0 (732
files), docs-lint 0, brand gate 0, scrub-lint --no-history 0. One commit; base
current at 429cbad8.

@github-actions github-actions Bot added readiness: checking Automated validation is still running readiness: action required A blocking check or review needs attention and removed readiness: action required A blocking check or review needs attention readiness: checking Automated validation is still running labels Aug 7, 2026
@CrysisDeu
CrysisDeu force-pushed the feat/tailnet-origin branch from 23c7d3d to 8cdfd0f Compare August 7, 2026 01:38
@CrysisDeu

Copy link
Copy Markdown
Collaborator Author

Disposition — 23c7d3d68cdfd0f3

GPT BLOCKING — tailnet.py:61 PATH-selected CLI inherits gateway credentials — accepted, fixed

Legitimate, and it lands squarely on reasoning I got wrong one revision earlier.
When I added dashboard/tailnet.py::_run_json to BENIGN_SPAWNS, the justification
I wrote said "the binary is resolved by shutil.which plus a fixed fallback tuple,
so the path never comes from input either." The first half of that is true of the
arguments and false of the binary: PATH is not input in the sense of a request
parameter, but it is attacker-selectable. ~/.local/bin is on PATH on a normal dev
box and is agent-writable — this session installed gh and node there — so an agent
that plants an executable named tailscale gets it executed by the next gateway
start with the feature enabled, inside the gateway process, with the gateway's
environment. That is a credential-boundary crossing, which is precisely what
sandboxed_spawn_argv exists to prevent, and my allowlist entry waved it through on
a claim that did not hold.

Two changes, matching the suggested fix:

1. Vetted absolute paths only — no PATH lookup.

_CLI_CANDIDATE_PATHS = (
    "/usr/bin/tailscale",                                    # Linux distro packages
    "/usr/local/bin/tailscale",                              # Linux tarball, macOS Homebrew (Intel)
    "/opt/homebrew/bin/tailscale",                           # macOS Homebrew (Apple silicon)
    "/Applications/Tailscale.app/Contents/MacOS/Tailscale",  # macOS app bundle
    r"C:\Program Files\Tailscale\tailscale.exe",             # Windows installer
)

shutil.which is gone. Every entry is a location the official packages install into
and that needs root to write. /usr/bin and the Windows path are new — dropping
the PATH lookup would otherwise have regressed the two most common installs, which
is the cross-OS breakage the charter calls out.

The trade is explicit: a non-standard install is no longer auto-derived and keeps
using explicit dashboard.url — the path it uses today. Narrower than before, which
is the right direction for a value that feeds the CSRF origin set.

2. Scrubbed environment. subprocess.run(..., env=scrub_env()) instead of
inheriting. Uses the repo's own sandbox.scrub_env() rather than a second, narrower
allowlist invented here — so this spawn cannot drift from the policy every other
spawn follows, and so it stays cross-OS safe (an aggressive allowlist would strip
HOME / SystemRoot, which the macOS and Windows CLIs need to find the daemon).

Tests (TestSpawnHardening, 3 new):

  • test_path_is_never_consulted — asserts shutil.which is not called and a
    planted path is not selected. This is the test whose absence let the original gap
    through.
  • test_candidates_are_absolute_and_vetted — every candidate is absolute.
  • test_credentials_are_not_inherited — injects AWS_SECRET_ACCESS_KEY into
    os.environ, asserts it is absent from the env= actually passed, and asserts the
    env is non-empty so a future over-scrub that breaks macOS/Windows fails here
    instead of in the field.

The BENIGN_SPAWNS justification is rewritten to state the pinning and the
scrub, and to name PATH as the gap that existed. A stale justification is how this
got waved through once; leaving the old text would set it up to happen again.

Verification on 8cdfd0f3

pytest -n0 on test_tailnet_origin.py + test_dashboard_origin.py +
test_config_loader.py + test_config_baseline.py + test_config_schema.py +
test_spawn_audit.py: 439 passed. isort 0, flake8 -j 1 0, mypy 0 (732
files), docs-lint 0, brand gate 0, scrub-lint --no-history 0. One commit; base
current at 429cbad8.

Still not from this PR

Backend Tests (3.10, 3) / (3.12, 3), Dependency Audit, Frontend Tests and
Coverage Gate fail identically on #1905, a docs-only PR on the same base. Backend
shard 3 is src/kiro_crew/sandbox.py:2690 ValueError: not enough values to unpack (expected 3, got 2) in the wrap_argv tests. Not chasing them here.

@github-actions github-actions Bot added readiness: checking Automated validation is still running readiness: action required A blocking check or review needs attention and removed readiness: action required A blocking check or review needs attention readiness: checking Automated validation is still running labels Aug 7, 2026
@CrysisDeu
CrysisDeu force-pushed the feat/tailnet-origin branch from 8cdfd0f to 09d312b Compare August 7, 2026 04:19
@github-actions github-actions Bot added readiness: checking Automated validation is still running and removed readiness: action required A blocking check or review needs attention labels Aug 7, 2026
@CrysisDeu

Copy link
Copy Markdown
Collaborator Author

Rebase onto 641586828cdfd0f309d312b9

No code change in this push. Every red on 8cdfd0f3 was either inherited from the
base or an incomplete review run, and main has since fixed all of them.

The reds, and where each one actually came from

Check Cause Fixed by
Frontend Tests catalogParity > ja > has no key missingmissing 11 key(s), e.g. components.kiroPrerequisiteGate.copied. This PR changes zero files under website/, so it cannot introduce a locale parity gap; #1905 (docs-only) failed the identical test. #1939 64158682
Coverage Gate Purely derived — the job's own log says Upstream coverage jobs did not succeed (coverage-combine=success, frontend-test=failure) -- failing closed. clears with Frontend
Backend Tests (3.10, 3) / (3.12, 3) src/kiro_crew/sandbox.py:2690 ValueError: not enough values to unpack (expected 3, got 2) in the wrap_argv tests — 2-tuple _last_unshare_failure fixtures broken by #1668 #1923 e9b8ffd0
Dependency Audit electron js-yaml override #1933 25aa5ab8
GPT 5.6 Review Not a finding. Per the lane's own guidance I read the job log before calling it infra, and the log ends with > **Incomplete review:** pass(es) 2 did not complete. — pass 1 returned, pass 2 did not, so no [GPT-REVIEWED] marker was emitted and the gate failed closed. No [BLOCK-MERGE] and no finding text anywhere in the 1.4 MB log. this rebase re-runs the lane

Opus 5 Review, Design Review, UX Review and Code Review were all green on
8cdfd0f3, so the round-2 hydration blocker and the round-3 PATH/credential blocker
are both closed.

Verification on 09d312b9

pytest -n0 on test_tailnet_origin.py + test_dashboard_origin.py +
test_config_loader.py + test_config_baseline.py + test_config_schema.py +
test_spawn_audit.py: 439 passed. isort 0, flake8 -j 1 0, mypy 0 (732
files), docs-lint 0, brand gate 0, scrub-lint --no-history 0. One commit, now
based on 64158682.

@github-actions github-actions Bot removed the readiness: checking Automated validation is still running label Aug 7, 2026
@github-actions github-actions Bot added the readiness: passed Eligible automated validation passed for the current revision label Aug 7, 2026
@iamwhatever
iamwhatever merged commit 97e3590 into main Aug 7, 2026
49 checks passed
@iamwhatever
iamwhatever deleted the feat/tailnet-origin branch August 7, 2026 05:06
@github-actions github-actions Bot removed the readiness: passed Eligible automated validation passed for the current revision label Aug 7, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants