Skip to content

fix(core): report self-hosted project binding support - #165

Merged
ian-pascoe merged 2 commits into
mainfrom
fix/self-hosted-doctor-project-binding
Jun 26, 2026
Merged

fix(core): report self-hosted project binding support#165
ian-pascoe merged 2 commits into
mainfrom
fix/self-hosted-doctor-project-binding

Conversation

@ian-pascoe

@ian-pascoeian-pascoe commented Jun 26, 2026

Copy link
Copy Markdown
Contributor

Summary

caplets doctor now reports authenticated self-hosted Project Binding support from the actual session route instead of assuming every self-hosted remote is unsupported. Supported remotes now show sessionSupport: "supported" and recover with caplets attach --once; older remotes without the route still report unsupported with an upgrade-oriented diagnostic.

The probe authenticates against the Project Binding session endpoint with a non-creating diagnostic request, so current servers prove support without leaving a binding lease behind. A patch changeset is included for the published CLI/runtime packages.

Validation

  • pnpm changeset status --since=origin/main
  • pnpm verify
  • node packages/cli/dist/index.js doctor --json against the live self-hosted daemon reported sessionSupport: "supported", lastUpgradeError: null, and recoveryCommand: "caplets attach --once"

Compound Engineering
GPT-5

Summary by CodeRabbit

  • Bug Fixes

    • Improved caplets doctor Project Binding session diagnostics for authenticated self-hosted remotes.
    • Session route support is now detected more reliably, with updated recoveryCommand guidance when support is unavailable, missing, or errors occur (including probe timeouts).
    • Enhanced error reporting for failed diagnostic checks and cleanup attempts.
  • Tests

    • Expanded caplets doctor test coverage for session probing outcomes, request handling, recovery messaging, and timeout/error scenarios.

@coderabbitai

coderabbitaiBot commented Jun 26, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Caution

Review failed

Pull request was closed or merged during review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 66b890cd-1fcd-4850-9647-082aa55c5685

📥 Commits

Reviewing files that changed from the base of the PR and between e9db027 and 2e2cc8c.

📒 Files selected for processing (2)
  • packages/core/src/cli/doctor.ts
  • packages/core/test/doctor-cli.test.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/core/src/cli/doctor.ts

📝 Walkthrough

Walkthrough

caplets doctor now probes authenticated self-hosted Project Binding sessions, records the result in the doctor report, updates recovery guidance, passes through a fetch override, and refreshes tests plus patch release notes.

Changes

Project Binding session diagnostics

Layer / File(s)Summary
Fetch plumbing and report fields
packages/core/src/cli.ts, packages/core/src/cli/doctor.ts
caplets doctor passes io.fetch into DoctorOptions, computes session support through the new helper, and fills projectBinding.sessionSupport, lastUpgradeError, and recoveryCommand from that result.
Session support probe and recovery
packages/core/src/cli/doctor.ts
resolveProjectBindingSessionSupport posts a diagnostic probe to the sessions endpoint, maps the response to supported, unsupported, or unknown, clears created diagnostic sessions on 201, and feeds the result into projectBindingRecovery.
Doctor tests and changeset
packages/core/test/doctor-cli.test.ts, .changeset/gentle-rivers-check.md
caplets doctor tests mock the sessions probe, check the new request shape and recovery output, update session-support JSON expectations, and record the patch release in the changeset.

Sequence Diagram(s)

sequenceDiagram
participant doctorJsonReport
participant resolveProjectBindingSessionSupport
participant remoteFetch
doctorJsonReport->>resolveProjectBindingSessionSupport: compute session support
resolveProjectBindingSessionSupport->>remoteFetch: POST diagnosticProbe
remoteFetch-->>resolveProjectBindingSessionSupport: status / bindingId / sessionId
resolveProjectBindingSessionSupport->>remoteFetch: DELETE diagnostic session (201)
resolveProjectBindingSessionSupport-->>doctorJsonReport: sessionSupport / lastError
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Poem

I hopped through doctor logs by moonlight glow,
Sniffed one tiny route and watched it show.
POST, then a DELETE—clean as clover dew,
Now supported paths can hop right through. 🐰
My whiskers twitch; the diagnosis sings.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check nameStatusExplanationResolution
Docstring Coverage⚠️ WarningDocstring coverage is 0.00% which is insufficient. The required threshold is 80.00%.Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check nameStatusExplanation
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Title check✅ PassedThe title clearly summarizes the main change: reporting self-hosted project binding support.
Linked Issues check✅ PassedCheck skipped because no linked issues were found for this pull request.
Out of Scope Changes check✅ PassedCheck skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/self-hosted-doctor-project-binding

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@greptile-apps

greptile-appsBot commented Jun 26, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR replaces the blanket "unsupported" assumption for self-hosted Project Binding sessions with an authenticated HTTP probe against the actual session endpoint, so caplets doctor now accurately reflects whether the remote supports the feature.

  • Probe logic (doctor.ts): a new resolveProjectBindingSessionSupport function POSTs a diagnosticProbe request to /v1/attach/project-bindings/sessions, interprets the status code to classify support as "supported", "unsupported", or "unknown", and deletes any accidentally-created session before returning.
  • Recovery messages updated: authenticated self-hosted remotes that report "supported" now recommend caplets attach --once; those without the route get an upgrade hint instead of the old blanket "not implemented" message.
  • Tests added for all key response shapes: 201 with cleanup, 400 as supported, non-201 2xx, malformed body, cleanup failure, timeout, and missing auth.

Confidence Score: 5/5

Safe to merge — the probe is read-only in the happy path (creates and immediately deletes a diagnostic session), all error paths return gracefully without throwing, and the change is fully gated behind authenticated self-hosted remotes in a diagnostic-only code path.

The probe, cleanup, URL construction, and recovery-message logic are all well-tested across six new cases. The existing selfHostedDoctorInput → resolveCapletsRemote → projectBindingWebSocketUrlForBase chain guarantees the WebSocket URL always ends in /connect, so the pathname regex replacement is safe. No production data paths are touched.

No files require special attention. The two findings in doctor.ts are cosmetic quality improvements to fetchWithTimeout that do not affect correctness for the current callers.

Important Files Changed

FilenameOverview
packages/core/src/cli/doctor.tsCore logic change: adds ~190 lines implementing the Project Binding session probe (POST + DELETE cleanup), URL builder, timeout wrapper, and updated recovery message logic. Well-structured with explicit status classification and individual error paths surfaced via lastError.
packages/core/src/cli.tsOne-liner change: threads io.fetch through to doctorOptions so tests can inject a mock fetch. No production risk.
packages/core/test/doctor-cli.test.tsAdds six new test cases covering the probe (201, 400, 204, malformed body, cleanup failure, timeout) and renames the unauthenticated-remote test. The helper saveSelfHostedProfile DRYs up repeated profile setup.
.changeset/gentle-rivers-check.mdPatch changeset for @caplets/core and caplets CLI, correctly scoped to the diagnostic bug fix.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
participant CLI as caplets doctor
participant Probe as resolveProjectBindingSessionSupport
participant Remote as Self-Hosted Remote
CLI->>Probe: invoke (options, env, remoteLogin)
alt not self-hosted or not authenticated
Probe-->>CLI: "{ value: "unknown", lastError: null }"
else authenticated self-hosted
Probe->>Remote: POST /v1/attach/project-bindings/sessions (5s timeout)
alt 201 Created
Remote-->>Probe: "{ binding.bindingId, sessionId }"
alt body valid
Probe->>Remote: "DELETE /v1/attach/project-bindings/{bindingId}/session"
alt DELETE ok
Remote-->>Probe: 200
Probe-->>CLI: "{ value: "supported", lastError: null }"
else DELETE failed
Remote-->>Probe: 5xx
Probe-->>CLI: "{ value: "unknown", lastError: "cleanup returned N" }"
end
else body malformed
Probe-->>CLI: "{ value: "unknown", lastError: "missing bindingId or sessionId" }"
end
else 400 Bad Request
Remote-->>Probe: 400
Probe-->>CLI: "{ value: "supported", lastError: null }"
else 2xx other
Remote-->>Probe: 200/202/204
Probe-->>CLI: "{ value: "supported", lastError: null }"
else 404/405/501
Remote-->>Probe: 404/405/501
Probe-->>CLI: "{ value: "unsupported", lastError: "endpoint returned N" }"
else 401/403
Remote-->>Probe: 401/403
Probe-->>CLI: "{ value: "unknown", lastError: "not authorized" }"
else timeout/network error
Probe-->>CLI: "{ value: "unknown", lastError: "timed out / error" }"
end
end
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
participant CLI as caplets doctor
participant Probe as resolveProjectBindingSessionSupport
participant Remote as Self-Hosted Remote
CLI->>Probe: invoke (options, env, remoteLogin)
alt not self-hosted or not authenticated
Probe-->>CLI: "{ value: "unknown", lastError: null }"
else authenticated self-hosted
Probe->>Remote: POST /v1/attach/project-bindings/sessions (5s timeout)
alt 201 Created
Remote-->>Probe: "{ binding.bindingId, sessionId }"
alt body valid
Probe->>Remote: "DELETE /v1/attach/project-bindings/{bindingId}/session"
alt DELETE ok
Remote-->>Probe: 200
Probe-->>CLI: "{ value: "supported", lastError: null }"
else DELETE failed
Remote-->>Probe: 5xx
Probe-->>CLI: "{ value: "unknown", lastError: "cleanup returned N" }"
end
else body malformed
Probe-->>CLI: "{ value: "unknown", lastError: "missing bindingId or sessionId" }"
end
else 400 Bad Request
Remote-->>Probe: 400
Probe-->>CLI: "{ value: "supported", lastError: null }"
else 2xx other
Remote-->>Probe: 200/202/204
Probe-->>CLI: "{ value: "supported", lastError: null }"
else 404/405/501
Remote-->>Probe: 404/405/501
Probe-->>CLI: "{ value: "unsupported", lastError: "endpoint returned N" }"
else 401/403
Remote-->>Probe: 401/403
Probe-->>CLI: "{ value: "unknown", lastError: "not authorized" }"
else timeout/network error
Probe-->>CLI: "{ value: "unknown", lastError: "timed out / error" }"
end
end
Loading

Reviews (2): Last reviewed commit: "fix(core): harden doctor project binding..." | Re-trigger Greptile

Comment threadpackages/core/src/cli/doctor.ts
Comment threadpackages/core/src/cli/doctor.ts

@coderabbitaicoderabbitaiBot 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.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@packages/core/src/cli/doctor.ts`:
- Around line 296-299: The Project Binding probe in doctor currently treats a
successful POST as fully supported even if the cleanup DELETE fails, which can
hide an orphaned lease. In the diagnostic-session flow around
endProjectBindingDiagnosticSession and the supported return path, capture and
surface any cleanup error instead of ignoring it, and if cleanup fails make the
probe report the failure via lastError rather than always returning
sessionSupport as supported; apply the same handling to the other
diagnostic-session cleanup path as well.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: a6e4aadb-fcd8-4ebd-b4f8-5cd8611c8f5d

📥 Commits

Reviewing files that changed from the base of the PR and between 1ccdaac and e9db027.

📒 Files selected for processing (4)
  • .changeset/gentle-rivers-check.md
  • packages/core/src/cli.ts
  • packages/core/src/cli/doctor.ts
  • packages/core/test/doctor-cli.test.ts

Comment threadpackages/core/src/cli/doctor.ts Outdated

@chatgpt-codex-connectorchatgpt-codex-connectorBot left a comment

Copy link
Copy Markdown

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:e9db027312

ℹ️ 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".

Comment threadpackages/core/src/cli/doctor.ts
Comment threadpackages/core/src/cli/doctor.ts Outdated
@ian-pascoe
ian-pascoe merged commit 75f4b64 into mainJun 26, 2026
5 of 6 checks passed
@ian-pascoe
ian-pascoe deleted the fix/self-hosted-doctor-project-binding branch June 26, 2026 09:12

@chatgpt-codex-connectorchatgpt-codex-connectorBot left a comment

Copy link
Copy Markdown

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:2e2cc8c390

ℹ️ 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".

Comment on lines +336 to +340
if (response.status === 401 || response.status === 403) {
return {
value: "unknown",
lastError: `Project Binding session probe was not authorized (${response.status}).`,
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Route unauthorized probes to remote login

When a saved self-hosted profile still looks authenticated locally but the server has revoked or rejected its bearer token, the protected Project Binding route returns 401/403; this branch records only unknown, and projectBindingRecovery then emits caplets doctor, so the user is told to rerun the same failing probe instead of re-establishing Remote Login. Treat this unauthorized probe result as a credentials problem (for example, recover with caplets remote login <url>) so revoked/stale credentials do not leave doctor in a loop.

Useful? React with 👍 / 👎.

}
return { value: "supported", lastError: null };
}
if (response.status === 400) return { value: "supported", lastError: null };

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Gate support on the attach path that starts binding

For a non-loopback self-hosted remote, this treats the current server's 400 from /project-bindings/sessions as supported and then recommends caplets attach --once, but the attach/native path only creates a self-hosted Project Binding manager when isLoopbackRemote(remoteOptions) (packages/core/src/native/service.ts:1601-1634) and only forwards attach-session metadata for loopback remotes (packages/core/src/native/service.ts:798-805). In that environment doctor now advertises a recovery path that the client never starts, so remote project-bound Caplets remain hidden; gate this supported result on the same capability or wire the non-loopback path before advertising it.

Useful? React with 👍 / 👎.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@ian-pascoe