Skip to content

refactor(macos): lock and unify PortGuardian tunnel record persistence so concurrent app instances cannot lose orphan records#100601

Merged
steipete merged 3 commits into
mainfrom
claude/portguardian-cleanup
Jul 6, 2026
Merged

refactor(macos): lock and unify PortGuardian tunnel record persistence so concurrent app instances cannot lose orphan records#100601
steipete merged 3 commits into
mainfrom
claude/portguardian-cleanup

Conversation

@steipete

@steipete steipete commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

Related: #100477

What Problem This Solves

Follow-up hardening and cleanup of the PortGuardian orphan-tunnel work from #100489. The record store had a remaining bug class: record()/removeRecord() rewrote port-guard.json from one app instance's in-memory view, so concurrent instances (prod + dev on one machine — the exact scenario from #100477) silently dropped each other's records. A lost record is an unreapable orphan after a crash, which undermines the reaper the file exists to feed. There were also two overlapping kill helpers (both spawning /bin/kill subprocesses, one without exit confirmation), a fire-and-forget record write that could leave a just-spawned tunnel untracked, and assorted vestigial structure.

Why This Change Was Made

The record store now has one canonical write path: every mutation is a read-merge-write of the shared file under an flock on a sidecar lock file (the kernel releases it if the holder dies; the sidecar is needed because atomic writes swap the json inode). Ownership is explicit — ownRecords holds only pids this process spawned; disk entries from other instances are reap-plan input, never adopted, which kills the resurrection hazard where one instance re-persisted records a sibling had deliberately removed. Removals are content-guarded (reap, removeRecord): a record is deleted only if the file still holds the exact record that was classified, so a sibling's fresh record on a recycled pid always survives. The two kill helpers collapsed into one Darwin.kill-based TERM→KILL helper with confirmed-exit semantics, which also upgrades the local-mode sweep kill (previously fire-and-forget signal delivery). RemotePortTunnel now awaits the record write before returning (no untracked-orphan window), shares one teardown path between deinit and terminate(), and drops a test-only overload. Vestigial single-element port loops in sweep/diagnose were flattened and the duplicate DEBUG extensions merged.

User Impact

Multi-instance dev machines get reliable orphan reaping: tunnel records can no longer be lost to cross-instance clobbering, so every crashed instance's tunnel stays reapable. Local-mode port takeover now waits for the squatter to actually exit before the gateway starts. No user-visible surface changes.

Evidence

  • All 8 focused port-guardian tests pass, including the real-process integration test; the reap plan test now also pins the new drop semantics (dead-pid records reported as drops, shadowed stale disk records superseded rather than dropped).
  • Full local suite swift test --package-path apps/macos --parallel: 639 tests in 113 suites, all passed.
  • swiftformat --lint clean; Codex autoreview clean on the first pass ("patch is correct", no actionable findings).
  • Diff shape: +156/−130 lines with the net growth being the file lock and single write path that replace the clobber-prone scattered writes.

@openclaw-barnacle openclaw-barnacle Bot added app: macos App: macos size: M maintainer Maintainer-authored PR labels Jul 6, 2026

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: b22ed2a146

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

return
}
defer { close(fd) }
_ = flock(fd, LOCK_EX)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Require the lock before read-merge-write

When another app instance is using port-guard.json, an interrupted or failed flock returns failure, but this code discards the result and still runs the read-merge-write body unlocked. In that failure path the new persistence path can again clobber a sibling instance's tunnel records—the exact concurrent prod+dev scenario this change is meant to protect—so the lock acquisition should be checked/retried or the mutation should fail closed instead of proceeding.

Useful? React with 👍 / 👎.

@clawsweeper

clawsweeper Bot commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

Codex review: needs real behavior proof before merge. Reviewed July 6, 2026, 2:08 AM ET / 06:08 UTC.

Summary
The PR refactors macOS PortGuardian and RemotePortTunnel to use per-process tunnel ownership, shared locked persistence, unified termination, updated tests, and refreshed native string inventory.

PR surface: Other +26. Total +26 across 4 files.

Reproducibility: yes. source-level. Current main rewrites the shared record file from one actor-local view, and the PR head still has source-visible record-loss paths through failed locking and no-op reaps; I did not run a live macOS multi-instance reproduction.

Review metrics: 1 noteworthy metric.

  • Shared tunnel record store: 1 JSON process-record store changed, 1 sidecar lock file added. The PR changes a macOS process-record store shared across app instances, so lock and stale-disk behavior matter before merge.

Root-cause cluster
Relationship: same_root_cause
Canonical: #100489
Summary: The merged orphan-tunnel reaper is the canonical completed fix for the original leak, while this PR is a follow-up hardening of the same PortGuardian record system and the broad remote Gateway UX issue only partially overlaps.

Members:

Proposal only: this assessment does not dispatch repair, suppress jobs, mutate sibling items, close, or merge anything.

Merge readiness
Overall: 🧂 unranked krab
Proof: 🧂 unranked krab
Patch quality: 🧂 unranked krab
Result: blocked until real behavior proof from a real setup is added.

Overall follows the weaker of proof and patch quality, so missing proof can cap an otherwise strong patch.

Rank-up moves:

  • [P1] Fix the lock-failure and no-op reap persistence defects on the PR head.
  • [P1] Add redacted real multi-instance macOS proof, such as terminal output or logs showing two app instances preserving each other's tunnel records.

Proof guidance:

  • [P1] Needs real behavior proof before merge: The PR body reports focused Swift tests, a real-process test, full Swift suite output, lint, and CI, but it does not include after-fix real multi-instance macOS app proof; add redacted terminal output, logs, or a recording showing sibling records survive, with private details redacted. After adding proof, update the PR body; ClawSweeper should re-review automatically. If it does not, the PR author or someone with repository write access can comment @clawsweeper re-review.

Risk before merge

  • [P1] A failed lock-file open or failed/interrupted flock can still run the shared port-guard.json mutation without serialization and drop a sibling app instance's tunnel record.
  • [P1] If port-guard.json is missing or stale while this process owns a live tunnel, the no-op reap path can skip restoring that record, leaving a later crash unreapable.
  • [P1] The PR changes macOS tunnel persistence and process cleanup, but the body only reports tests/CI and lacks redacted real multi-instance macOS app proof.

Maintainer options:

  1. Fix both persistence gaps before merge (recommended)
    Require lock acquisition before the mutation body and restore kept owned records when the disk file is missing or stale, then re-review the exact head with real proof.
  2. Accept residual orphan risk
    Maintainers could intentionally accept the rare residual record-loss paths, but that would leave the central hardening claim incomplete.
  3. Pause until live proof exists
    Pause this PR until a redacted multi-instance macOS run shows sibling records survive concurrent mutation and stale-disk recovery.

Next step before merge

  • [P1] The PR has a protected maintainer label and a contributor proof gate; the author should fix the two persistence defects and add redacted real multi-instance macOS proof before maintainer review.

Security
Cleared: No concrete security or supply-chain issue was found; the diff changes local macOS process cleanup and record persistence without new dependencies, secrets, workflow permissions, or package-resolution changes.

Review findings

  • [P2] Require the lock before mutating records — apps/macos/Sources/OpenClaw/PortGuardian.swift:592
  • [P2] Persist kept own records during no-op reaps — apps/macos/Sources/OpenClaw/PortGuardian.swift:146
Review details

Best possible solution:

Keep the single shared record-store direction, but require a successful lock, restore kept owned records when disk is missing or stale, and add redacted multi-instance macOS proof before merge.

Do we have a high-confidence way to reproduce the issue?

Yes, source-level. Current main rewrites the shared record file from one actor-local view, and the PR head still has source-visible record-loss paths through failed locking and no-op reaps; I did not run a live macOS multi-instance reproduction.

Is this the best way to solve the issue?

No. A single locked read-merge-write path with explicit own-record ownership is the right fix shape, but this implementation must enforce the lock and restore kept owned records before it reliably solves the issue.

Full review comments:

  • [P2] Require the lock before mutating records — apps/macos/Sources/OpenClaw/PortGuardian.swift:592
    withRecordFileLock falls back to body() when the lock file cannot be opened and ignores the result of flock(fd, LOCK_EX). If either step fails, the shared port-guard.json read-merge-write still runs unlocked and can clobber a sibling instance's tunnel record, which is the failure this PR is trying to remove.
    Confidence: 0.9
  • [P2] Persist kept own records during no-op reaps — apps/macos/Sources/OpenClaw/PortGuardian.swift:146
    When disk is missing or stale but this instance's live tunnel is still in plan.keep, removals is empty and this early return skips rewriting port-guard.json. A later crash then leaves that live tunnel unrecorded and unreapable, so the reap path should also restore kept owned records when disk no longer matches them.
    Confidence: 0.88

Overall correctness: patch is incorrect
Overall confidence: 0.91

AGENTS.md: found and applied where relevant.

Codex review notes: model internal, reasoning high; reviewed against 241c761d0036.

Label changes

Label justifications:

  • P1: The PR targets a macOS remote-mode workflow where lost tunnel records can leave orphaned SSH processes and unavailable preferred gateway ports for real users.
  • merge-risk: 🚨 availability: Merging the current patch could still lose tunnel records through lock failure or stale-disk no-op reap paths, leaving SSH tunnel processes unreapable.
  • rating: 🧂 unranked krab: Overall readiness is 🧂 unranked krab; proof is 🧂 unranked krab and patch quality is 🧂 unranked krab.
  • status: 📣 needs proof: The PR needs real behavior proof before ClawSweeper can clear the contributor ask. Needs real behavior proof before merge: The PR body reports focused Swift tests, a real-process test, full Swift suite output, lint, and CI, but it does not include after-fix real multi-instance macOS app proof; add redacted terminal output, logs, or a recording showing sibling records survive, with private details redacted. After adding proof, update the PR body; ClawSweeper should re-review automatically. If it does not, the PR author or someone with repository write access can comment @clawsweeper re-review.
Evidence reviewed

PR surface:

Other +26. Total +26 across 4 files.

View PR surface stats
Area Files Added Removed Net
Source 0 0 0 0
Tests 0 0 0 0
Docs 0 0 0 0
Config 0 0 0 0
Generated 0 0 0 0
Other 4 159 133 +26
Total 4 159 133 +26

What I checked:

  • Repository policy applied: Root AGENTS.md was read fully; its deep PR review, protected-label, real-behavior-proof, process/security review, and merge-risk guidance affected this review. No apps/macos scoped AGENTS.md or matching maintainer notes were present. (AGENTS.md:16, 241c761d0036)
  • Current main still has the shared-record clobber class: Current main keeps one actor-local records array and saves that array directly to port-guard.json from record/removeRecord, so one app instance can overwrite another instance's disk records. (apps/macos/Sources/OpenClaw/PortGuardian.swift:87, 241c761d0036)
  • PR head still runs mutations without a proven lock: At the current PR head, withRecordFileLock runs body() if the lock file cannot be opened and ignores the return value of flock(fd, LOCK_EX), so persistence can still run unlocked on lock failure. (apps/macos/Sources/OpenClaw/PortGuardian.swift:592, 2c063c0e3f30)
  • PR head skips stale-disk restoration when nothing is removed: reapOrphanedTunnels returns when removals is empty, even if plan.keep contains this process's live owned tunnel record and the disk file is missing or stale. (apps/macos/Sources/OpenClaw/PortGuardian.swift:146, 2c063c0e3f30)
  • Runtime entry and caller path checked: RemoteTunnelManager.ensureControlTunnel creates the SSH tunnel, and the PR head now awaits PortGuardian.record before returning from RemotePortTunnel.create, which is the right entry point for the persistence hardening. (apps/macos/Sources/OpenClaw/RemoteTunnelManager.swift:68, 241c761d0036)
  • Adjacent tests do not cover the two remaining persistence gaps: The updated test checks planTunnelReap classification and drop reporting, but it does not exercise lock acquisition failure or restoring kept owned records when disk is missing or stale. (apps/macos/Tests/OpenClawIPCTests/LowCoverageHelperTests.swift:252, 2c063c0e3f30)

Likely related people:

  • steipete: Authored the current PR, authored/merged the related orphan-tunnel reaper PR, and recently touched adjacent macOS RemotePortTunnel and cache identity behavior. (role: recent area contributor and current follow-up author; confidence: high; commits: 4a5bdc6ae3a9, d5371395218c, 18db5f7c6939; files: apps/macos/Sources/OpenClaw/PortGuardian.swift, apps/macos/Sources/OpenClaw/RemotePortTunnel.swift, apps/macos/Sources/OpenClaw/RemoteTunnelManager.swift)
  • teslamint: Authored the merged PortGuardian remote-mode change that prevents killing Docker/direct/SSH listeners, an invariant this PR must preserve while changing sweep and process termination behavior. (role: adjacent remote-mode policy contributor; confidence: medium; commits: 2bfe18851056; files: apps/macos/Sources/OpenClaw/PortGuardian.swift, apps/macos/Tests/OpenClawIPCTests/LowCoverageHelperTests.swift)
  • drobison00: Authored merged SSH host-key hardening that touched RemotePortTunnel behavior and is relevant adjacent history for the macOS SSH tunnel path. (role: adjacent SSH tunnel hardening contributor; confidence: medium; commits: 114b87caf293; files: apps/macos/Sources/OpenClaw/RemotePortTunnel.swift)
  • tyler6204: CONTRIBUTING.md lists Tyler Yust for the macOS app area, making this useful routing context for protected maintainer handling. (role: listed macOS app area owner; confidence: medium; files: CONTRIBUTING.md, apps/macos/Sources/OpenClaw/PortGuardian.swift)
What the crustacean ranks mean
  • 🦀 challenger crab: rare, exceptional readiness with strong proof, clean implementation, and convincing validation.
  • 🦞 diamond lobster: very strong readiness with only minor maintainer review expected.
  • 🐚 platinum hermit: good normal PR, likely mergeable with ordinary maintainer review.
  • 🦐 gold shrimp: useful signal, but proof or patch confidence is still limited.
  • 🦪 silver shellfish: thin signal; proof, validation, or implementation needs work.
  • 🧂 unranked krab: not merge-ready because proof is missing/unusable or there are serious correctness or safety concerns.
  • 🌊 off-meta tidepool: rating does not apply to this item.

Shiny media proof means a screenshot, video, or linked artifact directly shows the changed behavior. Runtime, network, CSP, and security claims still need visible diagnostics.

How this review workflow works
  • ClawSweeper keeps one durable marker-backed review comment per issue or PR.
  • Re-runs edit this comment so the latest verdict, findings, and automation markers stay together instead of adding duplicate bot comments.
  • A fresh review can be triggered by eligible @clawsweeper re-review comments, exact-item GitHub events, scheduled/background review runs, or manual workflow dispatch.
  • PR/issue authors and users with repository write access can comment @clawsweeper re-review or @clawsweeper re-run on an open PR or issue to request a fresh review only.
  • Maintainers can also comment @clawsweeper review to request a fresh review only.
  • Fresh-review commands do not start repair, autofix, rebase, CI repair, or automerge.
  • Maintainer-only repair and merge flows require explicit commands such as @clawsweeper autofix, @clawsweeper automerge, @clawsweeper fix ci, or @clawsweeper address review.
  • Maintainers can comment @clawsweeper explain to ask for more context, or @clawsweeper stop to stop active automation.
Review history (2 earlier review cycles)
  • reviewed 2026-07-06T04:55:44.665Z sha d8a0044 :: needs real behavior proof before merge. :: [P2] Require the lock before read-merge-write
  • reviewed 2026-07-06T05:20:28.772Z sha 278fa5d :: needs real behavior proof before merge. :: [P2] Require the lock before mutating records | [P2] Persist kept own records during no-op reaps

@clawsweeper clawsweeper Bot added rating: 🧂 unranked krab Not merge-ready due to missing proof or serious correctness/safety concerns. status: 📣 needs proof The PR needs real behavior proof before ClawSweeper can clear the contributor ask. P1 High-priority user-facing bug, regression, or broken workflow. merge-risk: 🚨 availability 🚨 May cause crashes, hangs, restart loops, stalls, or process outages. labels Jul 6, 2026
@steipete
steipete force-pushed the claude/portguardian-cleanup branch from d8a0044 to 278fa5d Compare July 6, 2026 05:05

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 278fa5d79b

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

if kept != self.records.sorted(by: byTime) || kept != disk.sorted(by: byTime) {
self.records = kept
self.save()
guard !removals.isEmpty else { return }

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Persist kept own records during no-op reaps

When port-guard.json is missing or has been clobbered while this instance's SSH tunnel is still alive, plan.keep still contains the ownRecords entry but removals is empty, so this early return leaves the disk file without the live tunnel record. The previous sweep path rewrote kept in-memory records whenever disk differed; without that repair, a later app crash leaves the still-running tunnel unrecorded and therefore unreapable by the next launch.

Useful? React with 👍 / 👎.

@steipete
steipete force-pushed the claude/portguardian-cleanup branch from 278fa5d to 2c063c0 Compare July 6, 2026 05:51
@openclaw-barnacle openclaw-barnacle Bot added docs Improvements or additions to documentation app: android App: android app: web-ui App: web-ui gateway Gateway runtime cli CLI command changes scripts Repository scripts commands Command implementations agents Agent runtime and tooling extensions: qa-lab size: XL and removed size: M labels Jul 6, 2026
@steipete
steipete merged commit 1256aa0 into main Jul 6, 2026
172 of 180 checks passed
@steipete
steipete deleted the claude/portguardian-cleanup branch July 6, 2026 06:43
vincentkoc added a commit to zhangqueping/openclaw that referenced this pull request Jul 6, 2026
* origin/main: (1287 commits)
  fix(android): block loopback canvas navigation (openclaw#99874)
  fix(hooks): suppress unhandled stdout/stderr stream errors in gmail watcher (openclaw#100519)
  fix(memory-core): guard qmd mcporter JSON.parse against non-JSON stdout (openclaw#98381)
  fix(build): fall back to tsx for build TypeScript scripts (openclaw#91262)
  feat(skills): suggest saving detected reusable workflows by default (openclaw#95477) (openclaw#100692)
  docs(changelog): remove generated release-note entries
  feat(telegram): offer BotFather web app flow in setup help and docs (openclaw#100540)
  fix(ios): unify Talk and Settings row typography on one branded detail row (openclaw#100515)
  feat(gateway): add persisted crash-loop breaker and fatal-config exit contract
  refactor(macos): lock and unify PortGuardian tunnel record persistence so concurrent app instances cannot lose orphan records (openclaw#100601)
  fix: stop reconnecting on protocol mismatch (openclaw#98414)
  fix(maint): reuse recent hosted gates after rebase (openclaw#100663)
  fix(ui): reopen web terminals without stale content (openclaw#100665)
  fix(browser): diagnose empty WSL2 Chrome replies (openclaw#100590)
  fix(ios): chat snaps back to bottom when scrolling to top via status-bar tap (openclaw#100502)
  Treat already-compacted CLI compaction as no-op (openclaw#99136)
  docs(changelog): remove direct main fix entry
  fix(feishu): strip internal tool-trace banners from outbound text (openclaw#98705)
  fix(message): thread --limit through to CLI formatter and surface provider pagination hints (openclaw#99089)
  fix(voyage): close response body stream when batch output JSONL parsing throws (openclaw#98840)
  ...

# Conflicts:
#	extensions/memory-wiki/package.json
github-actions Bot pushed a commit to Desicool/openclaw that referenced this pull request Jul 6, 2026
…e so concurrent app instances cannot lose orphan records (openclaw#100601)

* refactor(macos): single locked write path and per-instance ownership for PortGuardian tunnel records

* chore(i18n): sync native app string inventory for PortGuardian refactor

* chore(i18n): sync native app string inventory for PortGuardian refactor
giodl73-repo pushed a commit to giodl73-repo/openclaw that referenced this pull request Jul 8, 2026
…e so concurrent app instances cannot lose orphan records (openclaw#100601)

* refactor(macos): single locked write path and per-instance ownership for PortGuardian tunnel records

* chore(i18n): sync native app string inventory for PortGuardian refactor

* chore(i18n): sync native app string inventory for PortGuardian refactor
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

agents Agent runtime and tooling app: android App: android app: macos App: macos app: web-ui App: web-ui cli CLI command changes commands Command implementations docs Improvements or additions to documentation extensions: qa-lab gateway Gateway runtime maintainer Maintainer-authored PR merge-risk: 🚨 availability 🚨 May cause crashes, hangs, restart loops, stalls, or process outages. P1 High-priority user-facing bug, regression, or broken workflow. rating: 🧂 unranked krab Not merge-ready due to missing proof or serious correctness/safety concerns. scripts Repository scripts size: XL status: 📣 needs proof The PR needs real behavior proof before ClawSweeper can clear the contributor ask.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant