Skip to content

feat: add relayfile control-plane client#344

Merged
khaliqgant merged 5 commits into
mainfrom
codex/integration-native-resource-globs
Jun 29, 2026
Merged

feat: add relayfile control-plane client#344
khaliqgant merged 5 commits into
mainfrom
codex/integration-native-resource-globs

Conversation

@khaliqgant

@khaliqgant khaliqgant commented Jun 29, 2026

Copy link
Copy Markdown
Member

Summary

  • add relayfile control-plane serve with a versioned local HTTP/JSON API over a Unix socket, including /v1/hello API negotiation
  • expose integration control-plane operations over that socket: providers/status/connect/resolve-path/bind/listBindings/unbind/writeback-secret
  • add the authoritative openapi/relayfile-control-plane-v1.openapi.yaml contract and generated @relayfile/client package
  • bump the CLI release surface to 0.10.16, including parseable Go-binary relayfile --version output and bind --list --json compatibility
  • wire @relayfile/client into root workspaces, CI codegen drift checks, changelogs, and the publish workflow

Release / Merge Ordering

Verification

  • npm run codegen --workspace=@relayfile/client && git diff --exit-code -- packages/client/src/generated/control-plane.ts
  • npm run typecheck --workspace=@relayfile/client
  • npm run build --workspace=@relayfile/client
  • npm run test --workspace=@relayfile/client
  • RELAYFILE_BIN=/tmp/relayfile-client-check/relayfile npm run test --workspace=@relayfile/client
  • npm pack --workspace=@relayfile/client --dry-run
  • npm ci --dry-run
  • npm run build
  • npm run typecheck
  • npm run test
  • go test ./...
  • scripts/check-contract-surface.sh
  • go run ./cmd/relayfile-cli --version -> 0.10.16
  • HOME=$(mktemp -d) go run ./cmd/relayfile-cli integration bind --list --json -> []

Review in cubic

@coderabbitai

coderabbitai Bot commented Jun 29, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@khaliqgant, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 55 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 0d40dbe4-4c17-44c3-9a59-ade4fb82822d

📥 Commits

Reviewing files that changed from the base of the PR and between 2cdfb77 and d4bac0e.

⛔ Files ignored due to path filters (4)
  • package-lock.json is excluded by !**/package-lock.json
  • packages/client/src/generated/control-plane.ts is excluded by !**/generated/**
  • packages/file-observer/package-lock.json is excluded by !**/package-lock.json
  • packages/sdk/typescript/package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (29)
  • .github/workflows/ci.yml
  • .github/workflows/publish.yml
  • cmd/relayfile-cli/control_plane.go
  • cmd/relayfile-cli/control_plane_socket_unix.go
  • cmd/relayfile-cli/control_plane_socket_windows.go
  • cmd/relayfile-cli/control_plane_test.go
  • cmd/relayfile-cli/main.go
  • openapi/relayfile-control-plane-v1.openapi.yaml
  • package.json
  • packages/agents/package.json
  • packages/cli/CHANGELOG.md
  • packages/cli/package.json
  • packages/cli/scripts/build-binaries.js
  • packages/client/CHANGELOG.md
  • packages/client/package.json
  • packages/client/src/client.test.ts
  • packages/client/src/client.ts
  • packages/client/src/index.ts
  • packages/client/tsconfig.json
  • packages/client/vitest.config.ts
  • packages/core/package.json
  • packages/file-observer/package.json
  • packages/local-mount/package.json
  • packages/mount-darwin-arm64/package.json
  • packages/mount-darwin-x64/package.json
  • packages/mount-linux-arm64/package.json
  • packages/mount-linux-x64/package.json
  • packages/sdk/typescript/package.json
  • scripts/finalize-changelogs.mjs
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch codex/integration-native-resource-globs

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.

@gemini-code-assist gemini-code-assist Bot 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.

Code Review

This pull request introduces a versioned local control-plane API for the relayfile daemon/CLI, implementing a Unix domain socket server with endpoints for managing integration bindings, resolving resource paths, and checking provider status. It also adds a typed TypeScript client (@relayfile/client) generated from the OpenAPI contract. The review feedback identifies several important issues: potential race conditions during concurrent binding updates, a security window where the Unix socket is briefly accessible to other local users before permissions are restricted, missing timeouts on both the Go HTTP server and the TypeScript client, incorrect response header ordering in error handling, potential NaN errors in semver comparison, and a directory traversal vulnerability in workspace path resolution.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment on lines +364 to +387
func handleControlPlaneBind(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
writeControlPlaneError(w, http.StatusMethodNotAllowed, controlPlaneErrInvalidArgument, "method not allowed")
return
}
var req bindRequest
if err := decodeControlPlaneJSON(r, &req); err != nil {
writeControlPlaneError(w, http.StatusBadRequest, controlPlaneErrInvalidArgument, err.Error())
return
}
binding, replaced, warning, err := bindRelayIntegration(relayIntegrationBindInput{
Provider: req.Provider,
Resource: req.Resource,
Channel: req.Channel,
WebhookID: req.WebhookID,
WebhookToken: req.WebhookToken,
SubscriptionID: req.SubscriptionID,
})
if err != nil {
writeControlPlaneMappedError(w, err)
return
}
writeControlPlaneJSON(w, http.StatusOK, bindResponse{Binding: binding, Replaced: replaced, Warning: warning})
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

high

Since the control plane HTTP server handles requests concurrently, multiple concurrent requests to /v1/integrations/bind or /v1/integrations/unbind will read and write the same bindings JSON file concurrently. This can lead to race conditions, lost updates, or file corruption. To prevent this, introduce a package-level sync.Mutex in control_plane.go and use it to synchronize access to bindRelayIntegration, unbindRelayIntegration, and readRelayIntegrationBindings in their respective HTTP handlers.

Comment on lines +152 to +164
if err := os.MkdirAll(filepath.Dir(sock), 0o700); err != nil {
return err
}
_ = os.Remove(sock)
listener, err := net.Listen("unix", sock)
if err != nil {
return err
}
defer listener.Close()
defer os.Remove(sock)
if err := os.Chmod(sock, 0o600); err != nil {
return err
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

high

When the Unix socket is created in a shared directory like /tmp (which is the default when XDG_RUNTIME_DIR and RELAYFILE_SOCK are unset), net.Listen creates the socket file with permissions governed by the process's umask. There is a brief window of time between net.Listen and os.Chmod where other local users could potentially connect to the socket if the umask is permissive. To prevent this, consider setting a restrictive umask (e.g., 0077) before listening, or creating a dedicated subdirectory with 0700 permissions to house the socket file.

Comment thread cmd/relayfile-cli/control_plane.go Outdated
return err
}

server := &http.Server{Handler: newControlPlaneHandler()}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

The http.Server is created without any timeouts configured. To prevent resource exhaustion and potential denial-of-service (DoS) from slow or hanging connections, it is highly recommended to set explicit timeouts (ReadTimeout, WriteTimeout, and IdleTimeout).

	server := &http.Server{
		Handler:      newControlPlaneHandler(),
		ReadTimeout:  5 * time.Second,
		WriteTimeout: 5 * time.Second,
		IdleTimeout:  15 * time.Second,
	}

Comment on lines +546 to +556
func writeControlPlaneJSON(w http.ResponseWriter, status int, value any) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
payload, err := json.MarshalIndent(value, "", " ")
if err != nil {
_, _ = w.Write([]byte(`{"error":{"code":"DAEMON_UNAVAILABLE","message":"failed to encode response"}}` + "\n"))
return
}
payload = append(payload, '\n')
_, _ = w.Write(payload)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

In writeControlPlaneJSON, w.WriteHeader(status) is called before json.MarshalIndent. If the JSON marshalling fails, the header has already been sent, meaning the client will receive a misleading 200 OK (or other success status) followed by the error JSON body. Marshalling the payload first allows you to correctly return a 500 Internal Server Error if encoding fails.

func writeControlPlaneJSON(w http.ResponseWriter, status int, value any) {
	payload, err := json.MarshalIndent(value, "", "  ")
	if err != nil {
		w.Header().Set("Content-Type", "application/json")
		w.WriteHeader(http.StatusInternalServerError)
		_, _ = w.Write([]byte(`{"error":{"code":"DAEMON_UNAVAILABLE","message":"failed to encode response"}}` + "\n"))
		return
	}
	payload = append(payload, '\n')
	w.Header().Set("Content-Type", "application/json")
	w.WriteHeader(status)
	_, _ = w.Write(payload)
}

Comment on lines +43 to +48
export function compareSemver(a: string, b: string): number {
const core = (v: string) => v.split(/[-+]/)[0]!.split('.').map((n) => Number.parseInt(n, 10) || 0);
const [a0, a1, a2] = core(a);
const [b0, b1, b2] = core(b);
return (a0! - b0!) || (a1! - b1!) || (a2! - b2!);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

If compareSemver is called with a 2-part version or a non-standard version string, core(v) might return an array with fewer than 3 elements, resulting in undefined values and causing the subtraction to return NaN. Since this function is exported, it should be made robust against such inputs by defaulting missing parts to 0.

Suggested change
export function compareSemver(a: string, b: string): number {
const core = (v: string) => v.split(/[-+]/)[0]!.split('.').map((n) => Number.parseInt(n, 10) || 0);
const [a0, a1, a2] = core(a);
const [b0, b1, b2] = core(b);
return (a0! - b0!) || (a1! - b1!) || (a2! - b2!);
}
export function compareSemver(a: string, b: string): number {
const core = (v: string) => v.split(/[-+]/)[0]!.split('.').map((n) => Number.parseInt(n, 10) || 0);
const aParts = core(a);
const bParts = core(b);
const a0 = aParts[0] ?? 0;
const a1 = aParts[1] ?? 0;
const a2 = aParts[2] ?? 0;
const b0 = bParts[0] ?? 0;
const b1 = bParts[1] ?? 0;
const b2 = bParts[2] ?? 0;
return (a0 - b0) || (a1 - b1) || (a2 - b2);
}

Comment on lines +156 to +168
const req = request(
{
socketPath: this.socketPath,
method: opts.method,
path,
headers: {
'X-Relayfile-API-Version': String(RELAYFILE_API_VERSION),
Accept: 'application/json',
...(payload
? { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(payload) }
: {}),
},
},

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

The HTTP request to the local Unix socket does not have any timeout configured. If the daemon hangs or becomes unresponsive, the client request will hang indefinitely. It is recommended to set a default timeout (e.g., 15 seconds) and handle the 'timeout' event to destroy the request and reject the promise.

      const req = request(
        {
          socketPath: this.socketPath,
          method: opts.method,
          path,
          timeout: 15000,
          headers: {
            'X-Relayfile-API-Version': String(RELAYFILE_API_VERSION),
            Accept: 'application/json',
            ...(payload
              ? { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(payload) }
              : {}),
          },
        },

Comment thread cmd/relayfile-cli/main.go
Comment on lines +8322 to +8333
func readMountedJSON(remotePath string, out any) bool {
record, ok := activeWorkspaceRecordForMountResolution()
if !ok || strings.TrimSpace(record.LocalDir) == "" {
return false
}
localPath := filepath.Join(record.LocalDir, filepath.FromSlash(strings.TrimPrefix(remotePath, "/")))
payload, err := os.ReadFile(localPath)
if err != nil {
return false
}
return json.Unmarshal(payload, out) == nil
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

In readMountedJSON, localPath is constructed by joining record.LocalDir with remotePath. If remotePath contains directory traversal sequences (like ..), it could potentially escape record.LocalDir. Although current callers slugify or sanitize inputs, it is highly recommended to implement defensive path traversal checks to ensure localPath remains strictly within record.LocalDir.

func readMountedJSON(remotePath string, out any) bool {
	record, ok := activeWorkspaceRecordForMountResolution()
	if !ok || strings.TrimSpace(record.LocalDir) == "" {
		return false
	}
	localPath := filepath.Join(record.LocalDir, filepath.FromSlash(strings.TrimPrefix(remotePath, "/")))
	rel, err := filepath.Rel(record.LocalDir, localPath)
	if err != nil || strings.HasPrefix(rel, "..") {
		return false
	}
	payload, err := os.ReadFile(localPath)
	if err != nil {
		return false
	}
	return json.Unmarshal(payload, out) == nil
}

@chatgpt-codex-connector chatgpt-codex-connector Bot 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: f9c60d083c

ℹ️ 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 +261 to +264
child = spawn(this.binary, ['control-plane', 'serve', '--sock', this.socketPath], {
detached: true,
stdio: 'ignore',
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Handle auto-start spawn errors

When autoStart is left at its default and relayfile/RELAYFILE_BIN is missing or not executable, Node's spawn() returns a child and emits the failure asynchronously, so this try/catch does not catch it. Because no error listener is attached, the consumer process gets an uncaught exception instead of a rejected DAEMON_UNAVAILABLE error. Attach an error handler before polling (or otherwise convert the spawn failure into the intended control-plane error).

Useful? React with 👍 / 👎.

Comment thread cmd/relayfile-cli/main.go
Comment on lines +2480 to +2484
type relayIntegrationUnbindResult struct {
Provider string
PathGlob string
Removed int
Warning string

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 Add JSON tags to unbind response

When /v1/integrations/unbind returns this value, encoding/json uses these exported Go field names because the new response struct has no JSON tags, so the daemon emits Provider, PathGlob, and Removed instead of the OpenAPI-documented provider, pathGlob, and removed. OpenAPI/JS clients reading the documented fields will see them as missing even though the operation succeeded; add lower-camel JSON tags or a tagged response DTO.

Useful? React with 👍 / 👎.

@devin-ai-integration devin-ai-integration 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.

Devin Review found 4 potential issues.

Open in Devin Review

Comment thread cmd/relayfile-cli/main.go
Comment on lines +2480 to +2485
type relayIntegrationUnbindResult struct {
Provider string
PathGlob string
Removed int
Warning string
}

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.

🔴 Unbind response fields are invisible to all non-Go API clients

The unbind result struct is serialized without JSON field-name tags (relayIntegrationUnbindResult at cmd/relayfile-cli/main.go:2480-2485), so all four fields arrive as PascalCase instead of the camelCase the OpenAPI contract promises.

Impact: Every TypeScript (or other non-Go) consumer of the /v1/integrations/unbind endpoint receives undefined for provider, pathGlob, removed, and warning.

Go struct lacks json tags while every other response type has them

The struct at cmd/relayfile-cli/main.go:2480-2485:

type relayIntegrationUnbindResult struct {
	Provider string
	PathGlob string
	Removed  int
	Warning  string
}

Go's json.Marshal produces {"Provider":"slack","PathGlob":"/slack/channels/**","Removed":1,"Warning":""} — PascalCase.

The OpenAPI spec at openapi/relayfile-control-plane-v1.openapi.yaml:457-468 declares the fields as provider, pathGlob, removed, warning (camelCase), and the generated TypeScript type at packages/client/src/generated/control-plane.ts:257-262 expects the same.

Every other response struct serialized by the control-plane has proper JSON tags (e.g., bindResponse at cmd/relayfile-cli/control_plane.go:98-102, helloResponse at line 45-49). This struct is the only one missing them.

The Go test at cmd/relayfile-cli/control_plane_test.go:113-122 deserializes into the same Go struct, so PascalCase round-trips fine and the test passes — but any external client using the OpenAPI-generated types sees all fields as undefined.

Suggested change
type relayIntegrationUnbindResult struct {
Provider string
PathGlob string
Removed int
Warning string
}
type relayIntegrationUnbindResult struct {
Provider string `json:"provider"`
PathGlob string `json:"pathGlob"`
Removed int `json:"removed"`
Warning string `json:"warning,omitempty"`
}
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +527 to +529
case strings.Contains(message, "PATH_GLOB") || strings.Contains(message, "resource"):
status = http.StatusUnprocessableEntity
code = controlPlaneErrResourceUnresolved

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.

🟡 Errors from connect and writeback endpoints can be misclassified as resource-resolution failures

Any error whose message contains the common word "resource" is classified as a 422 resource-resolution failure (strings.Contains(message, "resource") at cmd/relayfile-cli/control_plane.go:527), even when the error originates from unrelated operations like provider connect or writeback-secret.

Impact: Clients may receive a misleading 422 "RESOURCE_UNRESOLVED" status instead of the correct error code when a connect or writeback call fails with a message that happens to include "resource".

writeControlPlaneMappedError is shared across all handlers but the "resource" match is too broad

writeControlPlaneMappedError at cmd/relayfile-cli/control_plane.go:519-534 is called from seven different handlers (lines 281, 329, 352, 383, 396, 414, 436). The strings.Contains(message, "resource") check at line 527 was designed to catch errors from resolveIntegrationBindPathGlob (e.g., "PATH_GLOB/resource is required", "%s resource is empty after trimming sigils"), but it also fires for errors from handleControlPlaneConnectProvider (line 329) and handleControlPlaneWritebackSecret (line 436).

For example, runIntegrationConnect can call listAccessibleResources at cmd/relayfile-cli/main.go:2764 which returns "list %s accessible resources: %w" — this contains "resource" and would be misclassified as 422/RESOURCE_UNRESOLVED instead of a connection error.

A narrower match (e.g., checking for specific error prefixes or using structured error types) would prevent the false positive.

Prompt for agents
The writeControlPlaneMappedError function at cmd/relayfile-cli/control_plane.go:519-534 uses strings.Contains(message, "resource") to classify errors as RESOURCE_UNRESOLVED (422). This is too broad because the function is called from all control-plane handlers, not just the resolve-path handler. Errors from runIntegrationConnect (e.g. "list jira accessible resources: ...") or other handlers that happen to contain the word "resource" get misclassified.

Possible fixes:
1. Use more specific string patterns (e.g. check for known error prefixes from resolveIntegrationBindPathGlob).
2. Use structured error types (e.g. a custom error interface with a Code() method) so the mapper can inspect the error type rather than string-matching.
3. Move the resource-specific error mapping closer to the handlers that deal with resource resolution (resolve-path, bind, unbind) instead of applying it globally.
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment thread cmd/relayfile-cli/main.go
Comment on lines +8341 to +8359
func activeWorkspaceRecordForMountResolution() (workspaceRecord, bool) {
activeWorkspaceRecordCacheOnce.Do(func() {
creds, _ := loadCredentials()
name, _ := activeWorkspaceName(resolveToken("", creds))
if strings.TrimSpace(name) == "" {
return
}
if record, ok := workspaceRecordByName(name); ok {
activeWorkspaceRecordCache = record
activeWorkspaceRecordCacheOK = true
return
}
if record, ok := workspaceRecordByID(name); ok {
activeWorkspaceRecordCache = record
activeWorkspaceRecordCacheOK = true
return
}
})
return activeWorkspaceRecordCache, activeWorkspaceRecordCacheOK

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.

🚩 Process-level sync.Once cache for workspace resolution may go stale in long-lived daemon

The activeWorkspaceRecordForMountResolution() function at cmd/relayfile-cli/main.go:8341-8359 uses a sync.Once to cache the active workspace record for the lifetime of the process. This is fine for short-lived CLI invocations, but the control-plane server (control-plane serve) is a long-lived daemon. If the user switches workspaces while the daemon is running (e.g., relayfile workspace use other-ws), the daemon will continue resolving native resources against the old workspace's mount directory. The resetActiveWorkspaceRecordForMountResolutionCache() function exists but is only called from test helpers (cmd/relayfile-cli/main_test.go:2803), not from any production code path. This may be acceptable if daemon restarts are expected on workspace changes, but it's worth confirming that expectation.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

args = append(args, "--wait-sync")
}
var out bytes.Buffer
if err := runIntegrationConnect(args, strings.NewReader(""), &out); err != nil {

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.

🚩 Connect endpoint passes empty stdin, blocking interactive Atlassian flows

The control-plane connect handler at cmd/relayfile-cli/control_plane.go:328 calls runIntegrationConnect(args, strings.NewReader(""), &out) with an empty reader as stdin. For Atlassian providers (Jira, Confluence), runIntegrationConnect calls runAtlassianSitePicker at cmd/relayfile-cli/main.go:2701-2704 which may prompt for interactive site selection via stdin. With an empty reader, the prompt would read an empty string and likely fail or select a default. This is probably acceptable since the control-plane is designed for non-interactive use and callers should use the noOpen: true option, but Atlassian connect via the control-plane socket will likely fail or behave unexpectedly without documenting this limitation.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

@agent-relay-code

Copy link
Copy Markdown
Contributor

gofmt reports no formatting issues. Everything is clean and verified. Let me write the final review.

Review: PR #344feat: add relayfile control-plane client

I reviewed the full diff (.workforce/pr.diff), traced every new symbol back to its definition, and ran the canonical build/test commands end-to-end with results below.

What this PR does

Adds a versioned local control-plane: a new Go HTTP/JSON server over a unix socket (relayfile control-plane serve), a typed TypeScript client package (@relayfile/client) generated from a new OpenAPI contract, native-resource→VFS-glob resolution for integration bind/unbind/resolve-path, a --version/version command, and release/CI wiring for the new package.

Verification (ran the way CI runs it)

Go is not preinstalled in this sandbox; I installed Go 1.23.4 and ran:

  • go build ./...OK (all packages, not just the edited one)
  • go vet ./cmd/relayfile-cli/OK
  • gofmt -l on all changed Go files → clean
  • go test ./cmd/relayfile-cli/ -count=1 (full package) → PASS (72s)

TypeScript client (mirrors the new client-typecheck CI job):

  • npm ci → OK
  • npm run codegen --workspace=@relayfile/client then git diff --exit-code on generated/control-plane.tsgenerated file matches committed (the CI drift gate passes)
  • build / typecheck / testPASS (7 unit tests, 3 contract tests)
  • Built the Go binary and re-ran with RELAYFILE_BIN set → all 10 client tests pass, exercising the real Go↔TS wire contract (hello/resolve/bind/list/unbind) over the socket.

Correctness checks I traced

  • All Go symbols referenced by control_plane.go exist with matching signatures (runIntegrationConnect/List/WritebackSecret, cloudIntegrationListEntry.Status, bindRelayIntegration, etc.); main.go already imports path, net/url, sync, encoding/json.
  • TS schema names used in client.ts (HelloResponse, Binding, ResolveResourcePathResponse, BindResponse, BindRequest, ConnectProviderRequest/Response, ProviderStatus, WritebackSecret) all exist in the generated types; field shapes match the Go JSON tags.
  • The data-plane contract rule in CLAUDE.md targets openapi/relayfile-v1.openapi.yaml + internal/httpapi/server.go; this PR adds a separate control-plane contract and a cmd/ server, so scripts/check-contract-surface.sh is unaffected.
  • Fail-closed defaults are preserved: controlPlaneVersionFromRequest returns max-uint32 on unparseable input → version-incompatible (does not fail open); the client treats only 2xx as success and surfaces typed error codes.

Addressed comments

  • No bot or human review comments are present in .workforce/context.json (no comments/reviews payload, and the run is comment-only metadata). There were no threads to resolve.

Advisory Notes

  • None of these block the PR; raising for the human author's judgment only:
    • handleControlPlaneListProviders always returns fallbackIntegrationCatalog() (static), unlike provider-status which queries the cloud. Likely intentional for a local catalog, but worth confirming it shouldn't reflect live/connected providers.
    • connect/writeback-secret/provider-status shell out to existing runIntegration* functions and string-match error text in writeControlPlaneMappedError (e.g. "no binding found", "connection refused"). Functional today and covered by tests, but brittle if those upstream messages change — a future typed-error path would be more robust.

No mechanical fixes were needed (gofmt clean, generated file in sync, no lint/import issues). I made no code edits; the working tree is unchanged.

The PR builds and passes the full Go and client test suites including the real-daemon wire-contract tests. I cannot confirm the live CI run status or mergeable/conflict state from this sandbox, and those checks (plus any required reviewers) are post-harness actions reported separately — so I am not declaring it human-ready here.

@github-actions

Copy link
Copy Markdown

Relayfile Eval Review

Run: .relayfile/evals/runs/2026-06-29T23-28-22-380Z-HEAD-provider
Mode: provider
Git SHA: 0552fdf

Passed: 4 | Needs human: 0 | Reviewable: 0 | Missing output: 0 | Failed: 0 | Skipped: 0

Human Review Cases

No reviewable human-review cases captured Relayfile output.

@khaliqgant khaliqgant merged commit cd10d25 into main Jun 29, 2026
10 checks passed
@khaliqgant khaliqgant deleted the codex/integration-native-resource-globs branch June 29, 2026 23:31
@agent-relay-code

Copy link
Copy Markdown
Contributor

Review: PR #344 — Integration native resource globs / control-plane client

Summary

This PR adds a versioned local control-plane to the relayfile CLI (relayfile control-plane serve) exposing HTTP/JSON over a unix socket, plus a new typed @relayfile/client package whose request/response types are generated from the authoritative OpenAPI contract. It also bumps the workspace to 0.10.17, wires the new package into CI/publish, and refactors integration bind/unbind into reusable bindRelayIntegration/unbindRelayIntegration functions guarded by a new mutex.

I traced the diff across callers, types, generated artifacts, the OpenAPI spec, lockfiles, and CI workflow, and verified it the way CI does.

Verification (ran the canonical build/test end-to-end)

Go (matched go.mod 1.22):

  • go build ./cmd/relayfile-cli/ → OK
  • go vet ./cmd/relayfile-cli/ → clean
  • go test ./cmd/relayfile-cli/ -run ControlPlane → ok (hello/version negotiation, binding conformance, cloud integration conformance)

Client package (exactly the new client-typecheck CI job):

  • npm ci → lockfile in sync (465 packages, new openapi-typescript/vitest/redocly deps resolve)
  • npm run codegen + git diff --exit-code on generated/control-plane.tsno drift
  • npm run build (tsc) → OK
  • npm run typecheck (tsc --noEmit) → OK
  • npm run test (vitest) → 9 passed / 3 skipped
  • Real-daemon contract tests (built the Go binary, set RELAYFILE_BIN) → 12/12 passed — confirms the Go daemon ↔ TS client wire contract round-trips (hello, resolvePath native→glob, bind → listBindings → unbind).

Cross-checked that every type/function the new control_plane.go references exists with matching signatures/fields in main.go (integrationCatalogEntry, cloudIntegrationListEntry, runIntegrationConnect(args, stdin, stdout), runIntegrationList, runIntegrationWritebackSecret, fallbackIntegrationCatalog, etc.). Version constant relayfileDefaultVersion = "0.10.17" is consistent across main.go, all package.json files, both lockfiles, and the --version test expectation. The generated TS schemas (ProviderStatus, WritebackSecret, Binding, …) line up with the Go JSON tags.

No source edits were required — the working tree is clean (only gitignored node_modules/dist were touched). I did not modify any code or tests.

Addressed comments

  • No bot or human reviewer comments were present in the provided context (.workforce/context.json carries only PR metadata; pr.diff and changed-files.txt carry no review threads). The head commit 962ea6b "Harden control plane feedback paths" indicates earlier hardening feedback was already folded in by a later push, so there is nothing outstanding to reconcile against the current checkout.

Observations (non-blocking, no change made)

  • writeControlPlaneMappedError classifies errors by substring matching ("no binding found", "PATH_GLOB"/"resource", "connection refused"/"credentials not found"/"agent-relay"). This is fragile to upstream error-message wording but is correct for the current messages and is exercised by the conformance tests. Not changing it — it's working behavior and any change here is a judgment call for the author.
  • handleControlPlaneListProviders returns the static fallbackIntegrationCatalog() rather than live providers; this matches the OpenAPI description ("known integration providers") and the test only asserts non-empty. Intentional as written.

Advisory Notes

  • None. The change is self-contained to the new control-plane/client surface and version bump; it does not touch unrelated packages.

This PR is mechanically and contractually sound on the current checkout. However, I cannot confirm the live CI run status, merge-conflict/mergeability state, or that all required checks have completed and passed — those are reported by the harness post-run, not something I can observe here. Because I cannot verify that every required check has completed and is green, I am not printing READY.

Sign up for free to 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