Skip to content

Commit 36e3324

Browse files
murphyclaude
authored andcommitted
feat(cli): let environment add override the advertised pairing endpoint
A pairing offer embeds whatever address the host advertised. "Share this Orca server" defaults its address picker to 127.0.0.1, so a link minted for a LAN or Tailscale peer routinely carries a loopback endpoint that the receiving machine dials against itself. The credentials are fine — only the address is wrong — but the CLI had no way to correct it, leaving hand-editing the base64 offer as the only path. `environment add --endpoint <host>` redirects the offer while keeping the deviceToken and pinned public key the host issued. Resolution reuses resolveAdvertisedPairingEndpoint, so the flag accepts the same host, host:port, and ws(s):// forms as the address field that produced the link, inherits the port from the pairing code when omitted, and rejects wildcard bind addresses no client can dial. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent e104010 commit 36e3324

8 files changed

Lines changed: 156 additions & 11 deletions

File tree

config/tsconfig.cli.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@
6868
"../src/main/kimi/kimi-hook-config-toml.ts",
6969
"../src/main/openclaude/hook-service.ts",
7070
"../src/main/rolling-file-backup.ts",
71+
"../src/main/runtime/pairing-endpoint.ts",
7172
"../src/main/runtime/runtime-metadata.ts",
7273
"../src/main/win32-utils.ts"
7374
],

src/cli/handlers/environment.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,12 @@ export const ENVIRONMENT_HANDLERS: Record<string, CommandHandler> = {
1717
'environment add': async ({ flags, json }) => {
1818
const name = getRequiredStringFlag(flags, 'name')
1919
const pairingCode = getRequiredStringFlag(flags, 'pairing-code')
20+
const endpointAddress = getOptionalStringFlag(flags, 'endpoint')
2021
const environment = redactRuntimeEnvironment(
2122
addEnvironmentFromPairingCode(getDefaultUserDataPath(), {
2223
name,
23-
pairingCode
24+
pairingCode,
25+
...(endpointAddress ? { endpointAddress } : {})
2426
})
2527
)
2628
printResult(
@@ -55,6 +57,17 @@ export const ENVIRONMENT_HANDLERS: Record<string, CommandHandler> = {
5557
}
5658
}
5759

60+
function getOptionalStringFlag(flags: Map<string, string | boolean>, name: string): string | null {
61+
const value = flags.get(name)
62+
if (value === undefined) {
63+
return null
64+
}
65+
if (typeof value !== 'string' || value.length === 0) {
66+
throw new RuntimeClientError('invalid_argument', `--${name} requires a value`)
67+
}
68+
return value
69+
}
70+
5871
function getRequiredStringFlag(flags: Map<string, string | boolean>, name: string): string {
5972
const value = flags.get(name)
6073
if (typeof value !== 'string' || value.length === 0) {

src/cli/help.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -212,7 +212,7 @@ Common Commands:
212212
orca status [--json]
213213
orca diagnostics memory [--json]
214214
orca agent-context [--json]
215-
orca environment add --name <name> --pairing-code <code> [--json]
215+
orca environment add --name <name> --pairing-code <code> [--endpoint <host>] [--json]
216216
orca environment list [--json]
217217
orca environment show --environment <selector> [--json]
218218
orca environment rm --environment <selector> [--json]
@@ -505,6 +505,8 @@ export function formatFlagHelp(flag: string): string {
505505
'display-name': '--display-name <name> Override the Orca display name',
506506
'element-index': '--element-index <n> Element index from get-app-state',
507507
title: '--title <text> Custom title for the terminal tab (omit to reset)',
508+
endpoint:
509+
'--endpoint <host> Reachable host, host:port, or ws(s):// URL replacing the address advertised by the pairing code',
508510
enter: '--enter Append Enter after sending text',
509511
force: '--force Force worktree removal when supported',
510512
focus: '--focus Reveal the created terminal session in Orca',

src/cli/runtime/environments.test.ts

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,4 +81,52 @@ describe('CLI runtime environments', () => {
8181
)
8282
expect(listEnvironments(userDataPath)[0]?.id).toBe(first.id)
8383
})
84+
85+
describe('endpointAddress override', () => {
86+
function addWithEndpoint(endpointAddress: string, offerEndpoint = 'ws://127.0.0.1:6768') {
87+
const userDataPath = mkdtempSync(join(tmpdir(), 'orca-env-store-'))
88+
addEnvironmentFromPairingCode(userDataPath, {
89+
name: 'workstation',
90+
pairingCode: pairingCode(offerEndpoint),
91+
endpointAddress,
92+
now: 100
93+
})
94+
return resolveEnvironmentPairingOffer(userDataPath, 'workstation')
95+
}
96+
97+
it('keeps the port from the pairing code for a bare host', () => {
98+
expect(addWithEndpoint('100.64.0.2').endpoint).toBe('ws://100.64.0.2:6768')
99+
})
100+
101+
it('accepts host:port and full ws(s):// forms', () => {
102+
expect(addWithEndpoint('desktop.tailnet.ts.net:7000').endpoint).toBe(
103+
'ws://desktop.tailnet.ts.net:7000'
104+
)
105+
expect(addWithEndpoint('wss://desktop.example.com/runtime').endpoint).toBe(
106+
'wss://desktop.example.com/runtime'
107+
)
108+
})
109+
110+
it('preserves the credentials the host issued', () => {
111+
const offer = addWithEndpoint('100.64.0.2')
112+
expect(offer.deviceToken).toBe('device-token')
113+
expect(offer.publicKeyB64).toBe(Buffer.from(new Uint8Array(32).fill(1)).toString('base64'))
114+
})
115+
116+
it('rejects an unreachable wildcard bind address', () => {
117+
expect(() => addWithEndpoint('0.0.0.0')).toThrow(/Invalid --endpoint "0\.0\.0\.0"/)
118+
})
119+
120+
it('leaves the advertised endpoint alone when omitted', () => {
121+
const userDataPath = mkdtempSync(join(tmpdir(), 'orca-env-store-'))
122+
addEnvironmentFromPairingCode(userDataPath, {
123+
name: 'workstation',
124+
pairingCode: pairingCode('ws://10.0.0.5:6768'),
125+
now: 100
126+
})
127+
expect(resolveEnvironmentPairingOffer(userDataPath, 'workstation').endpoint).toBe(
128+
'ws://10.0.0.5:6768'
129+
)
130+
})
131+
})
84132
})

src/cli/runtime/environments.ts

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@ import type {
1313
KnownRuntimeEnvironment,
1414
PublicKnownRuntimeEnvironment
1515
} from '../../shared/runtime-environments'
16-
import type { PairingOffer } from '../../shared/pairing'
16+
import { parsePairingCode, type PairingOffer } from '../../shared/pairing'
17+
import { resolveAdvertisedPairingEndpoint } from '../../main/runtime/pairing-endpoint'
1718
import { RuntimeClientError } from './types'
1819

1920
export type EnvironmentAddResult = {
@@ -32,9 +33,38 @@ export { getEnvironmentStorePath, listEnvironments }
3233

3334
export function addEnvironmentFromPairingCode(
3435
userDataPath: string,
35-
args: { name: string; pairingCode: string; now?: number }
36+
args: { name: string; pairingCode: string; now?: number; endpointAddress?: string }
3637
): KnownRuntimeEnvironment {
37-
return translateStoreError(() => addEnvironmentFromPairingCodeInStore(userDataPath, args))
38+
const { endpointAddress, ...rest } = args
39+
const endpoint = endpointAddress
40+
? resolveEndpointOverride(args.pairingCode, endpointAddress)
41+
: null
42+
return translateStoreError(() =>
43+
addEnvironmentFromPairingCodeInStore(userDataPath, {
44+
...rest,
45+
...(endpoint ? { endpoint } : {})
46+
})
47+
)
48+
}
49+
50+
// Why: reuse the host's advertise grammar so --endpoint accepts the same hosts,
51+
// host:port, and ws(s):// forms the "Share this Orca server" address field does.
52+
function resolveEndpointOverride(pairingCode: string, address: string): string {
53+
const offer = parsePairingCode(pairingCode)
54+
if (!offer) {
55+
throw new RuntimeClientError(
56+
'invalid_argument',
57+
'Invalid pairing code. Expected an orca://pair?... URL or bare pairing payload.'
58+
)
59+
}
60+
const resolved = resolveAdvertisedPairingEndpoint(offer.endpoint, address)
61+
if (!resolved.ok) {
62+
throw new RuntimeClientError(
63+
'invalid_argument',
64+
`Invalid --endpoint "${address}". ${resolved.guidance}`
65+
)
66+
}
67+
return resolved.endpoint
3868
}
3969

4070
export function removeEnvironment(userDataPath: string, selector: string): KnownRuntimeEnvironment {

src/cli/specs/environment.test.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import { describe, expect, it } from 'vitest'
2+
3+
import { parseArgs, validateCommandAndFlags } from '../args'
4+
import { ENVIRONMENT_COMMAND_SPECS } from './environment'
5+
6+
describe('environment command specs', () => {
7+
it('accepts --endpoint on environment add and carries its value through', () => {
8+
const parsed = parseArgs([
9+
'environment',
10+
'add',
11+
'--name',
12+
'work-laptop',
13+
'--pairing-code',
14+
'orca://pair?code=abc',
15+
'--endpoint',
16+
'100.64.0.2'
17+
])
18+
19+
expect(() => validateCommandAndFlags(ENVIRONMENT_COMMAND_SPECS, parsed)).not.toThrow()
20+
expect(parsed.flags.get('endpoint')).toBe('100.64.0.2')
21+
})
22+
23+
it('rejects --endpoint on commands that do not resolve an address', () => {
24+
for (const path of [
25+
['environment', 'list'],
26+
['environment', 'show', '--environment', 'work-laptop'],
27+
['environment', 'rm', '--environment', 'work-laptop']
28+
]) {
29+
const parsed = parseArgs([...path, '--endpoint', '100.64.0.2'])
30+
expect(() => validateCommandAndFlags(ENVIRONMENT_COMMAND_SPECS, parsed)).toThrow(
31+
/Unknown flag --endpoint/
32+
)
33+
}
34+
})
35+
})

src/cli/specs/environment.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,15 @@ export const ENVIRONMENT_COMMAND_SPECS: CommandSpec[] = [
55
{
66
path: ['environment', 'add'],
77
summary: 'Save a remote Orca runtime environment from a pairing code',
8-
usage: 'orca environment add --name <name> --pairing-code <code> [--json]',
9-
allowedFlags: [...GLOBAL_FLAGS, 'name'],
10-
examples: ['orca environment add --name work-laptop --pairing-code orca://pair?code=...']
8+
usage: 'orca environment add --name <name> --pairing-code <code> [--endpoint <host>] [--json]',
9+
allowedFlags: [...GLOBAL_FLAGS, 'name', 'endpoint'],
10+
examples: [
11+
'orca environment add --name work-laptop --pairing-code orca://pair?code=...',
12+
'orca environment add --name work-laptop --pairing-code orca://pair?code=... --endpoint 100.64.0.2'
13+
],
14+
notes: [
15+
'Use --endpoint when the host advertised an address this machine cannot reach (for example a 127.0.0.1 link generated for a LAN or Tailscale peer). It accepts a host, host:port, or ws(s):// URL and keeps the port from the pairing code when omitted.'
16+
]
1117
},
1218
{
1319
path: ['environment', 'list'],

src/shared/runtime-environment-store.ts

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -41,15 +41,25 @@ export function listEnvironments(userDataPath: string): KnownRuntimeEnvironment[
4141

4242
export function addEnvironmentFromPairingCode(
4343
userDataPath: string,
44-
args: { name: string; pairingCode: string; now?: number; source?: RuntimeEnvironmentSource }
44+
args: {
45+
name: string
46+
pairingCode: string
47+
now?: number
48+
source?: RuntimeEnvironmentSource
49+
/** Already-resolved ws(s):// URL replacing the offer's advertised endpoint. */
50+
endpoint?: string
51+
}
4552
): KnownRuntimeEnvironment {
46-
const offer = parsePairingCode(args.pairingCode)
47-
if (!offer) {
53+
const parsed = parsePairingCode(args.pairingCode)
54+
if (!parsed) {
4855
throw new RuntimeEnvironmentStoreError(
4956
'invalid_argument',
5057
'Invalid pairing code. Expected an orca://pair?... URL or bare pairing payload.'
5158
)
5259
}
60+
// Why: a host that advertised loopback mints an offer no other device can dial;
61+
// the token stays valid, so let the caller redirect it instead of re-pairing.
62+
const offer = args.endpoint ? { ...parsed, endpoint: args.endpoint } : parsed
5363
const store = readEnvironmentStore(userDataPath)
5464
const now = args.now ?? Date.now()
5565
const existing = store.environments.find((entry) => entry.name === args.name)

0 commit comments

Comments
 (0)