Skip to content

πŸ”„ sync: top up v5 with latest v4 (436 commits) - #5017

Merged
clubanderson merged 440 commits into
v5from
sync/v4-into-v5-topup-2
Aug 28, 2026
Merged

πŸ”„ sync: top up v5 with latest v4 (436 commits)#5017
clubanderson merged 440 commits into
v5from
sync/v4-into-v5-topup-2

Conversation

@clubanderson

@clubanderson clubanderson commented Aug 28, 2026

Copy link
Copy Markdown
Member

Top-up merge of v4 (production) into v5 (the RFC implementation line: #4000 approvals desk, #4001 hooks, #4002 turn model, plus the reviewer role #5002).

A merge commit, not a squash β€” sync PRs preserve both histories, matching #4058 and the earlier v2β†’v4 top-ups. Merge commit 4369eb95 has two parents: 847774a9 (v5) and d3e2b5a5 (v4).

Incoming: 436 commits from v4, 923 files. v5 was 15 ahead.

Major incoming changes from v4

Area Change
pkg/delegation/ (new) Delegation chain / principal / minter, shipped observe-only β€” enforcement deliberately absent, with invariant tests asserting no enforcement path consults the chain and the minter exposes no decision API
pkg/hub (144 files) upgradeCollectible heartbeat gating (#4997) at three sites; per-server uncollectible-upgrade de-dup (#5000); pull-only upgrade reasons; stale-upgrade reconciliation
pkg/dashboard (125 files) Approvals/panel surface, contribute-pane cwd fixes, large hermetic-test wave
pkg/agent (68 files) Pause provenance + PauseByCause, SetPauseTransitionObserver, Copilot/Claude durable-token watchdog audit reasons, per-UID HOME isolation
CI / release Release-channel starvation fix β€” channel publication moved into src/scripts/publish-image-tags.sh with immutable short-SHA tags and generation-checked moving tags; release-line guard; podman rootless/rootful/arm64 lanes; quadlet gate
Hermetic tests Wide coverage/hermeticity work across pkg/config, pkg/hub, pkg/agent, pkg/proxy, pkg/scheduler, pkg/review, pkg/retro, pkg/tracing
Agent packs New telemetry and operations agents at L5/L6

Conflicts and resolutions

14 conflicted paths. The standing rule β€” v5 wins in the RFC packages, v4 wins everywhere else β€” resolved most of them, but three cases were adjudicated on evidence instead, and are called out below.

File Resolution Rationale
src/pkg/turn/envelope.go, journal.go, runner.go v5 See "pkg/turn" below β€” v4 grew an independent prototype, not a newer copy.
src/pkg/turn/runner_test.go, store.go Deleted (neither side) v4-prototype-only files that auto-merged with no conflict. See below β€” this is the semantic conflict.
src/pkg/config/config.go v5 Both hunks are purely v5-additive RFC #4000 surface (ToolApprovalConfig, ToolApprovalRule). v4's side differed only in gofmt alignment of the surrounding struct, which v5's side already carries.
src/pkg/config/packs/level-5.yaml, level-6.yaml Union of both See "agent packs" below.
src/pkg/config/acmm_packs_test.go Neither β€” 12/13 See below.
src/cmd/hive/hookwire.go v4 v4 implemented the agent_paused/upgrade_pause emitters that v5's code comment described as not-yet-existing, and moved the pause action to PauseByCause so the depth-1 loop guard receives structured causation. Verified agent.PauseByCause, agent.SetPauseTransitionObserver, and hub.SetUpgradePauseObserver all exist in the merged tree.
src/cmd/hive/hookwire_test.go v4 Carries TestAgentPausedEmitterCarriesHookCausationAndStopsPauseLoop, the positive control for the loop-safety invariant the emitter introduces. Dropping it would have left v4's new emitter untested.
src/cmd/hive/main.go Both Three disjoint hunks, no overlap: v5's approval-desk construction + Approvals: hook adapter, and v4's github.PrepareRequestDirs(logger) and installAgentPauseEmitter(agentMgr). All four intents verified present by grep after resolution.
src/pkg/dashboard/deps.go Both v5 adds ApprovalDesk/ApprovalInbox; v4 adds HookFire. Disjoint struct fields.
src/pkg/agent/audit.go Both v5 adds AuditToolApproval; v4 adds AuditCopilotTokenMissing/AuditClaudeTokenMissing. Disjoint constants.
.github/workflows/docker.yml v4 v5's side was the stale-head-check + inline release_channels emission. v4's f992bfa6 deliberately removed that mechanism to fix release-channel publication starvation, moving publication into publish-image-tags.sh. Keeping v5's side would have reintroduced the starvation bug. v5's safety intent is preserved structurally β€” see below.
src/docs/hooks.md v4 prose, v5-corrected status See below.

pkg/turn β€” the semantic conflict, and the one that mattered

v4 is not a newer copy of v5's turn model. v4 independently landed feat(turn): prototype durable re-entrant turns (#4933, 1052 lines added fresh), while v5 carries the full RFC #4002 implementation. Taking v4's side would have silently deleted the RFC line this branch exists to build β€” the diff in that direction removes 2206 lines including journaled_exec.go, operations.go, types.go, and replay_test.go. Resolved to v5.

The dangerous part was not in the conflict markers. v4's prototype runner_test.go and store.go do not exist on v5, so git added them with no conflict at all, and go build ./... passed β€” go build ignores _test.go. go vet caught it:

vet: pkg/turn/runner_test.go:31:6: effectID redeclared in this block

Both files belong to the discarded prototype (store.go defines turn.FileStore, referenced only by that test β€” the FileStore hits elsewhere in the tree are the unrelated knowledge.FileStore). Removed. pkg/turn now matches v5's file set exactly.

Agent packs β€” union, and a count neither side had

level-5.yaml and level-6.yaml conflicted because both sides appended different agents at the same list position: v5 added reviewer (#5002), v4 added telemetry and operations. This is an append collision, not a disagreement β€” either side alone silently drops the other's agents. Kept the union (v4's two first, then reviewer, preserving sort_order intent).

That makes acmm_packs_test.go the trap: v5 asserted 5: 10, 6: 11 (+1) and v4 asserted 5: 11, 6: 12 (+2). With all three agents present the correct value is 5: 12, 6: 13 β€” a value neither branch contained. Either side's number would have been a green-looking test asserting the wrong tree. Confirmed empirically: go test ./pkg/config/ -run 'ACMM|Pack' passes at 12/13.

pkg/hooks β€” no adjudication needed

Worth recording since it was flagged high-risk: v4's pkg/hooks is a byte-identical port from v5 (#4837 port state-triggered hooks from v5) β€” dispatcher.go hashes to b0dd545e on both branches. The "v5 wins" rule never had to fire there.

docs/hooks.md

The causation prose took v4's wording, which is now the accurate one (the emitter it describes exists after this merge). But v4's two "⚠️ NOT YET FUNCTIONAL / does not work yet" callouts for enqueue-approval are stale on v5: the backing queue landed here with #4057 as toolapprove.Inbox, wired via newHookApprovalAdapter β†’ WithApprovalQueue in cmd/hive/main.go. Carrying them in would have shipped documentation telling operators a working control does not work. Dropped the callouts and rewrote Status: to state that it is functional on v5 and gated by tool_approval.enabled (default off, sink nil, records an unwired-sink error).

docker.yml β€” checking that v5's intent survived

v5's block carried a deliberate comment that stable/candidate/edge must never be retagged by a v5 build. Taking v4's side removes that comment, so the intent was verified structurally instead: publish-image-tags.sh gates channel refs on [[ $branch == "$release_branch" ]], and docker.yml passes release_branch as the literal v4. A v5 build therefore cannot move a production channel. v5 still gets its own v5-latest tag β€” LONG_LIVED is "v2 v4 v5 mk dd".

Anchor verification (merged tree)

Standing security anchors β€” all present and non-empty:

Anchor Result
MintSSOToken 13 files
verifyHeartbeatBearer 16 files
derivePerHiveKey 24 files
SecretFilePathAllowed 6 files
mintHubUserCookieValueV2 13 files

Behavioral anchors:

Anchor Result
CONTRIBUTOR_MODE_MARKER constant, no env indirection bin/gh-wrapper.sh:32 β€” CONTRIBUTOR_MODE_MARKER="/etc/hive/contributor-mode", a literal
_extract_author last-value semantics Loop over all args uses continue, never an early break β€” last --author/-A wins
Trusted bot-identity file (#4049) bin/gh-wrapper.sh:49 β€” BOT_LOGIN_FILE="/var/run/hive-metrics/agent-tokens/gh-bot-login", a constant; HIVE_GH_WRAPPER_BOT_LOGIN_FILE honored only under the REAL_GH test harness
agent-env-scrub.sh + BASH_ENV wiring (#4048) Script present, unsets GITHUB_TOKEN/GH_TOKEN/…; exported at bin/agent-launch.sh:229; interactive-shell install at src/Dockerfile:422
cd-prefixed launches, 3 spawn sites (#4046) bin/contributor-relay.sh:1776, bin/contributor-agent.sh:582, plus dashboard site covered by src/pkg/dashboard/contribute_pane_cwd_test.go
Three upgradeCollectible gates (#4997) hub/saas.go:6073,6167,6255, hub/server.go:3834, hub/stale_upgrade.go:416
Per-server uncollectible field (#5000), no package global s.uncollectibleUpgradeNoted map guarded by s.uncollectibleUpgradeMu in hub/pullonly_upgrade.go β€” a HubServer field, not a global
src/pkg/delegation/ + observe-only invariant test Package present; observe_only_test.go carries TestObserveOnlyInvariant_NoEnforcementConsultsChain and TestObserveOnlyInvariant_MinterHasNoDecisionAPI

Build / test evidence

  • go build ./... β€” clean
  • go vet ./pkg/turn/... ./pkg/toolapprove/... ./pkg/hooks/... ./pkg/review/... ./cmd/hive/... ./pkg/config/... ./pkg/hub/... ./pkg/agent/... β€” clean (this is what caught the pkg/turn semantic conflict)
  • go test ./pkg/review/... β€” ok (the flagged high-risk package: v5's reviewer-role grounding and v4's coverage-floor tests coexist)
  • go test ./pkg/turn/... β€” ok
  • go test ./pkg/toolapprove/... β€” ok
  • go test ./pkg/hooks/... β€” ok
  • go test ./pkg/config/... β€” ok (17.7s)
  • go test ./cmd/hive/... β€” ok (11.9s)
  • go test ./pkg/hub/... β€” ok (121s)
  • go test ./pkg/delegation/... β€” ok
  • go test ./pkg/dashboard/ -run 'PinsPaneWorkingDirectory|Cwd' β€” ok
  • bin/gh-wrapper.test.sh, bin/test_gh_wrapper_gates.sh, bin/test_agent_env_scrub.sh β€” all pass

Note on the two pkg/config tests expected to fail pre-existing (TestExportEmitsShellExportOnBothPaths/warm_cache, TestSharedCacheIsCreatedPrivate): both PASS on the merged tree β€” v4's incoming hermetic-test wave fixed them. No failure was ignored on this branch.

gofmt: all resolved files are clean. pkg/turn/operations.go remains unformatted, which is pre-existing on origin/v5 and untouched by this merge.

Follow-up commit: e4b6f398 β€” release-line guard

CI surfaced one genuine merge-induced failure, release-lines-in-sync, fixed in e4b6f398 rather than waived.

v4's release-line carry-forward guard (#4405) arrived in this merge declaring release_lines: [v2, v4], and met v5's workflow triggers which already list v5. Neither parent fails on its own β€” v5 has no guard (.github/workflows/release-line-guard.yml and .github/release-lines.yml are v4-only), and on v4 no workflow names v5 β€” so the merge is precisely what surfaces it. That is the condition the guard exists to catch, so waiving it would have defeated its purpose.

Fixed per the manifest's own three-edit instruction:

  • release_lines: [v2, v4] β†’ [v2, v4, v5]
  • Added v5 to the pinned workflows missing it: go-security-analysis.yml, podman-contract.yml, podman-rootful-lane.yml, podman-rootless-lane.yml, podman-arm64-lane.yml, quadlet-gate.yml. (v2-ci.yml, v2-tests.yml, suid-contract.yml already had it.)
  • env_lists: docker.yml LONG_LIVED already carried v5, so no edit.

scorecard.yml is recorded as an exclusion ([main, -v2, -v5]), deliberately not widened: scorecard-action hard-fails on any non-default branch (refs/heads/v5 not supported with push event), which is why 90dea447 dropped v5 from its trigger in the first place. The manifest's design is that an exclusion must be written down and justified rather than left as a silent gap.

One more thing the guard's own self-test needed: test-release-lines-guard.sh built its "plausible future release line" fixture by adding v5 to the real manifest. Once v5 became a declared line that fixture was a no-op β€” the demonstration would have kept passing while asserting nothing, the exact green-forever failure mode the guard was written to prevent. Moved the fixture to v6 so the negative control still proves the guard catches a stale allow-list.

Verified locally: src/scripts/check-release-lines.sh β†’ PASS, src/scripts/test-release-lines-guard.sh β†’ PASS.

Also folded in: go test ./pkg/agent/ β€” ok (586s). The tmux tests pass; the socket-path errors noted as expected were avoided by using a short clone path (/tmp/hv/s2).

CI

release-lines-in-sync is green after e4b6f398, and the workflows that fix newly cover v5 (podman rootless/rootful/arm64 lanes, quadlet gate, gosec, govulncheck) all pass on this branch.

One check is red and is pre-existing on v4, not merge-induced: NOTICE matches the module graph fails identically on the six most recent origin/v4 commits, including d3e2b5a5 β€” this merge's own v4 parent. It is not a required status check on v5 (the only required context is gate, which passes). Verified before ignoring rather than assumed.

Merge verification: pkg/turn and pkg/toolapprove are byte-identical to origin/v5, and 196 sampled v4 production files across pkg/hub, pkg/proxy, pkg/scheduler, pkg/tokens, and pkg/rotation are byte-identical to origin/v4 β€” so no v4 fix was regressed by a stale v5 copy, and no RFC surface was lost. The reviewer role's files (grounding_test.go, withhold_test.go, policies/reviewer-advisory.md) and all three OnDemand governor gates survived intact.

Follow-up commit: 0ae23929 β€” CSP inline handlers (a second semantic conflict)

CI's test (rest 2/3) shard caught a second semantic conflict, of the same shape as the pkg/turn one and equally invisible to git.

TestEmbeddedStaticHasNoInlineEventHandlers (#4680) reported 7 inline on*= handlers in static/index.html. v4 tightened CSP to script-src-attr 'none' and added that guard; v5's RFC #4000 approvals panel was written with inline handlers. The panel's HTML is v5-only and the guard is v4-only, so the two sides never touched the same lines and git merged them without a murmur β€” but the merged result would have shipped an Approvals panel whose every control is dead under the new policy: buttons render, clicks do nothing, and nothing reports an error. Exactly the failure mode #4680 exists to prevent.

Converted all 7 to the data-action dispatch idiom rather than weakening the test or relaxing the CSP:

Site Conversion
Section header (3211) data-action="toggleSection" data-arg0="approvals-section" β€” verbatim the idiom used by the sibling beads-section / platform-section headers
Approve/Reject selected (3219-3220) `data-arg0="true
Filter chips (9355) data-arg-types="j", preserving the null sentinel that distinguishes "all" from a named rule
Row checkbox (9369) data-change-action="approvalsToggle" β€” the change listener's default: branch already routes to hiveDispatchAction
Per-row Approve/Reject (9377-9378) data-arg-types="s,j", id escaped through escapeHtml, matching the repo-pr-pill precedent

Neuter-test (the test is not green by construction): re-adding a single inline onclick puts the guard back to red, reporting exactly 1 handler; restoring the fix returns it to green. So the guard still detects what it claims to.

go test ./pkg/dashboard/ β€” ok (108s), full package.

The test job

test is an aggregator that rolls up the shards (list-packages, test-hub, test-agent, test-rest) and fails if any shard failed. Its log shows test-rest: failure as the only non-success input β€” so it was the same CSP failure surfacing, not a distinct problem, and it clears with 0ae23929.

NOTICE matches the module graph β€” pre-existing, and not a stale NOTICE

This one is not a dependency-graph change from the merge, and regenerating NOTICE cannot fix it. The job's own log shows the generator aborting before it writes anything:

F0828 main.go:77] one or more libraries have an incompatible/unknown license: map["FORBIDDEN":["github.com/fumiama/go-docx"]]

github.com/fumiama/go-docx is pinned at the same version on both parents (v0.0.0-20250506085032-0c30fd09304b in origin/v4:src/go.mod and origin/v5:src/go.mod), so the merge neither introduced it nor changed its version. The identical FORBIDDEN … go-docx abort appears on clean origin/v4 at d3e2b5a5 β€” this merge's own v4 parent β€” and on the six most recent v4 commits. It is a pre-existing licensing issue on the release line, out of scope for a sync PR, and it is not a required status check on v5 (the only required context is gate, which passes).

src/pkg/review/ β€” the flagged high-risk package

It did not conflict, and that was checked rather than assumed. The two sides touched disjoint files: v4 added artifact_roundtrip_test.go and quality_gap_test.go (its coverage-floor work), while v5's #5002 modified prompts.go and added grounding_test.go + withhold_test.go. No shared file, hence no conflict.

Because a clean merge is not evidence, the package was built and run on the merged tree:

  • go build ./pkg/review/ β€” OK
  • go vet ./pkg/review/ β€” OK
  • go test ./pkg/review/ β€” ok, 40 tests pass, with both lineages' tests executing: v5's TestPromptDirectsReviewerToReadRepo, TestPromptCarriesMergeBaseCommit, TestPromptGroundingDegradesWithoutMergeBase, TestFalsePositiveDisciplineIsInThePrompt, TestNonApproveVerdictsNeverAuthorizeMerge, TestAbsentVerdictDoesNotAuthorizeMerge, TestPartialPerspectiveCoverageIsNotApproval alongside v4's TestCollectAndWriteRoundTrip, TestWriteArtifactUnwritableDir, TestLoadArtifactErrors, TestSeverityRankOrdering, and the rest.

The reviewer role's wiring also survived intact: policies/reviewer-advisory.md kick template, the reviewer cadence entry, and all three OnDemand gates in pkg/governor/governor.go (which auto-merged) β€” go test ./pkg/governor/ ok.

Danathar and others added 30 commits August 22, 2026 22:14
…gin (#4596) (#4606)

Hive auto-restarts an agent whose pane shows a login prompt while a valid
credential exists in the shared config, on the theory that the agent has
simply not picked the token up yet. Three guards decide WHEN that restart
may fire (login streak, kick grace, cooldown). None of them decided HOW
MANY TIMES, so when something the restart cannot reach holds the agent at
the prompt, it retried at the 60s cooldown forever.

Restarts are not free β€” they destroy in-flight work, which is the exact
failure the kick grace was added for β€” so an unbounded retry of a repair
that demonstrably is not working is harmful on its own.

#4596 is precisely that shape. Claude's authentication spans TWO files
and hive has only ever probed one:

  * $HOME/.claude/.credentials.json β€” the OAuth token. claude.HasValidToken
    reads this, and it is what configHasTokens() gates the restart on.
  * $HOME/.claude.json β€” the session state: which account is signed in
    (oauthAccount) and whether onboarding completed.

A valid token beside a session file that has lost its oauthAccount makes
the CLI re-run onboarding and show "Select login method". Every
file-level check hive performs says "authenticated" while the operator is
looking at a login menu, so configHasTokens() stays true, the pane keeps
showing a prompt, and the restart loop never terminates β€” each relaunch
rewriting the same contended file.

This change:

  * Caps consecutive token-triggered restarts that fail to clear the
    prompt at 3, then stops and records a diagnosis. The counter resets
    the moment the prompt clears, so an agent that recovers is never
    permanently barred from a later legitimate nudge.
  * Adds diagnoseStuckLogin, which names BOTH files, says which one is
    the problem, and says restarting cannot fix it. Where both files look
    correct it says the cause is not on-disk state instead of guessing;
    non-claude backends get an honest generic message rather than a
    claude-shaped one.
  * Adds inspectClaudeSession, a read-only classifier for .claude.json
    (absent / unreadable / no-signed-in-identity / signed-in). An
    unparseable file reports unknown, never "identity missing" β€” this
    exists to replace guesses, not to add one.

Deliberately DIAGNOSTIC ONLY. Nothing here repairs .claude.json or
relaxes its permissions: #4596 measured that making the shared file
group-writable converts "one agent cannot log in" into "no agent stays
logged in", because every agent then rewrites it. Nothing changes
AgentHome either β€” whether interactive-auth backends should get per-agent
homes is a fleet-wide behavioural decision (it would relocate credentials
for every existing deployment and cost one login per agent) and is left
to the maintainers. This makes the failure legible and stops hive
amplifying it.

The give-up path deliberately does not `continue`: it disables only the
token restart, leaving the TLS-error and hung-CLI detectors below live.

The restart decision moved into AgentProcess.decideTokenRestart so the
rule is unit-testable β€” the inline version sat behind a real tmux pane
capture and could only be exercised in production. Accounting lives with
the decision so "fired" and "counted an attempt" cannot drift apart.

Tests verify the cap with a FIXED probe count rather than one derived
from tokenRestartMaxAttempts: an earlier draft derived its loop bound
from that constant and passed happily with the cap set to 1<<30. Both
halves are falsified β€” the suite fails with the constant raised, and
fails again with the give-up branch removed and the constant left sane.

Signed-off-by: Danathar <doug.baggett@gmail.com>
…ke-podman fixture (#4584)

pkg/sandbox sat at 67.6% (floor 67): Run was 20% covered (only the
missing-binary branch) and Available 0%. Add a fake-podman script
fixture so Run's success, non-zero-exit, invalid-spec, and
credential-env-stripping paths run deterministically on any host β€”
no real podman, no image pulls, no rootless uidmap prerequisites.
Also cover Available and the remaining PodmanArgs branches
(workspace-required, --name, --read-only, custom mount/workdir).

Package coverage: 67.6% -> 98.5%. The [sandbox]=67 floor override in
coverage-hourly.yml and v2-tests.yml can now be removed (ratchet
rule), but the App token lacks workflows permission, so a maintainer
should drop it in a follow-up.

Signed-off-by: hive-quality <hive-quality@users.noreply.github.com>
Co-authored-by: hive-quality <hive-quality@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CHANGELOG both-added conflict: kept both the #4593 ttyd --url-arg entry and
the #4596 login-loop entry.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
πŸ› fix(deploy): keep ttyd's --url-arg when it is credentialed (#4593)
* fix(contributor): auto-review Codex approvals

Signed-off-by: Danathar <doug.baggett@gmail.com>

* fix(contributor): add a reviewer-key escape hatch and guard whitespace workspaces

Review feedback on #4589, both points from the quality agent.

1. approvals_reviewer had no off switch.

   `-c approvals_reviewer=auto_review` is a Codex CONFIG KEY, and hive
   cannot prove every Codex release accepts it. The reviewer asked what
   happens if one rejects an unknown -c key at startup: every codex
   contributor task fails at launch.

   It was worse than "no preflight". The value was read with
   ${HIVE_CODEX_APPROVALS_REVIEWER:-auto_review}, and `:-` treats an
   explicitly EMPTY value as unset β€” so setting the variable to "" still
   produced auto_review. There was no way to drop the key at all; the
   only escape was HIVE_CODEX_DANGEROUSLY_BYPASS_APPROVALS_AND_SANDBOX=1,
   i.e. the unsandboxed posture this PR exists to remove.

   Switched to ${VAR-auto_review} (no colon) and skip the flag when the
   result is empty. An operator hitting an incompatible Codex can now
   drop just the reviewer key and keep --ask-for-approval/--sandbox.

2. --add-dir silently corrupted argv for a workspace path with spaces.

   backend_perm_flag returns a whitespace-separated string that
   agent-launch.sh word-splits (`read -r -a PERM_ARGS <<< "$PERM_FLAG"`),
   so the contract cannot carry a path containing whitespace:
   `--add-dir /work space` arrives as three argv words and grants Codex
   the wrong directory.

   The grant is now omitted with a warning on stderr rather than emitted
   corrupted. The sandbox posture still applies β€” only the flag that
   cannot be expressed correctly is dropped.

The default output is byte-identical to before, so the relay test's
pinned argv is unchanged (163/163 still pass).

Tests pin both behaviours: an empty reviewer drops the -c key while
keeping the sandbox and the workspace grant, and a whitespace workspace
omits --add-dir while keeping the posture. Verified they fail against the
pre-fix config.

Docs record the whitespace constraint and point at the empty-string
escape hatch as the supported response to a Codex version that rejects
the key β€” explicitly in preference to the dangerous bypass.

Note: bin/contributor-agent.test.sh still aborts later on this host at the
pre-existing non-hermetic "missing codex binary" check (a real codex on
PATH yields NOT_AUTHED, tracked in #4592). Unrelated and unchanged; the
full suite passes with codex off PATH.

Signed-off-by: Danathar <doug.baggett@gmail.com>

---------

Signed-off-by: Danathar <doug.baggett@gmail.com>
…hing live /data state (#4605)

TestNewGitHubProxy and TestNewGitHubProxyWithWritableDir wrote the CA to the
live /data/proxy-ca.pem default, and the latter deleted it afterwards β€”
running unit tests on a hive host would destroy the production proxy CA.
Redirect CACertPath/caKeyPath to t.TempDir() with the save/restore pattern
already used by sibling tests.

TestLoadPromptTemplate_FromPoliciesDir used agent name 'scanner', which the
live /data/policies copy shadows since loadPromptTemplate consults /data
paths before cfg.Policies.LocalDir. Use a name that cannot exist on a host.

TestCollector_Summary_Initially expected a nil summary, but NewCollector
unconditionally loads /data/token-summary.json, which exists on live hosts.
Construct the collector with a temp persistPath so the no-snapshot premise
actually holds.

Test-only changes; no production code touched.

Signed-off-by: hive-quality[bot] <hive-quality[bot]@users.noreply.github.com>
Co-authored-by: hive-quality[bot] <hive-quality[bot]@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
… ratchet (#4570) (#4578)

* 🌱 test: lift the three lowest packages over 90% and turn the coverage ratchet (#4570)

The gate that filed #4570 is green again β€” the `hub: TESTS FAILING` it
reported was a semantic conflict between #4562 and #4564 and was fixed by
#4571 β€” but the issue it filed is about the floor, and the floor had gone
slack in two different ways.

Real coverage, on the three packages sitting furthest below the 90%
default. All three are security-path code, and the untested statements
were the security-relevant ones:

  pkg/sandbox     67.6% -> 98.5%
  pkg/ioscan      71.0% -> 97.1%
  pkg/pushbroker  72.8% -> 100.0%

  - sandbox: PodmanLauncher.Run was 20% covered because every test that
    touched it needed a container runtime. Pointing Binary at a recorded
    stub makes the whole body β€” argv construction, env sanitization,
    stream capture, exit-code reporting β€” deterministic on every runner,
    including the assertion that a credential env var never reaches
    podman's argv. Also covers the spec-validation rejections and the
    allowlist-is-not-an-escape-hatch rule in SanitizedEnv.
  - ioscan: confusableASCII was 12% covered β€” 22 of the 25 homoglyphs an
    attacker can substitute for their ASCII twin were never exercised, so
    a gap in that table would have been silent. The whole table is pinned
    now, plus an end-to-end check that a Cyrillic-spoofed directive folds
    back and still blocks. Adds the canary registry's disk round trip and
    best-effort-save behavior, the classifier's semantic verdict/redaction
    marker, its retry exhaustion, and the OpenAI-compatible client's
    error paths.
  - pushbroker: covers every rejection stage of Broker.Run through a
    scripted git runner (unreadable HEAD, nothing committed, mint failure,
    empty token, rejected push), both base-ref fallbacks, and
    GitHubAppMinter β€” including that the minted token is scoped to the
    single repository and lands on the least-privileged default tier.

Floors, in both copies of PKG_THRESHOLD. The three above move with the
gain, per the map's own rule that an improving package has its floor
raised in the same PR. Three more had drifted well above floors recorded
at d89121f and were no longer catching anything: intent 65 -> 87 (holds
87.7), tracing 76 -> 85 (86.0), dashboard 78 -> 82 (83.1). scheduler
(90.4) and escalation (93.2) now clear the 90% default, so their entries
are gone. No floor was lowered, and nothing was raised to paper over a
drop.

Every value was re-measured on v4 at cd8aee5; each new floor keeps about
a point of headroom so ordinary churn does not turn the gate red.
Verified with -race and -count=3.

Signed-off-by: Danathar <doug.baggett@gmail.com>

* test(ioscan): read the whole request body in the OpenAI happy-path test

Review feedback on #4578: the test captured the outbound request with a
single r.Body.Read into a ContentLength-sized buffer. One Read is not
guaranteed to fill the buffer, so a request delivered across more than
one segment would leave trailing NUL bytes in gotBody and fail the
temperature/max_tokens Contains assertions intermittently β€” a flake that
would read as a real determinism regression in the classifier.

Use io.ReadAll and surface a read error instead of discarding it.

Coverage is unchanged at 97.1% (ratchet floor 96).

Signed-off-by: Danathar <doug.baggett@gmail.com>

---------

Signed-off-by: Danathar <doug.baggett@gmail.com>
…4609)

* [quality] fix(tests): hermetic pkg/config tests on live hive hosts

Five pkg/config tests fail when run on a live hive host because they
read the host's real state instead of test fixtures:

- TestValidate_MissingOrg: HIVE_REPO is set on hive hosts and
  applyBootstrapEnv backfills project.org from it, so the expected
  validation error never fires. Clear it with t.Setenv.
- TestValidate_NoAgents: Load() merges per-agent overlays from the
  default /data/agent-configs, which is populated on a live host, so
  'no agents' validates fine. Point data.agents_dir at an empty temp dir.
- TestSaveSkipsDashboardOverlayOutsideK8s: IsKubernetesPod() probes the
  hardcoded serviceaccount token path, which exists on hive hosts, so
  the overlay is written even with KUBERNETES_SERVICE_HOST unset. Turn
  the probe path into a test seam (saTokenFile var) and redirect it,
  plus RuntimeConfigFile, to a temp dir.
- TestPausePersist_ConcurrentSoak / _TwoUnsynchronizedSavers: reloads
  merge the live /data/agent-configs overlays (which carry paused: false)
  over the persisted paused flags, dropping every pause. Use an empty
  temp agents_dir and redirect RuntimeConfigFile/DashboardOverlayFile.

Same hermeticity pattern as #4595 (scheduler/tokens/proxy); this covers
the config package. No production behavior change: saTokenFile is a var
only as a test seam, identical to the existing CACertPath precedent.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: quality <quality@hive.kubestellar.io>

* test: make gh app token script tests portable

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Andy Anderson <andy@clubanderson.com>

---------

Signed-off-by: quality <quality@hive.kubestellar.io>
Signed-off-by: Andy Anderson <andy@clubanderson.com>
Co-authored-by: quality <quality@hive.kubestellar.io>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Andy Anderson <andy@clubanderson.com>
…coverage gate (#4612)

Adds hermetic unit tests for the lowest-covered logic paths not already
addressed by in-flight PRs #4578 (sandbox/ioscan/pushbroker) and #4595
(scheduler/tokens/proxy hermeticity):

- pkg/review 80.8% -> 90.2%: dispatch state persistence round trip and
  error paths (LoadDispatchState/WriteDispatchState were 0%), upsertHuman,
  removePendingForHead, reviewCapableAgents filtering/allow-list,
  ValidateReport rejection matrix, Collect error and skip paths.
- pkg/retro 82.4% -> 90.5%: Lane.Due, metaBool, severityToPriority,
  roundDuration, applyPRMetadata branches, prRefFromAttrs, splitPRRef/
  repoPart/canonicalIssueRef edge cases, completedAt sources,
  openDuplicate open-vs-closed advisory matching.
- pkg/tracing 86.0% -> 94.8%: formatReachTime, TimelineSpanName fallback,
  AgentKickAttributes (was 0%), SaveReachState write/rename error paths,
  reach overflow-log saturation branch.

All tests are hermetic (t.TempDir, in-process stores, no network, no
live-hive state) so they stay green on live hive hosts.

Signed-off-by: quality <quality@hive.kubestellar.io>
Co-authored-by: quality <quality@hive.kubestellar.io>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The controller accepts agy as a backend and rotation has an agy/Google
fallback rung, but the image did not ship /usr/local/bin/agy. Agy was
therefore always rejected as agy-binary-missing and DeepSeek exhaustion
could strand an otherwise rotatable agent.

Install the vendor's pinned public Linux artifact (v1.1.19) with the
published SHA-512 for amd64 and arm64. The binary is self-contained.
Document that operators must seed a pre-authenticated /home/james/.gemini
session into persistent storage; a pod cannot initiate Google OAuth.

Signed-off-by: hanthor <jreilly1821@gmail.com>
…4608)

High-volume agents normally avoid subscription providers, but that policy
stranded them when their current prepaid/metered provider was positively
measured exhausted: every fallback was excluded, so the only terminal
action was pause.

Keep the guard for normal operation and subscription-source failures. Waive
it only for a successful exhaustion probe on the CURRENT metered provider,
allowing the agent to rotate to a measured-healthy Codex, Claude, or Agy
fallback rather than lose service. Probe errors remain fail-open.

Add a regression test for DeepSeek/litellm -> Codex at 300s cadence.

Signed-off-by: hanthor <jreilly1821@gmail.com>
src/docs/README.md is the index readers navigate from, so a page missing
from it is one nobody finds. A sweep of every top-level src/docs/*.md
against the index found eight unlinked pages -- two more than #4617
reported (roadmap.md and landscape.md were missed).

Each entry goes in the section a reader would actually look in rather
than a dumping ground: the five feature/config pages under
"Configuration and agents", the heartbeat bearer cutover under
"Security (v4)" since it retires a hub-master-derived credential, and
roadmap/landscape under "Architecture and design" as project-direction
documents rather than operator how-tos.

Entries use relative links, which is what recent additions to the index
actually chose: five of the six most recent index commits that ADDED an
entry used a relative link, and the one absolute case edited an existing
line in place rather than picking the format. Both formats appear for
same-directory files historically, so the rule is not "in-directory
files are relative" -- it is simply the live convention for new entries.

roadmap.md and landscape.md both self-date in their own headers
("Directional, not a promise"; "This document will rot"), so their index
entries carry a currency hint pointing the reader at that date rather
than presenting them as settled fact.

Also adds an advisory docs-index reminder workflow modelled on
changelog-reminder.yml (#4429, #4440): it comments once when a PR adds a
top-level src/docs page the index does not link, and never blocks. It
reports only pages that PR adds, never the pre-existing backlog, and
excludes subdirectories such as adr/ that carry their own README index.

Signed-off-by: Andy Anderson <andy@clubanderson.com>
Back-fill ADR statuses and roadmap links for the implemented knowledge, skill registry, CEL trigger, and hub/spoke subsystems.

Closes #4600

Closes #4602

Signed-off-by: Andy Anderson <andy@clubanderson.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…authenticates the fleet (#4619)

The shared HOME=/data/home meant Claude Code's wholesale atomic rewrite
of $HOME/.claude.json (tmp file + rename, needing only DIRECTORY write
permission on the 2775 dev:node shared home) let ANY agent replace the
session file β€” an unauthenticated agent's rewrite stripped oauthAccount
from under the authenticated one and the whole fleet fell back to the
login menu. File permissions can never close a directory-entry race,
which is why #4606 shipped diagnosis + a bounded restart, not a fix.

Now each per-UID interactive agent runs with HOME=/data/home/agents/<name>:

- AgentHome() routes per-UID non-inference agents to the per-agent home
  (single source of truth; buildAgentEnv, the auth probe, caveman install
  and #4606's stuck-login diagnosis all follow automatically).
- setupInteractiveHome() provisions it at launch (mkdirAllNoFollow,
  tighten-to-0700-chown-uid with the tightenInferenceHome unprivileged
  fallback) and bridges shared state back to /data/home by Lstat-guarded
  symlinks: .claude (holds .credentials.json β€” the shared OAuth token, so
  one login still authenticates the fleet), .copilot, .config, .codex,
  .bob, .gemini, .cache, .local, plus the entrypoint-written .gitconfig/
  .bashrc/.profile. Real files/dirs at a bridge name are never clobbered.
- The contended ~/.claude.json becomes truly per-agent, ADOPTED at launch
  from a signed-in source (legacy shared file first, then any signed-in
  sibling) via #4606's inspectClaudeSession classifier β€” never fabricated
  (no signed-in source anywhere -> the login menu is the honest state),
  and a signed-in per-agent file is never overwritten. This also makes
  #4606's capped token-triggered restart curative.
- Orphaned /data/home/.claude.json.tmp.* debris (ten accumulated in one
  afternoon on the reporting hive) is swept, bounded and best-effort.
- .bash_history contention disappears as a side effect (not bridged);
  .npm deliberately un-bridged (per-agent caches end the cross-UID
  EACCES collisions the shared cache suffered).
- entrypoint.sh pre-creates /data/home/agents (0755 dev:node).
- HIVE_SHARED_AGENT_HOME=1 escape hatch restores the legacy layout.

Migration: none. Credentials stay put; the first launch adopts the
legacy session; single-agent hives behave as before.

Fixes #4596

Signed-off-by: Andrew Anderson <andy@clubanderson.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Refs #4592, #4610

Signed-off-by: Andy Anderson <andy@clubanderson.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add hermetic tests for tmux session probes, pane capture/readiness, blocking prompt detection, trust prompt dismissal, and Copilot session refresh looping.

Signed-off-by: Andy Anderson <andy@clubanderson.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Redirect pkg/agent tests away from live metrics, ACMM fallback, token, and tmux state so they stay deterministic on hosts with active hive data.

Signed-off-by: Andy Anderson <andy@clubanderson.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…eshooting (#4626)

* πŸ“– docs: retire stale v2 disclaimers in architecture and legacy troubleshooting

src/docs/architecture.md opened with a blockquote making two claims, both
stale. It said the page "describes the `v2` branch" β€” v2 was retired in
August 2026 and this file documents the current v4 line under src/. It also
said automatic goal decomposition, plan review and stall-triggered re-planning
"are not implemented", which is no longer true: all three ship today as the
planning-intelligence lane.

Rather than swap v2 β†’ v4 and leave the capability claim standing, the
blockquote now states what is actually true β€” the three capabilities exist but
are gated at ACMM L5+ (PlanningMinACMMLevel, src/pkg/planning/issue.go), since
the architect that decomposes epics has no cadence below L5 β€” and points at
planning-intelligence.md and ADR-0006, which architecture.md itself does not
cover.

docs/troubleshooting.md (root, legacy v1/systemd) labelled its forward link
"v2 troubleshooting" while the sentence containing it already said "branch
`v4`; code under `src/`". Only the visible label was stale; the target path
was correct. The legacy banner itself is correct and is left intact.

Fixes #4624
Fixes #4625

Signed-off-by: Andy Anderson <andy@clubanderson.com>

* πŸ“– docs: hedge the decomposition claim to match the wiring

Follow-up verification of the planning package showed the first draft of this
note overstated automatic goal decomposition. The request side is automatic and
L5-gated, but the fulfillment side is not wired into the daemon: the only
non-test caller of planning.Decompose / DecomposeFromOutput is the bd decompose
CLI (src/cmd/bd/decompose.go:62), agentparse.ParseTaskList has exactly one
consumer (src/pkg/planning/decompose.go:179), ClearDecomposePending is reachable
only through that path, and the architect's standing policy never mentions
decomposition. So a stock L5 hive kicks the architect but nothing ingests its
output back into child beads; the epic stays decompose_pending.

Correcting a note that understated the feature must not overstate it in the
other direction, so the blockquote now names that specific step. Plan review and
the stall-replan lane are separated out, since both genuinely do run
automatically.

Signed-off-by: Andy Anderson <andy@clubanderson.com>

---------

Signed-off-by: Andy Anderson <andy@clubanderson.com>
…al (#4632)

Nine "v2 HEAD" references across six docs named a branch that is no
longer the line of development. Two of them were hiding a second,
independent error behind the stale branch name.

ioscan.md claimed "`ioscan` is not present in v2 HEAD. There is no
`ioscan` package, no `ioscan:` config block ... and no canary/fail-mode
implementation wired into the kick path." Every clause is false on v4:
src/pkg/ioscan is ~1400 non-test lines, src/hive.yaml.example carries an
ioscan: block, and canaries plus fail_mode are wired from
src/cmd/hive/main.go. The page is rewritten to describe the shipped
feature rather than deny it.

api-reference.md described itself as "generated from route
registrations". No generator exists anywhere in the tree, so the word
"generated" was a second error β€” the same one found in env-vars.md. The
page now says it is compiled by hand and must be updated alongside route
changes.

The knowledge-curator.md claims were verified and still hold:
RunExtraction has no non-test callers and nothing wires NewCurator, so
only the branch name is retired there. Likewise the federation and
inference-backends statements are accurate for the current tree.

Signed-off-by: Andy Anderson <andy@clubanderson.com>
* fix(github): slow-start pacing after secondary rate limits

GitHub's secondary limit throttles burst/concurrency. When it trips,
go-github blocks correctly until the reset β€” but at the reset boundary
every queued caller (pr-request watcher retries, enumeration, automerge
sweep, stats) fires SIMULTANEOUSLY, GitHub sees the burst, and the limit
re-trips for another hour. Observed live on kubestellar/console
(2026-08-23): three consecutive hourly re-trips (07:49 -> 08:49 -> 09:49)
with the spoke's entire GitHub output dark throughout.

slowStartTransport breaks the loop at the one place all callers converge β€”
the shared proxy-trusting transport. A REAL secondary 403 (Retry-After
header; permission 403s carry none) opens a caution window extending 10
minutes past the reset, during which requests are globally serialized with
a ~2s jittered gap. The post-reset wave becomes a trickle the secondary
limiter tolerates; full concurrency resumes when the window closes.

Tests: paced spacing after a Retry-After 403; plain 403s and normal
traffic unpaced.

Signed-off-by: Andrew Anderson <andy@clubanderson.com>

* fix(github): share slow-start STATE, wrap the CURRENT transport per client

The sync.Once wrapper pinned the process to the FIRST inner transport it
saw: after the proxy-trust layer rebuilt its transport (new CA), every
later client silently rode the stale one with dead TLS roots β€” caught by
the proxytrust pool/leak regressions. Pacing state stays global (the herd
is cross-caller); the wrapper is now cheap and constructed per client
around whatever inner is current. Pool test updated to assert the
proxy-trust guarantee through the wrapper's inner.

Signed-off-by: Andrew Anderson <andy@clubanderson.com>

* test(github): SharesTransport asserts the shared INNER through the wrapper

Per-client slow-start wrappers are intentional; the #3875 socket-pool
guarantee lives on the shared inner transport, and the pacing ledger must
also be one shared instance β€” assert both explicitly.

Signed-off-by: Andrew Anderson <andy@clubanderson.com>

---------

Signed-off-by: Andrew Anderson <andy@clubanderson.com>
The v2 branch is retired and mainline is v4, but eight files still used
"v2" to name the current running software, a path that no longer exists,
or a directory to grep. Each was verified against the code before being
touched, so capability claims keep their substance and only the branch
name is retired.

- troubleshooting.md: dashboard-only mode and the tmux agent manager are
  both current v4 behavior (src/cmd/hive/main.go, src/pkg/agent/manager.go).
- env-vars.md: three operator audit commands grepped a `v2` directory that
  no longer exists, so they returned empty and looked successful. Now `src`.
- agent-configuration.md: `v2/policies/` is `src/policies/`.
- knowledge-system.md: section already cited `src/pkg/` while calling the
  architecture "v2".
- notifications.md, beads-cli.md, tls-setup.md: name-only retirement. Native
  TLS is still genuinely absent (no ListenAndServeTLS, no tls yaml keys).
- deployment-scripts.md: the doc was accurate and the script is the defect β€”
  src/deploy/bootstrap-lxc.sh still hardcodes `git clone --branch v2`. Kept
  the description honest and recorded the defect for operators.

Signed-off-by: Andy Anderson <andy@clubanderson.com>
… gates (#4654)

`docs/advisory-staleness.md` was linked from nowhere: the only `git grep` hits
for it are Go comments discussing the concept, not links to the page. It was
the sole orphan in root `docs/`, so operators could not discover it.

It is current v4 documentation sitting in the legacy tree. It cites
`src/pkg/hub/advisory_staleness.go` and documents the hub's live stale-advisory
pill and warning alert, and unlike its neighbours it carries no "Legacy
v1/systemd documentation" banner. Move it to `src/docs/`, which is the
documented source of truth and which the advisory `docs-index-reminder`
workflow (#4618) covers β€” root `docs/` is not covered, so leaving the page
there means the next orphan goes unnoticed the same way. `git mv` keeps the
history. Nothing referenced the old path: no sync manifest, no workflow, no
script.

Root `docs/` pages are reachable by direct URL from search engines and old
bookmarks, so a bare delete would 404 anyone holding a link. Leave the house
retirement stub β€” a short banner plus pointer, matching `docs/architecture.md`.

The page had also drifted while nobody could find it. It was last touched at
#4167; #4528 then added a whole suppression arm the page never learned:

- `allAgentsQuietByDesign` β€” with no error reported, an ageing digest is
  suppressed when every agent is paused or off-schedule. Documented as a new
  gate, including its deliberate conservatism (no agents, or a legacy agent
  entry, does not qualify) and that a reported post error still flags.
- `suppressed-agents-quiet` was missing from the diagnostics classification
  list, and the per-hive `hidden_stale` field was undocumented.

Cross-link with `src/docs/advisory.md` rather than merging: that page covers
what the digest shows and how findings are retired, and its "digest stopped
updating" section is the operator-facing counterpart to these gates. The two
"staleness" concepts are different (finding auto-close vs digest-post age), so
they stay separate pages that point at each other.

Signed-off-by: Andy Anderson <andy@clubanderson.com>
src/docs/design/ held four documents and no README. Only two of them
(knowledge-system.md, master-key-rotation.md) were linked from anywhere a
reader navigates; pr-reach-telemetry.md and master-delivery-wrapped.md were
orphans. Their only inbound "references" are string literals in a
component-mapping test table (src/pkg/reach/mapping_test.go,
src/pkg/hub/reach_api_test.go) and a Go comment
(src/pkg/hub/wrapkey_test.go) β€” none of which is navigation.

The structural reason both went unnoticed: the docs-index-reminder workflow
(#4618) is scoped to ^src/docs/[^/]+\.md$, top level only, so that
subdirectories carrying their own README index β€” the adr/ pattern β€” are not
false-positived. design/ had no such README, so it got neither the
delegation pattern's coverage nor the guard's, and fixing the one file named
in the issue would have left the next one to recur.

So this adds src/docs/design/README.md following the shape of
src/docs/adr/README.md, and links it from src/docs/README.md exactly as adr/
is linked, completing the delegation chain.

Each entry states its status, checked against the code rather than the page's
own header, so an accepted plan is not read as current behaviour:

- master-key-rotation.md β€” generations foundation and the session-cookie
  domain landed; remaining per-domain adoptions pending.
- master-delivery-wrapped.md β€” header still reads DESIGN ONLY, but the
  sealing primitive and spoke wrap-key lifecycle landed as wrapkey.go /
  wrapkey_store.go. Flagged both ways.
- pr-reach-telemetry.md β€” phases 1 and 2a shipped, 2b/2c pending. Its
  v2/pkg/... examples are recorded as DELIBERATELY historical: pre-rename
  merges report those paths from the forge API forever and the mapper still
  handles both eras. Not "corrected" to src/....
- knowledge-system.md β€” wiki layers and programmatic extraction are live;
  the planned scheduler.RunCurator(schedule) wiring is not implemented, so
  knowledge.curator.schedule is defaulted and validated but read by nothing.

The workflow is left alone deliberately; see the PR body.

Signed-off-by: Andy Anderson <andy@clubanderson.com>
…bient HIVE_HUB_URL (#4658)

hubDomainSuffix() prefers HIVE_HUB_PUBLIC_URL / HIVE_PUBLIC_URL /
HIVE_HUB_BASE_URL / HIVE_DASHBOARD_URL / HIVE_HUB_URL over the cluster
domain, so on live hive hosts (which export HIVE_HUB_URL) every test hive
in url_reachability_test.go stopped counting as hub-fronted and six
alert tests failed with 'got []'. Clear the five overrides via t.Setenv
in newURLHealthTestHub so the tests exercise only the domain they set.

Same env-bleed class as the scheduler/tokens/proxy fixes in #4595.

Fixes #4657

Signed-off-by: sec-check <sec-check@hive.kubestellar.io>
Co-authored-by: sec-check <sec-check@hive.kubestellar.io>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…4639)

MergeAllowed, EvaluateForAppSelfMerge, BuildEvidence, LinkedIssueNumbers,
and LinkedIssueInBeads were at 0% coverage despite gating what enters
merge-eligible.json. Pin the authorization x alignment matrix, the
App-self-merge tier-gate preservation (callAllowed=false must be exactly
Evaluate; Tier1 stays issue-gated), and evidence extraction against a
real beads.Store. pkg/intent: 88.x% -> 94.4%.

Signed-off-by: hive-quality <quality@hive.local>
Co-authored-by: hive-quality <quality@hive.local>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
TestSlowStart_PacesAfterSecondaryLimit asserted >=gap spacing between
server-observed request arrival times, but the transport only guarantees
each request starts no earlier than its claimed slot: scheduler and
connection latency can push an early request toward the next one's slot,
compressing observed inter-arrival gaps below gap even when pacing worked.
Observed 4/4 failures on a loaded hive host (gaps 78us-44ms vs 60ms).

Assert the burst's total elapsed lower bound instead: 4 cautious requests
claim slots >=gap apart, so the burst cannot finish in under 3*gap. Load
can only increase elapsed time, while an unpaced stampede still completes
near-instantly and fails. Also widen the test caution window to a minute
so delayed goroutines cannot escape it (pacing cost stays 3 gaps).

Fixes #4653

Signed-off-by: sec-check <sec-check@hive.kubestellar.io>
Co-authored-by: sec-check <sec-check@hive.kubestellar.io>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
… hosts (#4634)

TestAgentEnvPairs_BaseEntryCount and TestAgentEnvPairs_BDDirFromBeadsDir
assert exact env-pair counts, but agentEnvPairs conditionally forwards
HIVE_ID, HIVE_SHA, and HIVE_ADVISORY_ISSUE from the ambient process env.
On a live hive host those are exported into agent sessions, so both
tests fail on pristine v4 (15/16 pairs vs 13/14 expected).

Blank the three vars via t.Setenv in the count-sensitive tests through
a clearAmbientHiveEnv helper; t.Setenv restores originals on cleanup.

Same hermeticity class as #4585, in pkg/agent.

Signed-off-by: hive-quality <quality@hive.local>
Co-authored-by: hive-quality <quality@hive.local>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
… hosts (#4627)

TestAgentAuthState_TruePositivePreserved and
TestAgentAuthState_CopilotCredentials fail on any live hive host with a
logged-in copilot fleet, because two probe paths could not be redirected:

- AgentAuthState stat'ed the const CopilotUserTokenPath directly; the
  dashboard device-flow token at /data/copilot-user-token exists on live
  hosts and short-circuits the probe to "authenticated". Introduce
  copilotUserTokenProbePath, a var seeded from the const, purely as a
  test seam (the sharedCopilotConfigPath convention), and redirect it in
  emptySharedPaths.

- uid>0 probes resolve HOME to <sharedAgentHome>/agents/<name>, and the
  test probes agent "scanner" β€” a real deployed agent whose per-agent
  home holds real copilot credentials since #4619. Redirect
  sharedAgentHome via the existing withSharedAgentHome seam.

Same class as #4605/#4622 (live-host test hermeticity, #4585).
No production behavior change: the var is initialized to the same const.

Signed-off-by: hive-quality <quality@hive.local>
Co-authored-by: hive-quality <quality@hive.local>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…24s β†’ 0.01s (#4628) (#4659)

TestConcurrentPauseResume_NoPanic pins the pause/resume mutex accounting
under concurrency, but its agents were plain (non-sandbox) claude configs,
so every Resume of a StatePaused agent took the full relaunch path:
ensureTmuxSession + launchInTmux β€” a real tmux session create plus a real
CLI launch per cycle, ~250 launches per run, none cleaned up. On any host
where HIVE_WORK_DIR is writable (every live hive host) that one test took
220-420s and pushed `go test ./pkg/agent` past go test's 600s default
timeout, aborting coverage profile writes along the way.

Route the test through the sandbox pause/resume branch instead
(sandbox-enabled agents + SetSandboxConfig): every cycle is now a pure
in-memory state transition, which is exactly the accounting the test
exists to hammer. Measured 223.57s β†’ 0.01s with -race on the same box.
Concurrent stress on the real (non-sandbox) relaunch path is not lost β€”
relaunch_race_guard_test.go covers that dance with proper stubs and
cleanup.

Also fix the tmux server mismatch in launch_coverage_test.go: the three
adopt-running-CLI tests pre-created their marker sessions with bare
`exec.Command("tmux", ...)` β€” the default-socket server β€” while the
manager inspects the package test server (-L defaultTmuxSocket). The
pre-created session was invisible to the manager, so each test silently
did a full launch instead of exercising the adopt branch it documents.
Coverage of manager.go's CLI-already-running block from these tests was 0
before this change and is full after it (verified with -coverprofile both
ways).

Audit of the rest of the package (issue bullet 2): every other tmux exec
in tests goes through testTmuxCommand or the manager's socket-aware
helpers, TestMain already isolates both the socket name (per-pid -L) and
TMUX_TMPDIR, and su-exec paths are stubbed β€” no other bare-socket execs
remain.

Fixes #4628


Claude-Session: https://claude.ai/code/session_01KSiYdFgsi6Lt5zwmn7MrQm

Signed-off-by: Douglas Baggett <doug.baggett@gmail.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…aude.json (#4636)

The caveman installer ran as the hive user with HOME pointed at the
agent's per-agent home. Caveman spawns the backend's own CLI (claude
plugin list / plugin install), and Claude Code rewrites $HOME/.claude.json
wholesale on every invocation β€” run as a uid that cannot read the
agent-owned 0600 session file, the CLI treats the home as a fresh install
and replaces the signed-in session with a blank skeleton. The agent drops
to the login menu on its next start, and every re-login is destroyed
again at the next launch. This is the #4596 wholesale-rewrite clobber
reintroduced from inside the manager, through the per-agent layout #4619
built to end it.

Observed on a live rootless hive: the stuck agent's home accumulated
.claude.json.tmp.<pid>.<hash> debris owned by the hive uid, each clobber
timestamped exactly at an "installing caveman" log line, and three
operator re-seeds of the session file were each destroyed within seconds
of the next agent launch.

Fix: wrap the install in su-exec as the agent user whenever the agent has
an allocated UID β€” matching every other command that touches the
per-agent HOME (tmuxCmd, setupCodexHome). The npm cache moves with it: a
cache written back when the install ran as the hive user is owned by the
wrong uid and npm EACCESes on its shards (#2284's failure class), so a
foreign-owned cache is removed, with a per-UID fallback path when removal
fails.

Also correct the stuck-login diagnosis for the unreadable-session case.
Under per-agent homes, "unreadable" means unreadable BY THE HIVE PROCESS
β€” the normal state of a healthy signed-in agent whose CLI owns the file
at 0600. The old wording claimed the agent's CLI could not load the
identity, which sent the investigation above down a permissions rabbit
hole on a file the agent could read fine.

Signed-off-by: Douglas Baggett <doug.baggett@gmail.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Danathar and others added 19 commits August 28, 2026 12:58
…ath (#4993)

Found by the backend-list parity guard in #4987 on its first run (#4988).

`amazonq` was offered to operators in the dashboard's backend/method picker
and named in the CLI Pin Value tooltip, while being absent from BOTH
authoritative registries β€” config/backends.conf's KNOWN_BACKENDS and
config.CLIBackends. validateBackendName (src/pkg/agent/manager.go) therefore
rejected it, so an operator who picked the backend the UI recommended got a
launch failure. That is precisely the accept-then-fail class that function
exists to prevent: its own comment says it dispatches on the same canonical
lists as ValidateBackend and backendBinary "so a backend accepted by any
write path is one the launch path can start". The picker is a write path
that was never brought into that agreement.

REMOVE, NOT FINISH β€” and this is not a product judgement. git history shows
amazonq once HAD a full dispatch path (backend_binary "q",
backend_perm_flag "--trust-all-tools") and was deliberately dropped from
backends.conf in #1045, whose commit message states the resulting supported
set: "6 CLIs: Claude, Copilot, Goose, Codex, Agy, Bob". gemini went in the
same commit. So this completes a removal that was already decided; it does
not decide against Amazon Q support.

Removed from seven places across both dashboard trees and the shell/JS
launch paths:

  - src/pkg/dashboard/static/index.html β€” the JS KNOWN_BACKENDS array that
    backendOptionList() renders the dropdowns from, and the CLI Pin Value
    tooltip that names the options in prose.
  - dashboard/index.html, dashboard/server.js β€” the standalone dashboard
    tree carries the same two lists, both claiming to mirror backends.conf.
  - bin/agent-launch.sh, bin/contributor-agent.sh, bin/contributor-relay.sh
    β€” three dead "no --model flag" lists. Unreachable already, since the
    backend can never be selected, but they are what made the name look
    half-wired rather than removed.
  - bin/contributor-relay.test.js β€” the test that pinned the dead entry.

GUARD FOR THE THIRD LIST. #4988 notes the dashboard's JS array is a third,
independent list with no guard at all, and calls a follow-up "worth it if
this class of drift matters". It plainly does β€” that gap is the whole issue
β€” so the guard lands here rather than being deferred:

  - TestBackendPickerOffersOnlyDispatchableBackends parses the JS array out
    of the embedded page and asserts every name is in CLIBackends,
    InferenceBackends, or a documented exception.
  - TestBackendPickerInferenceListMatchesGo pins the page's second array,
    since a name drifting between the two mislabels a picker entry as a CLI
    rather than a gateway even when both lists look individually plausible.
  - TestCLIPinTooltipNamesNoUndispatchableBackend covers the prose tooltip,
    which the array guard structurally cannot reach β€” and which is the other
    place #4988 found amazonq.

DIRECTION IS DELIBERATE: picker βŠ† registries, not the reverse. A name the
picker offers but nothing can launch is a broken promise to the operator.
A supported backend the picker omits (`pi`, `agy`) is only
under-advertisement, and whether agy belongs in the HUB's picker turns on
whether it can authenticate in a pod β€” its sign-in is an interactive Google
OAuth flow with no API-key mode β€” which is a question this guard must not
answer by implication. Left for maintainers.

`openrouter` stays, as a declared exception with its reason recorded: it is
a Model Gateway preset that agent routing resolves as a configured gateway
name, and the page deliberately offers it before a matching gateway exists
so an operator can pick it and then fund it. The exception list carries the
same staleness check as cliBackendExceptions β€” an exception that becomes a
real registered backend, or leaves the picker, fails the test rather than
rotting.

Mutation-checked: restoring amazonq to the array, restoring it to the
tooltip only, and dropping watsonx from the JS inference list each fail the
right test with a message naming the specific backend and list.

Verified the shell/JS edits are inert: bash -n on both scripts, and the
relay suite is 162/163 before and after (the one failure, "a relaunch cds
somewhere resolvable before starting the CLI", is pre-existing on a clean
v4 and unrelated).

Closes #4988

Signed-off-by: Danathar <doug.baggett@gmail.com>
* πŸ”’ feat(identity): observe-only delegation chain

Signed-off-by: Andy Anderson <andy@clubanderson.com>

* test(identity): cover minter, publish, and validation paths to clear the coverage floor

Signed-off-by: Andy Anderson <andy@clubanderson.com>

---------

Signed-off-by: Andy Anderson <andy@clubanderson.com>
…fetime (#5000)

* πŸ› fix: scope uncollectible-upgrade de-dup per server and bound its lifetime

undeliverableUpgradeNoted β€” the memory that keeps the hub from writing an
identical "upgrade not armed" timeline entry on every 2-minute poll β€” was a
package-level global. Two hubs in one process shared and clobbered it, so
one server's note suppressed the other's entirely: an operator watching the
second hub saw no refusal at all, which is the exact silence that timeline
entry exists to break. Two hubs carrying the same hive ID is the normal
case in tests, not an exotic one.

The tell was in the tests: every one of them opened by hand-scrubbing the
global for hive IDs its own fresh server had never seen (12 call sites), so
each new test had to remember the incantation and a test that forgot it
failed only in combination with whichever earlier test used the same ID.

Entries also leaked forever. The only removal path ran when a hive was
successfully ARMED, but an uncollectible hive is by definition never armed,
so nothing could ever retire its entry β€” unbounded growth across hive churn,
contradicting the bounded-growth claim in the comment that justified the map,
and precisely for the unassigned-placeholder population this file handles.

Moves the map onto HubServer under its own mutex (not s.mu: the note path
does not hold s.mu and goes on to call recordTimeline), makes
forgetUncollectibleUpgrade a method so it is scoped to its server, and hooks
hive removal β€” removeRegistryEntry and handleRegistryDelete β€” so entries have
a bounded lifetime. The map allocates lazily, since bare &HubServer{} literals
do not pre-allocate it.

Verified by neuter-test: reverting to a shared package global turns the
per-server and removal-lifecycle tests RED while the de-duplication controls
stay green, so the tests detect the old design rather than merely describing
the new one. Race detector clean.

Fixes #4995

Signed-off-by: Andy Anderson <andy@clubanderson.com>

* πŸ› fix: convert #4997's re-arm tests to the per-server forget method

Semantic merge conflict with #4997, which landed first: its new
rearm_collectible_test.go calls forgetUncollectibleUpgrade as a bare
function, while this branch makes it a HubServer method. Git rebases
cleanly because the two touch different lines; the package simply stops
compiling. Caught by building the rebased branch rather than trusting the
clean rebase.

Signed-off-by: Andy Anderson <andy@clubanderson.com>

---------

Signed-off-by: Andy Anderson <andy@clubanderson.com>
…t as shell source (#5001)

The #4938 deny list puts (, ), * and , into backend_perm_flag's output.
The argv consumer (agent-launch.sh, read -r -a) handles that correctly
and is tested β€” but four consumers interpolate the flag string into text
a shell re-parses: the Justfile contribute-hive local mode, the
contributor agent's interactive tmux launch, hive.sh's supervisor
send-keys, and the AGENT_LAUNCH_CMD env files that supervisor.sh
materializes into a launcher via unquoted heredoc. All four died at the
first paren:

    -bash: syntax error near unexpected token `('

before the CLI ever started.

No single spelling is both argv-safe and shell-line-safe (%q escapes
survive read -r, raw parens break the parser), so add
backend_perm_flag_shell β€” per-word %q β€” and move the four shell-line
consumers to it. The raw contract on backend_perm_flag is unchanged.

Contract tests pin all three sides: the _shell output parses as shell
source, reduces to the identical argv, and the raw spelling still does
NOT parse (if it ever does, the variants have converged and one should
be deleted).

Refs #4938

Signed-off-by: Douglas Baggett <doug.baggett@gmail.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Add Danathar to the Adopters List table, following the schema documented
in the file's "How to Add Yourself" section and matching the description
and maturity level of the existing entries.

Signed-off-by: Douglas Baggett <doug.baggett@gmail.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
T0 of the #4907 TUI epic: the design record every later sub-issue reads
before picking up work. All five sections the issue asks for β€” scope,
the verbatim architecture decisions, the layout sketch, the keybinding
table, the testing convention β€” plus two corrections that the sub-issue
bodies do not carry and that change what several of them can do.

PATHS. The epic specifies `v2/internal/tui/...` and `v2/docs/tui.md`.
There is no `v2/` directory and no module at the repo root; the module is
`src/`, which is where go build and every CI shard run from, so a tree at
the repo root would never be compiled or tested. The prefix is not a typo
but a stale spelling: #3996 renamed the `v2/` source tree to `src/`, which
src/pkg/reach/mapping.go:36-37 records verbatim and still compensates for,
because a PR merged before the rename reports `v2/...` paths from the
forge API forever. The page carries the full mapping table so T2 through
T27 do not each rediscover it.

CONTRACT. The epic names `dashboard/openapi.json` as the source of truth
for every client task, with an escape hatch when an operation is missing.
That hatch is going to fire constantly, so the size of the gap is measured
once here rather than surprising each task: the spec publishes 32 routes,
all GET, against 298 the dashboard actually registers (137 GET, 83 POST,
64 PUT, 14 DELETE). It documents NO write operation of any kind, so all
four Phase 2 action tasks β€” T14 pause/resume, T17 model, T19 ACMM, T20
kick β€” have no contract to build against, and each endpoint they need is
already exercised by hivectl today. Two read tasks are hit too, including
T2 (#4917), which is named `/api/health` β€” an endpoint the spec does not
contain. The page cites the live endpoint for each so those tasks can file
the spec-gap issue AND still ship, rather than stalling on a reading that
would block six of them at once and leave the TUI read-only.

Also recorded: the Delivery decision resolves to `hivectl tui` (T1,
#4919) because `hive` is the spoke daemon with no subcommand dispatch
while hivectl is the cobra CLI that already defaults --server to the
:3001 proxy and --token-env to HIVE_DASHBOARD_TOKEN β€” and the one
deviation that forces, since the epic names HIVE_DASHBOARD_URL and
hivectl spells the same thing --server.

Two deliberate deviations from the issue text:

- Location is `src/docs/design/tui.md`, not `v2/docs/tui.md`, per the
  path correction above. src/docs/design/ is this repo's home for
  "longer-form design records ... the reference record for a decision",
  which is exactly what T0 asks for, and its status taxonomy gives the
  page the "design only" header it needs while nothing is shipped.

- The issue says not to touch any docs index because T26 owns that. This
  adds ONE line to src/docs/design/README.md anyway. That file is the
  directory's own index, not the top-level one T26 names, and its
  "Adding a document here" section makes the entry part of the procedure
  for adding a page β€” it states pages here are reached through this index
  rather than through src/docs/README.md. Following the letter of the
  issue would land a page nothing links to, which is the exact failure
  the docs-index-reminder workflow exists to catch. Easy to drop if a
  maintainer disagrees; it is one bullet.

Verified: doc-only, `go build ./...` still passes, and every relative
link resolves. Each numeric claim was measured against origin/v4 at
45a13d5 rather than estimated.

Part of #4907. Closes #4915

Signed-off-by: Danathar <doug.baggett@gmail.com>
Co-authored-by: Andy Anderson <andy@clubanderson.com>
T2 of the #4907 TUI epic: the one place that knows how a request to the
dashboard is addressed and authenticated. Client, New(), Health(), and
the shared getJSON() helper the pane-client tasks (T4/T6/T8/T10) build on.

THE AUTH HEADER, AND WHY IT IS NOT GUESSED. The issue says to send the
token "the same way the web dashboard does per dashboard/openapi.json
security scheme (check the spec; do not guess the header)". Checked: the
spec has no security scheme. It has no `components` block at all β€” the
whole document is `openapi`, `info`, `servers`, `tags`, `paths`, and
nothing else β€” so there is no `securitySchemes`, no top-level `security`,
and no per-operation `security` to read. `/api/health` is not in it
either.

Rather than guess, the header is transcribed from the two things that
actually verify it, which agree:

  - src/proxy/server.js requireAuth() matches `/^Bearer\s+(.+)$/i` against
    req.headers.authorization and timing-safe compares to
    HIVE_DASHBOARD_TOKEN.
  - src/pkg/hivectl/client.go sets `Authorization: Bearer <token>` from
    the same environment variable.

So: `Authorization: Bearer $HIVE_DASHBOARD_TOKEN`, and omitted entirely
when no token is set β€” a bare `Bearer ` fails that regex and would turn a
public endpoint into a 401, which is why an empty token is not sent as an
empty header. Both are pinned by tests.

NO DUPLICATE SPEC-GAP ISSUE. The issue's note says to file one if the
spec is missing what this task needs, and completing that filing
completes the task. #4912 is already open and already reports exactly
this β€” including a recommendation to add "the HIVE_DASHBOARD_TOKEN
security scheme". A second issue would be noise, and stopping at a filing
would leave T4/T6/T8/T10 blocked behind a client that can be written
correctly today from the verifier. Corrections to #4912's route inventory
are posted there as a comment rather than duplicated here.

WHERE /api/health ACTUALLY LIVES. It is a Go dashboard route
(src/pkg/dashboard/server.go) on the isPublicPath list, reachable at the
epic's :3001 base URL because src/proxy/server.js proxies /api/* to the
Go API on :3002. That public status is load-bearing for the TUI: a
tokenless health check is what separates "the dashboard is unreachable"
from "the dashboard is there and my token is wrong", which are different
things to tell an operator. Confirmed independently by
src/pkg/hub/heartbeat.go, which probes http://localhost:3001/api/health,
and by the compose healthcheck.

Also carried over deliberately: the transport lesson from
src/pkg/hivectl/client.go. A nil Transport silently shares
http.DefaultTransport, whose idle pool every parallel httptest teardown
calls CloseIdleConnections on β€” which showed up in this repo before as a
flaky "connection broken" from an unrelated test. This client clones its
own, and a test asserts it.

pkg/hivectl's client was evaluated for reuse instead of a new one. It
speaks the same API with the same token, but its Do() returns `any`
because it feeds the CLI's table/json/yaml printer, so a TUI decoding
into typed structs would re-marshal that map on every poll; and its
constructor returns an error, which this task's New() contract cannot.
The reasoning is recorded in the package doc so the next reader does not
have to redo it.

Tests: httptest fixtures assert path, method, Authorization, and Accept;
the tokenless path; APIError across 401/404/502/500-empty-body; the
error-body cap; getJSON decode, malformed-body, transport-error and
context-cancellation paths; and the two environment variables with their
default. Every load-bearing assertion is mutation-checked β€” swapping
Bearer for Token, sending an empty Bearer, dropping the trailing-slash
trim, sharing DefaultTransport, and unbounding the error body each fail
their test.

Verification: go build ./..., go vet ./..., golangci-lint with the repo
config (0 issues), gosec on pkg/tui (0 issues), every test binary in the
module compiles, and pkg/tui/client is at 96.9% against the gate's 90%
floor. No dependency changes β€” this is stdlib only, so go.mod and go.sum
are untouched and the vulnerability surface is identical to v4.
govulncheck could not be run locally (v1.1.4 crashes in the x/tools SSA
builder on the Go 1.27 toolchain here); CI runs it on Go 1.25.

Out of scope, per the issue: any endpoint other than health, SSE,
retries and backoff.

Part of #4907. Closes #4917

Signed-off-by: Danathar <doug.baggett@gmail.com>
Co-authored-by: Andy Anderson <andy@clubanderson.com>
…on (#4975)

A hive configured with governor.work_source.type: linear aborted every
eval cycle when ghClient.EnumerateActionable failed (e.g. the GitHub App
lacks the Issues permission and every repo returns 403 "Resource not
accessible by integration"). The work-source overlay runs only after that
call, so the Linear backlog was never enumerated and the queue sat at 0
until an Issues permission a Linear-sourced hive should not need was
granted.

Extract the decision into actionableAfterGitHubEnumerate: on the default
GitHub work source the cycle still aborts and keeps prior state; on a
non-GitHub work source the failure is logged as a warning naming the work
source and the cycle continues with whatever partial result GitHub
returned (an empty result when nil), letting the overlay populate issues.


Claude-Session: https://claude.ai/code/session_019Ukvc9PBjCzVJQ1ZSX5GH7

Signed-off-by: Gregory Hunt <greg@on-board.ai>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Documents who responds to a vulnerability report (the Maintainer
Committee, rostered in OWNERS), the end-to-end handling flow, the
60-day publicly-known-vulnerability fix commitment recorded on the
OpenSSF Best Practices badge, how security-response membership is
added/removed via the existing maintainer lifecycle, an honest note
that the current cross-org roster is incidental rather than a policy
target, the escalation path (Maintainer Committee -> KubeStellar
Steering Committee / CNCF CoC channels), and the project's stated
known limits (no on-call, no third-party audit, no CVSS policy).

Adds a 'Who Responds' pointer in SECURITY.md and indexes the new page
in src/docs/README.md. Does not change or duplicate the existing
SECURITY.md reporting process.

Signed-off-by: Andy Anderson <andy@clubanderson.com>
* πŸ“– docs: add CNCF General Technical Review

Answers the CNCF GTR questionnaire (Day 0 Planning, Day 1 Installation
and Deployment, Day 2 Day-to-Day Operations) against this repository,
citing file:line, doc, or workflow evidence for every substantive claim.
Questions with no supporting evidence in the repo are marked
[NEEDS OPERATOR INPUT] with a one-sentence description of the gap
rather than answered speculatively.

A summary block at the top of the document gives the answered/needs-input
counts and groups the outstanding items by section for operator follow-up.

Adds an index entry in src/docs/README.md alongside the existing
CNCF TAG-Security self-assessment.

Signed-off-by: Andy Anderson <andy@clubanderson.com>

* πŸ“– docs: resolve all operator-input markers in General Technical Review

Fills in the 13 NEEDS OPERATOR INPUT markers with the operator's answers:
vision/goals, use-case scope, target personas, adopter org types, end-user
research (citing real adopter-filed issues), sovereignty posture,
compliance, certificate rotation boundary, SLO/SLI/load-testing/limits
(all none), third-party attribution (known gap, PR in flight), and
security-response policy (known gap, issue being filed).

Updates the summary block to 73 answered / 0 pending and replaces the
per-section NEEDS OPERATOR INPUT index with a Known gaps list naming the
five real standing gaps: no SLOs, no load testing, no NOTICE file, no
security-response policy, no formal compliance, no third-party audit.

Signed-off-by: Andy Anderson <andy@clubanderson.com>

---------

Signed-off-by: Andy Anderson <andy@clubanderson.com>
Adds a repo-root NOTICE file listing Go module dependencies compiled
into hive/hive-hub/hive-contributor, addressing a CNCF General
Technical Review gap: go.sum pins dependency versions but nothing
assembled an attribution document from it.

- src/scripts/generate-notice.sh: walks the resolved module graph
  with google/go-licenses, pinned to an exact tagged version and
  installed via `go install`, matching the existing govulncheck/gosec
  pattern in go-security-analysis.yml. See the script header for why
  go-licenses was chosen over a hand-rolled go-list walk.
- go-security-analysis.yml: new `notice-drift` job regenerates NOTICE
  on every change to src/go.mod, src/go.sum, or the script (plus
  weekly) and fails the build if the committed file has drifted.
- release.yml: attaches NOTICE to the GitHub Release alongside the
  existing per-image SBOMs, same asset-upload call.
- src/docs/releases.md: documents generation, enforcement, and
  scope β€” explicitly what NOTICE does NOT cover (base-image OS
  packages, from-source Node/tmux layers β€” those stay on the SBOMs).

The committed NOTICE cannot yet be the real, go-licenses-generated
file: this change was authored in an environment where `go` cannot be
run, so no `go` tooling (including go-licenses itself) could produce
the actual resolved module graph or verified license text. Rather
than fabricate license identifiers, NOTICE is a placeholder derived
statically from src/go.mod's require blocks only, with every license
field marked UNVERIFIED and the file's own header explaining exactly
that. The first notice-drift CI run is expected to fail once, showing
the real diff; a maintainer applies that regenerated output (or runs
generate-notice.sh locally with go installed) and commits it.

CHANGELOG entry under Added.

Signed-off-by: Andy Anderson <andy@clubanderson.com>
Console is maintained by a hive today β€” issue triage, fixes, review, and merge
run continuously against that repo. It is the first entry at the Production
maturity level; every other adopter is Pre-production.

Also fixes a malformed link in the same table: the Open Horizon Services row
was missing the closing bracket on its markdown link, so it rendered as
literal text rather than a link.

ADOPTERS.md is read directly during CNCF maturity review β€” the Incubation
criteria ask for a publicly documented adopter list with adoption levels β€” so
a broken link there is more visible than in ordinary docs.

Signed-off-by: Andy Anderson <andy@clubanderson.com>
…at shipped (#5009)

Two answers in the General Technical Review described work as "being
dispatched". Both landed, so the answers now describe what exists.

Security response (#5005): src/docs/security-response.md documents who
responds, how a report is handled end to end, the 60-day fix commitment,
membership changes through the existing maintainer lifecycle, and escalation.
The GTR now cites it instead of recording a gap.

Third-party attribution (#5006): the mechanism exists β€” generator, notice-drift
CI guard, release attachment β€” but the answer deliberately does NOT claim the
work is finished. The committed NOTICE is a statically-derived placeholder
whose license fields all read UNVERIFIED, because it was assembled without
running Go tooling and no license was inferred. A reviewer who follows the link
finds exactly that, so the document says it up front and points at #5007 for
the one remaining maintainer step.

The Known gaps list is updated in the same spirit. "No security-response
policy" becomes "no on-call or diversity target", which is what is actually
missing now that the process is written β€” the document is explicit that it
defines neither, and that response depends on whichever of three part-time
maintainers sees a report first.

Overstating either item would be the expensive kind of error here: a reviewer
who catches one inflated claim discounts the answers that are solid.

Signed-off-by: Andy Anderson <andy@clubanderson.com>
…the binary (#4974)

On a hive whose config path is read-only β€” the ordinary shape of a
bind-mounted hive.yaml under Docker/podman β€” entrypoint.sh falls back to
reading /data/hive.yaml.runtime directly and signals that by exporting
HIVE_CONFIG. That export was inert.

main.go reads HIVE_CONFIG only to choose the DEFAULT of its -config flag,
and the image's own CMD passes that flag explicitly:

    CMD ["--config", "/etc/hive/hive.yaml"]

An explicit flag outranks a default, so the binary loaded the stale,
unwritable file the entrypoint had just decided not to use.

The damage outlives the boot. Config.Save() writes
/data/hive.yaml.runtime unconditionally, so the first save of any kind β€”
a dashboard edit, a heartbeat-delivered config β€” wrote the stale config
back over the operator's real persisted state. Reported as an ACMM level
set from the dashboard reverting L4 -> L3 on every restart, reproduced
twice, with /data fully intact (#4973).

main.go cannot fix this alone: an explicit --config coming from the
image's own CMD is indistinguishable from one an operator typed. The
entrypoint is the component that knows the config path is stale, so it
now says so in the argv it launches with, appending
`--config "$HIVE_CONFIG"`. Appended rather than substituted because
flag.Parse keeps the LAST occurrence of a repeated flag, so this wins
without the script having to parse and rewrite the CMD. When
HIVE_CONFIG is unset β€” the ordinary boot, where the copy succeeded and
the config path IS current β€” nothing is appended and the CMD applies
unchanged.

Verified end to end against the real script: with a read-only config
path holding acmm_level 3 and a runtime config holding 4, the binary
resolved /etc/hive/hive.yaml (level 3) before and
/data/hive.yaml.runtime (level 4) after.

Also adds a WARN when the loaded config path disagrees with
HIVE_CONFIG. That state should now be unreachable, but it was
previously invisible from every vantage point: /api/config/provenance
reads HIVE_CONFIG directly, so it reported the file the entrypoint
chose while the process ran on the one it did not β€” which is exactly
what made this hard to diagnose from outside the container.

Tests extract the real launch block from entrypoint.sh by marker rather
than paraphrasing it, and cover: the read-only branch firing at all, the
redirect landing in argv, an unset HIVE_CONFIG leaving the CMD alone,
the launch still passing "$@", and the Dockerfile/entrypoint coupling
that broke β€” an image that ships an explicit -config must be paired with
an entrypoint that outranks it.

Signed-off-by: Danathar <doug.baggett@gmail.com>
…e hives (#4979)

The Linear agent install and the OpenRouter funding flow built their
redirect_uri from hub.dashboard_url if set, else from the request's
X-Forwarded-Proto/X-Forwarded-Host/Host. On a hub-less hive whose
dashboard is private but whose /linear/callback is published on another
hostname β€” behind an ingress that rewrites Host (Traefik with a fixed
upstream Host, a Cloudflare Tunnel "HTTP Host Header") β€” the install leg
and the callback leg derived different origins and Linear rejected the
code exchange with "redirect_uri is invalid". The only knob was
hub.dashboard_url, a hub-namespaced field that is wrong for a hive with
no hub and discoverable only by reading source.

Add dashboard.public_url: the externally reachable origin of this
dashboard. Precedence is dashboard.public_url β†’ hub.dashboard_url (kept
for hub-hosted spokes) β†’ forwarded/request host, implemented once in
oauthPublicOrigin and used by both callback builders. The value is
validated at load time as an absolute http(s):// origin with no path,
query, fragment or credentials (trailing slash trimmed). The install
endpoint now also returns the redirect_uri it used beside authorize_url
so an operator can see the value Linear must match.

Tests: precedence/trimming table, validation table plus a Load-level
error test, and a handler test where X-Forwarded-Host differs between
the install and callback requests and both legs must still agree on
redirect_uri β€” that test fails on the parent commit.


Claude-Session: https://claude.ai/code/session_019Ukvc9PBjCzVJQ1ZSX5GH7

Signed-off-by: Gregory Hunt <greg@on-board.ai>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Andy Anderson <andy@clubanderson.com>
…ead of GitHub (#4981)

The dashboard's ACMM "Open Issue" / "Open All" buttons always created a
GitHub issue via the go-github client, even on a hive whose backlog lives
in Linear (governor.work_source.type: linear). Operators of such hives got
GitHub issues nobody triages.

Config: governor.acmm.issue_tracker: github | work_source (default github;
unset hives see zero behavior change; any other value fails validation).
work_source means "file where the backlog lives": a Linear work source
creates the issue via GraphQL issueCreate on the team whose teams[].repo
matches the criterion's repo (else the first team); GitHub / GitHub
Projects / Jira / unset stay on GitHub.

API: POST /api/acmm/issue accepts an optional `tracker` field
("github" | "work_source") that overrides the config for one request;
unknown values are a 400. The response now carries `tracker`
("github" | "linear") and, for Linear, `identifier` and `team`.
GET /api/acmm/evaluation reports the effective default as `issue_tracker`
plus `work_source_type`, stamped from live config on every response so a
config edit is visible without waiting for the evaluation cache.

Linear: LinearSource gains CreateIssue (team-key lookup + issueCreate) and
TeamForRepo, sharing the existing endpoint/auth path (bare API key) through
a new doGraphQL seam that doQuery now delegates to. The issue title and
body are rendered once (acmmIssueContent) and are identical on both
trackers; the audit entry records tracker either way.

UI: when the work source is Linear, the level dialog shows a "File in:
GitHub / Linear" selector pre-selected from config; issue links render the
Linear identifier (ENG-42) instead of #42.

Tests: config default + validation; request-vs-config resolution and the
400; Linear creation against an httptest GraphQL server (team routing,
verbatim title/body, bare Authorization header); GitHub path unchanged.


Claude-Session: https://claude.ai/code/session_019Ukvc9PBjCzVJQ1ZSX5GH7

Signed-off-by: Gregory Hunt <greg@on-board.ai>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
… at all (#5010)

The notice-drift job introduced in #5006 failed on its first real run against
v4 β€” but not from the placeholder drift it was designed to detect. It failed
before generating anything:

  Failed to find license for github.com/kubestellar/hive/pkg/forge: cannot
  find a known open source license for ".../hive/src/pkg/forge" ... and
  locates up until ".../hive/src"

Every error names a github.com/kubestellar/hive/... path β€” the project's own
packages, not a dependency. The cause is this repository's layout: LICENSE
lives at the REPO root while the Go module is src/, so go-licenses' upward
search stops at src/ and never sees it. It then exits non-zero and no NOTICE is
produced.

Both invocations now pass --ignore github.com/kubestellar/hive. That is the
correct answer rather than a workaround: a THIRD-PARTY attribution file should
not list the project's own code, so excluding it is what the file means. The
alternative β€” a duplicate src/LICENSE purely to satisfy the tool's search path
β€” would add a second copy of the license to keep in sync for no benefit, and
this repo has already been bitten today by a LICENSE that drifted from
canonical.

Until this lands the job fails on every PR in the repo, including contributors'
work that has nothing to do with it, which trains people to read red as noise.

Signed-off-by: Andy Anderson <andy@clubanderson.com>
Signed-off-by: Danathar <doug.baggett@gmail.com>
Merge commit (never squash) preserving both lineages: v5's RFC line
(#4000 approvals desk, #4001 hooks, #4002 turn model, reviewer role
#5002) and v4 production.

Conflict rule applied: v5 wins in the RFC packages, v4 wins everywhere
else. Two exceptions were adjudicated on evidence rather than the rule:

- pkg/turn: v4 grew an INDEPENDENT prototype (#4933) that collided with
  v5's full RFC #4002 implementation. v5 won, and v4's prototype-only
  runner_test.go + store.go were removed β€” they auto-merged cleanly but
  test an API v5 does not have (effectID redeclared; caught by go vet,
  not by go build).
- level-5/6 packs: both sides APPENDED different agents at the same
  position. Kept the union (v4 telemetry+operations, v5 reviewer) and
  set the pack-count assertion to 12/13 β€” a value neither side had.

pkg/hooks needed no adjudication: v4's copy is a byte-identical port
from v5 (#4837).

Signed-off-by: Andy Anderson <andy@clubanderson.com>
@kubestellar-prow kubestellar-prow Bot added the dco-signoff: no Indicates the PR's author has not signed the DCO. label Aug 28, 2026
@kubestellar-prow

Copy link
Copy Markdown
Contributor

[APPROVALNOTIFIER] This PR is NOT APPROVED

This pull-request has been approved by:
Once this PR has been reviewed and has the lgtm label, please assign clubanderson for approval. For more information see the Code Review Process.

The full list of commands accepted by this bot can be found here.

Details Needs approval from an approver in each of these files:

Approvers can indicate their approval by writing /approve in a comment
Approvers can cancel approval by writing /approve cancel in a comment

@kubestellar-prow kubestellar-prow Bot added the size/XXL Denotes a PR that changes 1000+ lines, ignoring generated files. label Aug 28, 2026
v4's release-line guard (#4405) arrived in this merge declaring
`release_lines: [v2, v4]`, and met v5's workflow triggers that already
list v5. Neither parent fails alone β€” v5 has no guard, and on v4 no
workflow names v5 β€” so the merge is what surfaces it. That is exactly
the condition the guard exists to catch, so it is fixed here rather
than waived.

Per the manifest's own instructions, cutting a line takes three edits:

- release_lines: [v2, v4] -> [v2, v4, v5]
- every pinned workflow gains v5: go-security-analysis, podman-{contract,
  rootful-lane,rootless-lane,arm64-lane}, quadlet-gate (v2-ci, v2-tests
  and suid-contract already had it)
- env_lists: docker.yml LONG_LIVED already carried v5

scorecard.yml is recorded as an EXCLUSION (`-v5`), not widened:
scorecard-action hard-fails on any non-default branch
("refs/heads/v5 not supported with push event"), which is why 90dea44
dropped v5 from its trigger. Writing it down keeps that a justified
line rather than a silent gap.

test-release-lines-guard.sh built its "plausible future release line"
fixture out of v5, which became a no-op once v5 was real β€” the
demonstration passed while asserting nothing. Moved to v6 so the
negative control still proves the guard catches a stale allow-list.

Signed-off-by: Andy Anderson <andy@clubanderson.com>
Semantic conflict from the v4 top-up: v4 tightened CSP to
`script-src-attr 'none'` and added TestEmbeddedStaticHasNoInlineEventHandlers
(#4680), while v5's RFC #4000 approvals panel was written with inline
`on*=` handlers. Both sides merged cleanly β€” the panel's HTML was v5-only
and the guard was v4-only, so git had nothing to flag β€” but the merged
result would have shipped an Approvals panel whose every control is dead
under the new policy: the buttons render, the clicks do nothing, and
nothing reports an error.

Converted all 7 to the data-action dispatch idiom already used by the
sibling sections, rather than weakening the test or relaxing the CSP:

- section header -> data-action="toggleSection" data-arg0="approvals-section"
  (matches beads-section / platform-section verbatim)
- Approve/Reject selected -> data-arg0 true|false with data-arg-types="j"
- filter chips -> data-arg-types="j", preserving the null sentinel that
  distinguishes "all" from a named rule
- row checkbox -> data-change-action="approvalsToggle" (the change
  listener's default branch already routes to hiveDispatchAction)
- per-row Approve/Reject -> data-arg-types="s,j" with the id escaped
  through escapeHtml, matching the repo-pr-pill precedent

Verified by neuter test: re-adding a single inline onclick puts the guard
back to red (1 handler reported), so the test still detects what it claims
to and this is not a green-by-construction fix.

Signed-off-by: Andy Anderson <andy@clubanderson.com>
@kubestellar-prow

Copy link
Copy Markdown
Contributor

Thanks for your pull request. Before we can look at it, you'll need to add a 'DCO signoff' to your commits.

πŸ“ Please follow instructions in the contributing guide to update your commits with the DCO

Full details of the Developer Certificate of Origin can be found at developercertificate.org.

The list of commits missing DCO signoff:

Details

Instructions for interacting with me using PR comments are available here. If you have questions or suggestions related to my behavior, please file an issue against the kubernetes-sigs/prow repository. I understand the commands that are listed here.

@clubanderson
clubanderson merged commit ce56697 into v5 Aug 28, 2026
55 of 58 checks passed
@kubestellar-prow
kubestellar-prow Bot deleted the sync/v4-into-v5-topup-2 branch August 28, 2026 18:56
@github-actions

Copy link
Copy Markdown
Contributor

Thank you for your contribution! Your PR has been merged.

Check out what's new:

Stay connected: Slack #kubestellar-dev | Multi-Cluster Survey

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

dco-signoff: no Indicates the PR's author has not signed the DCO. size/XXL Denotes a PR that changes 1000+ lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

10 participants