An explicit --report-ip is the user naming the primary address on a
multi-NIC host, but identityFromHost appended it after the auto-detected
interface addresses while every consumer of ResourceIdentity.IPAddresses
treats the first entry as primary, so the override never changed what
the Machines table displayed. Prepend it instead.
The install script also rejected --report-ip as an unknown argument even
though the agent supports the flag, forcing hand edits to the service
unit that a later --update run would drop. Accept the flag, render it
into the service ExecStart, persist it in connection state, and
recognise it during saved-state and arg-stream recovery so updates
preserve it.
Refs #829
Contract-Neutral: behavioral fix: user-specified report-ip leads host identity addresses and the installer passes --report-ip through; no public contract change (#829)
Keep macOS notarization mandatory for every release candidate while requiring Windows Authenticode only for stable promotion, matching the publish workflow and RC4 release packet.
New scripts/release_control/format_staged_frontend.py mirrors the staged
Go formatter: formats staged frontend-modern/src {ts,tsx,css,json} blobs
through prettier --stdin-filepath, writes results back to the index
directly (no broad restaging), syncs the worktree only when it matches
the previously staged content, and iterates to a fixed point to absorb
prettier's occasional non-idempotence. Skips gracefully when prettier is
not installed (fresh clones, linked worktrees without node_modules).
Wired into .husky/pre-commit after the Go formatter, with unit tests in
the governance battery, a README note, and a .gitignore allowlist entry.
With the one-time sweep in the previous commits, prettier drift can no
longer re-accumulate and make format stays clean on a clean tree.
Use the installer-owned ProgramData directory consistently for token lookup, enrollment state, agent identity, receipts, and service runtime data. Pass that directory explicitly to the Windows service and retain the existing Linux default elsewhere.
Contract-Neutral: Windows agent state-path alignment preserves the existing installer and agent public contract
Contract-Neutral: Legacy Windows PowerShell TLS enum compatibility preserves the existing installer transport contract without changing its public interface
A fresh install's natural first question ("how is my machine doing?",
observed live with Ollama qwen3:8b) made the model call pulse_summarize
action=fleet without resource_ids; the tool errored and the agentic loop
surfaced a structured question asking a first-run user for "the
comma-separated list of resource IDs" — pure jargon, funnel-killing
(GitHub discussion #1042). Two root fixes:
pulse_summarize self-targets. action=fleet with resource_ids omitted now
enumerates the known fleet from the executor's unified resource provider
(infrastructure parents first, then guests, then storage; deduped;
bounded at the existing 50-resource cap with a truncation note). Both
modes resolve what models actually pass — canonical unified IDs and
unambiguous names — onto the reporting request shape the way the API
path's resolveReportSubject does: the canonical ID stays ResourceID
(findings/recovery keying) and the resolved metrics target rides
MetricsResourceID, so store queries find data instead of silently
returning zero points. Reporting types are classified from the unified
resource (agent-backed hosts "agent", pure Proxmox nodes "node" — the
documented target-type exception — Docker hosts "docker-host");
resource_type becomes an optional filter/default. Remaining error paths
instruct the model to enumerate or retry and forbid asking the operator
for resource IDs.
Ask-user policy covers tool-argument recovery. The system prompt's
resolve-before-asking section (945e2198's target policy) now extends to
failed tool calls: missing/invalid arguments are self-recovered
(enumerate, retry), and internal identifiers are never valid questions.
The pulse_question description carries the same prohibition so the
structured clarification surface cannot become an identifier elicitation
channel on small local models.
ai-runtime and api-contracts contracts updated in-commit (the
subsystem_lookup line pin follows the api-contracts insertion); full
./internal/ai/... and ./internal/agentcapabilities/... suites green.
The 30d window said 18 of 26 approved action attempts failed but
carried zero cause information. Every approved attempt that is not a
verified success now lands in exactly one content-free bucket:
- pre_dispatch: terminally refused before dispatch (plan drift, expiry,
emergency stop, policy authorization)
- execution: dispatched execution failed or ended inconclusive
- unverified: execution succeeded but outcome verification was not
confirmed (reads as success in the UI, counted as failure here)
- stuck_executing: still executing over an hour after dispatch
plus one sanitized machine reason code for the most recent failure.
Successes, failure buckets, and recent in-flight attempts partition the
attempt count, so the next window explains its own gap.
RefuseActionExecution now persists the specific refusal code
(plan_drift, action_plan_expired, ...) as the canonical execution
reason code instead of the generic pre_dispatch_refused, so audit truth
and telemetry distinguish refusal causes without message parsing.
Privacy disclosures updated in both PRIVACY.md copies; the adoption
report script surfaces the new counters.
Contract-Neutral: additive content-free telemetry counters + specific pre-dispatch refusal reason codes; privacy disclosures updated in-commit per security-privacy extension point
fetch_rows_remote loaded every row in the window into a list and
json.dumps'd the whole payload on the remote side; on the 1GB
pulse-license droplet a 30-day window over a 104k-row DB got the
process OOM-killed. The remote helper now prints a db_stats header
line and then one JSON line per row while iterating the cursor, so
it never holds the full result set. Local parsing updated to match
and covered by unit tests; fetch_rows_local is unchanged.
Verified end to end against the droplet with --since-days 30.
Contract-Neutral: fetch_rows_remote OOM fix: stream JSON-lines instead of one giant payload; no contract delta, report output unchanged
Production telemetry (7d, 2026-07-17) shows a persistent tail of live
installs on 6.0.0-rc.x and 6.0.3/6.0.4 while 6.0.5 has been stable since
2026-07-09. Root cause for the reachable stranding class: release
selection trusted GitHub's created_at ordering, and this repo interleaves
v5-line maintenance releases with v6 releases (v5.1.36 was created the
day before v6.0.5). Whenever a v5.1.x release is the most recently
created stable:
- stable channel (getLatestReleaseForChannel) returned the first
non-prerelease in list order -> "no update" for every v6 install
until the next v6 release ships
- the RSS rate-limit fallback returned the first feed entry matching the
channel, same failure, both channels
- the unattended updater trusted /releases/latest, which GitHub defines
as most-recently-created stable -> timer no-ops in the same window
All three now select the highest eligible version: the API path tracks
newest stable + newest prerelease in one pass (prerelease = GitHub flag
OR tag shape), the feed fallback picks the max matching the channel, and
pulse-auto-update.sh scans /releases pairing each tag with its own
draft/prerelease flags before the fail-closed shape filter, keeping
/releases/latest as fallback. helm-chart-* tags fail semver parsing and
are dropped everywhere.
Verified not broken (pinned by new tests): rc-channel installs are
offered the newer stable (6.0.0-rc.6 -> 6.0.5) and move onto the next rc
line when one opens (-> 6.1.0-rc.2); the auto-update prerelease filter
(fail-closed is_prerelease_tag, metadata-flag refusal) still refuses
prerelease targets on stable. Go tests fail on the pre-fix code
(returned 5.1.37 / "no update").
Contract: deployment-installability now pins version-max release
selection with proofs in manager_stranded_upgrade_test.go and
pulse_auto_update_test.go.
Every mock-mode boot (e2e/CI managed backends, qual runs, demo containers)
was sending startup/heartbeat pings describing the synthetic fixture fleet,
registering as real installs in the license-server telemetry DB and
dominating the VMware/Kubernetes/TrueNAS adoption aggregates (346 of 6,225
weekly-active installs matched the mock signature, incl. 120 of 227 on
6.1.0-rc.1).
- internal/telemetry: route startup and heartbeat sends through sendEvent,
which drops the ping while mock.IsMockEnabled() is true; checked per event
so runtime mock toggles take effect immediately. Disclosed in the package
doc and both PRIVACY.md copies.
- scripts/telemetry_adoption_report.py: exclude historical mock-fleet rows
(kubernetes_pods = 120×N with vmware_hosts = 7×N, the internal/mock
fixture template) by default, report the excluded row/install counts, and
add --include-mock-fleet to audit them. Versions through 6.1.0-rc.2 keep
pinging the mock fleet until upgraded, so report-side filtering stays
necessary.
- security-privacy contract: record the mock-mode suppression boundary and
the report-side mock-fleet exclusion as subsystem invariants.
- Tests: Go sendEvent suppression/sending pair against an httptest endpoint;
Python signature-matcher and summarize_rows exclusion coverage.
The core.bare=true corruption of the shared repository recurred on
2026-07-17: a script that runs scratch git commands while the pre-commit
environment from a linked worktree (absolute GIT_DIR) is still exported
re-initializes the REAL repository as bare. a0fda6b26 fixed four helper
test files but missed two spots in scripts/release_control/internal:
- verify_commit_slice_test.py's git() helper only popped GIT_INDEX_FILE,
so its scratch 'git init' calls re-init the real repo when GIT_DIR is
inherited. It now scrubs via the shared repo_file_io.strip_local_git_env.
- verify_commit_slice.py's production git_env() kept the inherited hook
env even when unit tests patch REPO_ROOT to a temporary repository,
pointing git plumbing (including index writes) at the wrong repo. It
now scrubs in the test-patched branch only, matching format_staged_go.
Regression teeth:
- verify_commit_slice_test.py gains a canary test that exports the real
hook env shape (absolute GIT_DIR + GIT_INDEX_FILE, no GIT_WORK_TREE —
with GIT_WORK_TREE set the corruption does not reproduce) against a
scratch repo + linked worktree and asserts core.bare stays false.
- repo_file_io_test.py (runs in the pre-commit battery) gains a static
guard failing any release-control *_test.py that runs scratch
'git init' without referencing strip_local_git_env.
- The six hand-rolled 4-var pop loops from a0fda6b26 migrate to the
shared strip_local_git_env helper so the guard enforces one pattern.
Verified: full release-control battery green; every touched test file
also green with GIT_DIR/GIT_INDEX_FILE pointed at a canary repo's linked
worktree, canary config and status intact afterward.
Pre-commit runs from a linked git worktree export an absolute GIT_DIR.
The release-control test helpers and the test-patched branch of
git_env() only removed GIT_INDEX_FILE, so their scratch-repo commands
targeted the REAL repository: 'git init' in a tempdir re-initialized it
with core.bare=true (breaking git status/commit for every checkout and
worktree) and the subsequent scratch 'git add' failed the hook.
Scrub GIT_DIR, GIT_WORK_TREE, GIT_INDEX_FILE, and GIT_COMMON_DIR in:
- git_env() of format_staged_go.py, governance_stage_guard.py, and
subsystem_contracts.py (test-patched branch only; production hook
behavior unchanged)
- the scratch-repo git() helpers in format_staged_go_test.py,
governance_stage_guard_test.py, readiness_assertion_guard_test.py,
subsystem_contracts_test.py
Matches the pattern contract_audit_test.py and status_audit_test.py
already used. Verified the full hook test battery passes with
GIT_DIR/GIT_WORK_TREE/GIT_INDEX_FILE pointed at a canary repo, which
stays un-corrupted.
Canonical Governance failed on nearly every push today (6 of the last 8
commits) because the local pre-commit hook and the CI workflow disagreed
about the canonical completion guard in two ways.
Frontend-only commits skipped the guard entirely: the hook gated ALL
governance checks behind governance-path detection as a perf
optimization, but subsystem contracts name canonical frontend files, so
those commits landed locally and failed the same guard in CI. The hook
now always runs the (cheap) completion guard; only the multi-minute Go
test and audit battery stays path-gated.
Contract-neutral bypasses did not travel: PULSE_ALLOW_CONTRACT_NEUTRAL_COMMIT
authorized the commit in the local shell and vanished, so CI re-ran the
guard without it and went red. A new prepare-commit-msg hook records the
reason as a Contract-Neutral trailer in the commit message, and the
workflow now evaluates the guard per commit (each commit's file list
against its own parent), honoring the trailer exactly as the commit-time
hook honored the env var. Per-commit evaluation also stops a compliant
commit from being blamed for range-mates.
The new hook is registered as a worktree-sensitive governance file in
governance_stage_guard.py with a matching unit test assert.
The Windows installer compared the downloaded agent's --version output
(v6.0.5) against the server's /api/version value (6.0.5) literally, so
every matching install still warned about a version mismatch (#1527).
Strip the leading v from both sides before comparing, matching what
install.sh already does.
Two agent-install failures from stock NAS shells: QNAP ships no od, so
the ELF header sniff read empty and rejected a valid download (issue
#1572); Synology DSM 7's patched systemd cannot apply the unit's
sandbox directives and killed the service with
status=227/NO_NEW_PRIVILEGES before exec (issue #1578). The header
sniff now falls back od -> hexdump -> xxd and skips with a warning when
none exist (checksum verification still guards integrity), and the
systemd unit omits the sandbox hardening block on DSM.