Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/cli-authenticated-base-url.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"executor": patch
"@executor-js/desktop": patch
---

Allow CLI commands to target authenticated local runtimes with `--base-url` or `EXECUTOR_BASE_URL`, report local 401 responses instead of starting a duplicate daemon, and let Desktop attach to an existing unauthenticated CLI daemon using the shared `~/.executor-global` scope.
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,16 @@ executor call gmail send '{"to":"alice@example.com","subject":"Hi"}'
`executor call`, `executor resume`, and `executor tools ...` commands auto-start a local daemon if needed.
If the default port is busy, the CLI will pick an available local port and track it automatically.

To point CLI commands at an existing authenticated local runtime, pass `--base-url` or set `EXECUTOR_BASE_URL`:

```bash
EXECUTOR_BASE_URL='http://executor:<password>@127.0.0.1:4789' executor tools sources
```

When using Executor Desktop, copy the password from **Settings → Desktop server**.
If a local runtime responds with `401 Unauthorized`, the CLI reports the auth requirement instead of starting a duplicate daemon on another port.
Executor Desktop also checks for an existing unauthenticated CLI daemon on `4788` using the shared `~/.executor-global` scope before it starts its own sidecar.

If an execution pauses for auth or approval, resume it:

```bash
Expand Down
36 changes: 36 additions & 0 deletions apps/cli/src/daemon.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { Buffer } from "node:buffer";
import { spawn } from "node:child_process";
import { createServer } from "node:net";
import * as Clock from "effect/Clock";
Expand Down Expand Up @@ -44,6 +45,41 @@ export const parseDaemonBaseUrl = (baseUrl: string, defaultPort: number): Parsed
};
};

export const baseUrlWithoutCredentials = (baseUrl: string): string => {
const parsed = new URL(baseUrl);
parsed.username = "";
parsed.password = "";
parsed.pathname = "";
parsed.search = "";
parsed.hash = "";
return parsed.toString().replace(/\/$/, "");
};

export const baseUrlAuthorizationHeader = (baseUrl: string): string | undefined => {
const parsed = new URL(baseUrl);
if (!parsed.username && !parsed.password) return undefined;

const username = decodeURIComponent(parsed.username);
const password = decodeURIComponent(parsed.password);
const credentials = Buffer.from(`${username}:${password}`).toString("base64");
return `Basic ${credentials}`;
};

export const daemonBaseUrlFromRequest = (input: {
readonly requestedBaseUrl: string;
readonly hostname: string;
readonly port: number;
}): string => {
const parsed = new URL(input.requestedBaseUrl);
parsed.protocol = "http:";
parsed.hostname = input.hostname;
parsed.port = String(input.port);
parsed.pathname = "";
parsed.search = "";
parsed.hash = "";
return parsed.toString().replace(/\/$/, "");
};

// ---------------------------------------------------------------------------
// Local-host checks
// ---------------------------------------------------------------------------
Expand Down
Loading