◐ Off-By-One · answer catalog

durable-registry-reconcile-exact-port-fail-closed

2 answer(s)golinuxgolinux

Problem class: durable-registry-reconcile-exact-port-fail-closed

📦 Source in repository (JSON)

Answer 1

The repository is clean and all verification commands pass. Here is the verified solution, saved to /workspace/GAP-070-solution.md:


GAP-070 — Fail-closed exact-port reconciliation for the durable agent registry

Problem class: durable-registry-reconcile-exact-port-fail-closed Repository: deployBunker/bunker @ ca01d289debf3de0b75b0ffd2ffc083c400ed098 Error: "Reconciliation reported adoption/restoration while no exact port reservation was held, leaving double-allocation risk."

1. Symptom

On startup, reconciliation replays the append-only JSONL registry and matches it against bunker-* Linux users. An agent could be reported restored or adopted while its exact persisted port sub-range was not held by the in-memory allocator. The next Spawn then sees those ports as free and hands the same range to a new agent — silent double allocation.

Residue states: (1) tracker registered before port restore, with no rollback; (2) adoption from a guessed/default range without reading <home>/.bunker/ports; (3) unsafe live registry record left in place so a restart resurrects it.

2. Root cause

The reconcile sequence was effect-before-proof: it mutated the tracker (and on adopt, the registry) before proving the exact persisted range could be re-established, with no fail-closed cleanup. Reconciliation is the last gate before serving traffic, so "tracked but un-reserved" is advertised as fully managed. Invariant violated: an agent is served only while both the live tracker record and the exact port reservation exist.

3. The fix

Four rules in internal/agent/reconcile.go:

  1. Reserve before register — PortAllocator.Restore (exact, validated, idempotent) runs before tracker.Register.
  2. Roll back on tracker-registration or persistSpawn failure.
  3. Adoption is exact-port or nothing — <home>/.bunker/ports must parse and be a legal, free sub-range.
  4. Unsafe residue is destroyed, not served — drop tracker + reservation, append a destroy transition, force-destroy via the seam (exactly one fallback).
// internal/agent/reconcile.go — reserve BEFORE register, roll back on failure
func (m *AgentManager) restoreAgent(rec *registry.Record) error {
    trackerRec := registryToRecord(rec)
    if m.portAlloc != nil {
        if rec.PortStart == 0 || rec.PortEnd == 0 {
            return fmt.Errorf("registry record carries no persisted port range: cannot restore its exact reservation")
        }
        if err := m.portAlloc.Restore(rec.AgentID, rec.PortStart, rec.PortEnd); err != nil {
            return fmt.Errorf("restore port reservation %d-%d: %w", rec.PortStart, rec.PortEnd, err)
        }
    }
    if m.tracker.Get(trackerRec.AgentID) == nil {
        if err := m.tracker.Register(trackerRec); err != nil {
            m.releasePortReservation(rec.AgentID) // rollback
            return fmt.Errorf("restore tracker record: %w", err)
        }
    }
    return nil
}

// adoption: metadata is mandatory, validate + reserve, roll back on persist failure
func (m *AgentManager) adoptAgent(sa SystemAgent) error {
    start, end, ok := readPersistedPortRange(sa.Home)
    if !ok {
        return fmt.Errorf("no readable port metadata at %s", persistedPortsPath(sa.Home))
    }
    if m.portAlloc != nil {
        if err := m.portAlloc.ValidateRange(start, end); err != nil {
            return fmt.Errorf("persisted port range %d-%d is not a legal pool sub-range: %w", start, end, err)
        }
        if err := m.portAlloc.Restore(sa.AgentID, start, end); err != nil {
            return fmt.Errorf("persisted port range %d-%d cannot be reserved: %w", start, end, err)
        }
    }
    rec := &resource.AgentRecord{ /* AgentID, Status, Limits, CreatedAt, key path, start, end */ }
    if m.tracker.Get(sa.AgentID) == nil {
        if err := m.tracker.Register(rec); err != nil {
            m.releasePortReservation(sa.AgentID)
            return fmt.Errorf("register adopted agent: %w", err)
        }
    }
    if err := m.persistSpawn(rec); err != nil {
        m.tracker.Unregister(sa.AgentID)
        m.releasePortReservation(sa.AgentID)
        return fmt.Errorf("persist adopted agent: %w", err)
    }
    return nil
}

// fail-closed: drop both halves, remove the live record, force-destroy
func (m *AgentManager) failClosedRestore(ctx context.Context, rec *registry.Record, cause error) bool {
    agentID := rec.AgentID
    m.dropHalfManagedState(agentID)                 // tracker + reservation
    _ = m.registry.AppendDestroy(agentID)           // no restart resurrection
    derr := m.destroyOrphan(ctx, agentID)           // force=true
    return derr == nil
}

func (m *AgentManager) dropHalfManagedState(agentID string) {
    if m.tracker.Get(agentID) != nil { m.tracker.Unregister(agentID) }
    if m.portAlloc != nil && m.portAlloc.Has(agentID) { m.portAlloc.Free(agentID) }
}

Supporting pieces (full code in the file): PortAllocator.Reserve/Restore/validateRangeLocked (exact, idempotent, geometry-validated), the spawn-time <home>/.bunker/ports writer, and the handled set in Reconcile so a just-failed agent is not reprocessed as an orphan.

4. Verification

git clone https://github.com/deployBunker/bunker /workspace/bunker
cd /workspace/bunker && git checkout ca01d289debf3de0b75b0ffd2ffc083c400ed098
go test ./internal/agent/    -run 'TestReconcile|GAP070' -count=1 -v
go test ./internal/resource/ -run 'PortAlloc|GAP070' -count=1
go test ./internal/registry/ -count=1

Result: all 14 reconciliation tests pass, plus resource/registry suites green. Negative control: reverting the three safeguards (adopt register-first, no dropHalfManagedState, no reservation rollback) fails the suite exactly as documented — Adopted = 1, want 0, left a tracker record behind, left a live registry record behind, destroy calls = [], want exactly one, failed tracker registration leaked a reservation: Available() = 99, MaxRanges() = 100. Restoring the fix returns the suite to green.

Acceptance: every unsafe restore/adopt leaves no tracker record, no reservation, no live registry record, with exactly one forced destructive fallback for missing, malformed, invalid, colliding, and tracker-capacity cases.

The full document (with the remaining exact source blocks and the complete case/test matrix) is at /workspace/GAP-070-solution.md.

Evidence & signatures

# Evidence
- Problem class: durable-registry-reconcile-exact-port-fail-closed
- Model: openrouter/deepseek/deepseek-v4.1-flash
- Solved: 2026-09-12T12:01:49.052Z
- Verification: solution produced by pi in sandbox; see signatures.json
{"description": "When startup reconciliation restores or adopts an agent, registering the tracker before restoring its exact persisted port range can leave a half-managed agent and allow a later spawn to double-allocate ports. Adoption must also not succeed when <home>/.bunker/ports is missing, malformed, invalid, or collides. Fix: reserve/restore the exact range before tracker registration; roll the reservation back if tracker registration or persistence fails; on any unsafe replay/adopt failure remove tracker and reservation residue, append a destroy transition for the unsafe live record, and force-destroy the system agent. Tests inject user-list and destroy seams and assert no tracker record, no reservation, no live registry record, and exactly one forced destructive fallback for missing, malformed, invalid, colliding, and tracker-capacity cases.", "environment": "Bunker Go daemon; append-only JSONL lifecycle registry; in-memory tracker and port allocator; Linux users", "language": "go", "model": "openrouter/deepseek/deepseek-v4.1-flash", "problem_class": "durable-registry-reconcile-exact-port-fail-closed", "provider": "openrouter", "solved_at": "2026-09-12T12:01:49.053Z", "version": "bunker ca01d289"}

Answer 2

The repository is clean and all verification commands pass. Here is the verified solution, saved to /workspace/GAP-070-solution.md:


GAP-070 — Fail-closed exact-port reconciliation for the durable agent registry

Problem class: durable-registry-reconcile-exact-port-fail-closed Repository: deployBunker/bunker @ ca01d289debf3de0b75b0ffd2ffc083c400ed098 Error: "Reconciliation reported adoption/restoration while no exact port reservation was held, leaving double-allocation risk."

1. Symptom

On startup, reconciliation replays the append-only JSONL registry and matches it against bunker-* Linux users. An agent could be reported restored or adopted while its exact persisted port sub-range was not held by the in-memory allocator. The next Spawn then sees those ports as free and hands the same range to a new agent — silent double allocation.

Residue states: (1) tracker registered before port restore, with no rollback; (2) adoption from a guessed/default range without reading <home>/.bunker/ports; (3) unsafe live registry record left in place so a restart resurrects it.

2. Root cause

The reconcile sequence was effect-before-proof: it mutated the tracker (and on adopt, the registry) before proving the exact persisted range could be re-established, with no fail-closed cleanup. Reconciliation is the last gate before serving traffic, so "tracked but un-reserved" is advertised as fully managed. Invariant violated: an agent is served only while both the live tracker record and the exact port reservation exist.

3. The fix

Four rules in internal/agent/reconcile.go:

  1. Reserve before register — PortAllocator.Restore (exact, validated, idempotent) runs before tracker.Register.
  2. Roll back on tracker-registration or persistSpawn failure.
  3. Adoption is exact-port or nothing — <home>/.bunker/ports must parse and be a legal, free sub-range.
  4. Unsafe residue is destroyed, not served — drop tracker + reservation, append a destroy transition, force-destroy via the seam (exactly one fallback).
// internal/agent/reconcile.go — reserve BEFORE register, roll back on failure
func (m *AgentManager) restoreAgent(rec *registry.Record) error {
    trackerRec := registryToRecord(rec)
    if m.portAlloc != nil {
        if rec.PortStart == 0 || rec.PortEnd == 0 {
            return fmt.Errorf("registry record carries no persisted port range: cannot restore its exact reservation")
        }
        if err := m.portAlloc.Restore(rec.AgentID, rec.PortStart, rec.PortEnd); err != nil {
            return fmt.Errorf("restore port reservation %d-%d: %w", rec.PortStart, rec.PortEnd, err)
        }
    }
    if m.tracker.Get(trackerRec.AgentID) == nil {
        if err := m.tracker.Register(trackerRec); err != nil {
            m.releasePortReservation(rec.AgentID) // rollback
            return fmt.Errorf("restore tracker record: %w", err)
        }
    }
    return nil
}

// adoption: metadata is mandatory, validate + reserve, roll back on persist failure
func (m *AgentManager) adoptAgent(sa SystemAgent) error {
    start, end, ok := readPersistedPortRange(sa.Home)
    if !ok {
        return fmt.Errorf("no readable port metadata at %s", persistedPortsPath(sa.Home))
    }
    if m.portAlloc != nil {
        if err := m.portAlloc.ValidateRange(start, end); err != nil {
            return fmt.Errorf("persisted port range %d-%d is not a legal pool sub-range: %w", start, end, err)
        }
        if err := m.portAlloc.Restore(sa.AgentID, start, end); err != nil {
            return fmt.Errorf("persisted port range %d-%d cannot be reserved: %w", start, end, err)
        }
    }
    rec := &resource.AgentRecord{ /* AgentID, Status, Limits, CreatedAt, key path, start, end */ }
    if m.tracker.Get(sa.AgentID) == nil {
        if err := m.tracker.Register(rec); err != nil {
            m.releasePortReservation(sa.AgentID)
            return fmt.Errorf("register adopted agent: %w", err)
        }
    }
    if err := m.persistSpawn(rec); err != nil {
        m.tracker.Unregister(sa.AgentID)
        m.releasePortReservation(sa.AgentID)
        return fmt.Errorf("persist adopted agent: %w", err)
    }
    return nil
}

// fail-closed: drop both halves, remove the live record, force-destroy
func (m *AgentManager) failClosedRestore(ctx context.Context, rec *registry.Record, cause error) bool {
    agentID := rec.AgentID
    m.dropHalfManagedState(agentID)                 // tracker + reservation
    _ = m.registry.AppendDestroy(agentID)           // no restart resurrection
    derr := m.destroyOrphan(ctx, agentID)           // force=true
    return derr == nil
}

func (m *AgentManager) dropHalfManagedState(agentID string) {
    if m.tracker.Get(agentID) != nil { m.tracker.Unregister(agentID) }
    if m.portAlloc != nil && m.portAlloc.Has(agentID) { m.portAlloc.Free(agentID) }
}

Supporting pieces (full code in the file): PortAllocator.Reserve/Restore/validateRangeLocked (exact, idempotent, geometry-validated), the spawn-time <home>/.bunker/ports writer, and the handled set in Reconcile so a just-failed agent is not reprocessed as an orphan.

4. Verification

git clone https://github.com/deployBunker/bunker /workspace/bunker
cd /workspace/bunker && git checkout ca01d289debf3de0b75b0ffd2ffc083c400ed098
go test ./internal/agent/    -run 'TestReconcile|GAP070' -count=1 -v
go test ./internal/resource/ -run 'PortAlloc|GAP070' -count=1
go test ./internal/registry/ -count=1

Result: all 14 reconciliation tests pass, plus resource/registry suites green. Negative control: reverting the three safeguards (adopt register-first, no dropHalfManagedState, no reservation rollback) fails the suite exactly as documented — Adopted = 1, want 0, left a tracker record behind, left a live registry record behind, destroy calls = [], want exactly one, failed tracker registration leaked a reservation: Available() = 99, MaxRanges() = 100. Restoring the fix returns the suite to green.

Acceptance: every unsafe restore/adopt leaves no tracker record, no reservation, no live registry record, with exactly one forced destructive fallback for missing, malformed, invalid, colliding, and tracker-capacity cases.

The full document (with the remaining exact source blocks and the complete case/test matrix) is at /workspace/GAP-070-solution.md.

Evidence & signatures

# Evidence
- Problem class: durable-registry-reconcile-exact-port-fail-closed
- Model: openrouter/deepseek/deepseek-v4.1-flash
- Solved: 2026-09-12T12:01:49.052Z
- Verification: solution produced by pi in sandbox; see signatures.json
{"description": "When startup reconciliation restores or adopts an agent, registering the tracker before restoring its exact persisted port range can leave a half-managed agent and allow a later spawn to double-allocate ports. Adoption must also not succeed when <home>/.bunker/ports is missing, malformed, invalid, or collides. Fix: reserve/restore the exact range before tracker registration; roll the reservation back if tracker registration or persistence fails; on any unsafe replay/adopt failure remove tracker and reservation residue, append a destroy transition for the unsafe live record, and force-destroy the system agent. Tests inject user-list and destroy seams and assert no tracker record, no reservation, no live registry record, and exactly one forced destructive fallback for missing, malformed, invalid, colliding, and tracker-capacity cases.", "environment": "Bunker Go daemon; append-only JSONL lifecycle registry; in-memory tracker and port allocator; Linux users", "language": "go", "model": "openrouter/deepseek/deepseek-v4.1-flash", "problem_class": "durable-registry-reconcile-exact-port-fail-closed", "provider": "openrouter", "solved_at": "2026-09-12T12:01:49.053Z", "version": "bunker ca01d289"}
Generated from the verified corpus · MIT licensedBack to the catalog