68e557e9f moved the Docker agent's JSON-marshal hook from a package global
to a per-Agent field, which touches internal/dockeragent/agent.go and
internal/dockeragent/container_update.go. It meant to take the
Contract-Neutral bypass, but a blank line separated the trailer from
Co-Authored-By, so git's trailer parser dropped it, CI ran the completion
guard without a reason, and Canonical Governance went red on three counts:
missing contract docs/release-control/v6/internal/subsystems/agent-lifecycle.md,
missing verification artifact for "agent runtime transport trust proof", and
missing verification artifact for "Docker container recreate, rollback,
durable result, and live network-mode proof". The sibling timer-hook commit
8e5ef365d has the same defect, so this backfill covers both seams.
Document the seams in the agent-lifecycle contract's Current State: both
newTimerFn and jsonMarshalFn are unexported Agent fields reached through the
newTimer and jsonMarshal methods, nil falls back to time.NewTimer and
json.Marshal, no package-level hook global remains in internal/dockeragent,
and injection happens at construction only so the fields need no mutex. The
seams stay internal — no enrollment, transport trust, command admission,
acknowledgement, or recreate/rollback semantics move, and they must not be
promoted into NewAgent options or any server-facing surface.
Pin that shape in the two registered verification artifacts.
agent_internal_test.go asserts the field names, types, and unexported-ness,
parses every non-test source in the package to fail if either hook returns as
a package-level var, proves the nil defaults run the standard library, drives
two Agents with different hooks concurrently so the isolation is checked under
-race, and proves sendCommandAck marshals through the receiver's own seam.
container_update_test.go proves decodeUpdateContainerPayload routes through
a.jsonMarshal, that an injected failure never reaches a sibling Agent, and
pins the method form at compile time.
Guard dry-run over the staged set passes with no Contract-Neutral bypass, and
replaying 68e557e9f's file list plus these three files is green, so the
original commit would have passed had it carried them.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Tests swapped the package-level newTimerFn hook while async goroutines
leaked from earlier tests (backup-cleanup and stop-command paths) were
still reading it in waitForAsyncDelay, tripping the race detector on CI.
Replace the global with a per-Agent newTimerFn seam (nil defaults to
time.NewTimer), make waitForContextDelay an Agent method, and inject the
immediate timer into the tests that previously swapped the global.
Verified with go test ./internal/dockeragent/ -race -count=20.
Contract-Neutral: test seam refactor to fix data race, no public contract delta
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
One-click updates recreated containers from the raw inspected config, so
Docker rejected the create for any container using network_mode:
container:<id> (compose service:<x>) or host with "conflicting options:
hostname and the network mode", leaving the workload stopped under its
_pulse_backup_ name until the rollback rename restored it. Docker fills
Config.Hostname with the namespace owner's ID on such containers, so the
verbatim copy always tripped the daemon validation (verified against a
real Docker 29.5.2 daemon: verbatim create 409s, sanitized create
succeeds and the replacement starts in the shared namespace).
Strip the namespace-owned settings before ContainerCreate: hostname and
domainname for container:/host modes, plus exposed/published ports,
links, DNS options, and extra hosts for container: mode, all of which
the daemon derives from the owning namespace. Bridge and user-defined
networks are untouched.
Refs #1564
Docker container URL preserved on update (#1054): container updates
recreate the container with a new runtime ID. The agent now includes
{oldContainerId, newContainerId} in the completion ACK payload; the
server uses this to copy persisted metadata (custom URLs, descriptions,
tags) to the new ID so nothing is lost. Migration is a copy, not a move,
so rollback scenarios still find metadata under the original ID.
Reduce metrics.db write amplification (#1124): add a UNIQUE index on
(resource_type, resource_id, metric_type, timestamp, tier) so rollup
reprocessing after a failed checkpoint uses INSERT OR IGNORE instead of
creating duplicate rows. Existing duplicates are deduplicated once on
startup if the index creation would otherwise fail. Also sets
wal_autocheckpoint(500) to checkpoint the WAL more frequently, preventing
unbounded WAL growth.
Fixes#1054Fixes#1124
- Add comprehensive test coverage for agent report, flush buffer, and deps
- Expand flow, HTTP, CPU, and swarm test coverage
- Refactor registry access to use deps interface for better testability
- Add container update and self-update test scenarios
Allows specifying which IP address the agent should report, useful for:
- Multi-homed systems with separate management networks
- Systems with private monitoring interfaces
- VPN/overlay network scenarios
Usage:
pulse-agent --report-ip 192.168.1.100
PULSE_REPORT_IP=192.168.1.100 pulse-agent
Content was being streamed twice:
1. During each iteration of the tool loop (intended for intermediate feedback)
2. Again after the loop ended with finalContent (redundant)
This caused duplicate responses when using Ollama and other providers.
- Add container update command handling to unified agent
- Agent can now receive update_container commands from Pulse server
- Pulls latest image, stops container, creates backup, starts new container
- Automatic rollback on failure
- Backup container cleaned up after 5 minutes
- Added comprehensive test coverage for container update logic