skydive-cli 0.5.0-beta.7 → 0.5.0-beta.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/js/api-DG5W6iwx.mjs +131 -0
- package/dist/js/bin.mjs +49 -13
- package/dist/js/{boot-odaKymWy.mjs → boot-BCClEpCM.mjs} +7 -6
- package/dist/js/client-BuU34IVE.mjs +5 -0
- package/dist/js/{client-CmEF9zrz.mjs → client-DabRpc_T.mjs} +136 -113
- package/dist/js/{daemon-DUkLMp3w.mjs → daemon-BxU59xie.mjs} +3 -2
- package/dist/js/daemon-client-C2BvZgKO.mjs +7 -0
- package/dist/js/{daemon-client-DM8RYJDz.mjs → daemon-client-fxf1A25Z.mjs} +1 -1
- package/dist/js/{daemon-DXuENdsx.mjs → daemon-k2kVkJ8D.mjs} +2 -2
- package/dist/js/forward-18QoL5dO.mjs +68 -0
- package/dist/js/{print-share-D9RczQx8.mjs → print-share-CKLPmsg0.mjs} +1 -1
- package/dist/js/{profiler-wvJ7kT6W.mjs → profiler-DawY0V0Z.mjs} +1 -1
- package/dist/js/{raw-pty-B6mAroiI.mjs → raw-pty-DY4KelZW.mjs} +1 -1
- package/dist/js/raw-pty-DmdUf4_w.mjs +5 -0
- package/package.json +1 -1
- package/dist/js/client-CGwes_WT.mjs +0 -4
- package/dist/js/daemon-client-B3Yzyd46.mjs +0 -6
- package/dist/js/raw-pty-3EkG-jjH.mjs +0 -5
- /package/dist/js/{client-Cn2af31H.mjs → client-c4c5MmgN.mjs} +0 -0
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { t as HttpError } from "./http-error-DzyrsLAZ.mjs";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
|
|
5
|
+
//#region ../portal-daemon/src/api.ts
|
|
6
|
+
/**
|
|
7
|
+
* The portal's session-authed REST surface, shared by `PortalClient` (the
|
|
8
|
+
* TUI/`portal open` connection) and the `skydive portal` management
|
|
9
|
+
* commands, so the endpoint contracts and response schemas live in exactly
|
|
10
|
+
* one place.
|
|
11
|
+
*/
|
|
12
|
+
const deviceSchema = z.object({
|
|
13
|
+
id: z.string(),
|
|
14
|
+
machineName: z.string(),
|
|
15
|
+
friendlyName: z.string(),
|
|
16
|
+
connected: z.boolean(),
|
|
17
|
+
lastSeen: z.string().nullable(),
|
|
18
|
+
grantedAgentIds: z.array(z.string())
|
|
19
|
+
});
|
|
20
|
+
const devicesResponseSchema = z.object({
|
|
21
|
+
devices: z.array(deviceSchema),
|
|
22
|
+
agents: z.array(z.object({
|
|
23
|
+
id: z.string(),
|
|
24
|
+
name: z.string()
|
|
25
|
+
}))
|
|
26
|
+
});
|
|
27
|
+
const deviceTokenSchema = z.object({ token: z.string().min(1) });
|
|
28
|
+
async function portalFetch(auth, path, init) {
|
|
29
|
+
const res = await fetch(`${auth.appUrl}${path}`, {
|
|
30
|
+
method: init.method,
|
|
31
|
+
headers: {
|
|
32
|
+
authorization: `Bearer ${auth.sessionToken}`,
|
|
33
|
+
accept: "application/json",
|
|
34
|
+
...init.body ? { "content-type": "application/json" } : {}
|
|
35
|
+
},
|
|
36
|
+
...init.body ? { body: init.body } : {}
|
|
37
|
+
});
|
|
38
|
+
if (!res.ok) {
|
|
39
|
+
const body = await res.text().catch(() => "");
|
|
40
|
+
throw new HttpError(res.status, body);
|
|
41
|
+
}
|
|
42
|
+
return res.json();
|
|
43
|
+
}
|
|
44
|
+
async function fetchPortalDevices(auth) {
|
|
45
|
+
const json = await portalFetch(auth, "/api/v1/portal/devices", { method: "GET" });
|
|
46
|
+
return devicesResponseSchema.parse(json);
|
|
47
|
+
}
|
|
48
|
+
const registerResponseSchema = z.object({ device: z.object({ id: z.string() }) });
|
|
49
|
+
/**
|
|
50
|
+
* Register this machine's device row without connecting. Connecting registers
|
|
51
|
+
* as a side effect; this covers granting an agent on a machine that has never
|
|
52
|
+
* shared yet (the grant references the device row).
|
|
53
|
+
*/
|
|
54
|
+
async function registerPortalDevice(auth, { machineName, friendlyName }) {
|
|
55
|
+
const json = await portalFetch(auth, "/api/v1/portal/devices", {
|
|
56
|
+
method: "POST",
|
|
57
|
+
body: JSON.stringify({
|
|
58
|
+
machineName,
|
|
59
|
+
friendlyName
|
|
60
|
+
})
|
|
61
|
+
});
|
|
62
|
+
return registerResponseSchema.parse(json).device;
|
|
63
|
+
}
|
|
64
|
+
/** Short-lived token the machine presents when dialing the portal WebSocket. */
|
|
65
|
+
async function mintPortalDeviceToken(auth) {
|
|
66
|
+
const json = await portalFetch(auth, "/api/v1/portal/device-token", { method: "POST" });
|
|
67
|
+
return deviceTokenSchema.parse(json).token;
|
|
68
|
+
}
|
|
69
|
+
const forwardTargetSchema = z.object({
|
|
70
|
+
daemonOrigin: z.string().min(1),
|
|
71
|
+
token: z.string().min(1),
|
|
72
|
+
expiresInSeconds: z.number()
|
|
73
|
+
});
|
|
74
|
+
/**
|
|
75
|
+
* Everything `portal forward` needs to dial an agent's sandbox daemon through
|
|
76
|
+
* the agent-webserver edge Worker: the daemon's stable public origin and a
|
|
77
|
+
* daemon auth token (canUse-gated server-side).
|
|
78
|
+
*/
|
|
79
|
+
async function fetchForwardTarget(auth, agentId) {
|
|
80
|
+
const json = await portalFetch(auth, "/api/v1/portal/forward-target", {
|
|
81
|
+
method: "POST",
|
|
82
|
+
body: JSON.stringify({ agentId })
|
|
83
|
+
});
|
|
84
|
+
return forwardTargetSchema.parse(json);
|
|
85
|
+
}
|
|
86
|
+
async function grantPortalAccess(auth, { deviceId, agentId, conversationId }) {
|
|
87
|
+
await portalFetch(auth, `/api/v1/portal/devices/${encodeURIComponent(deviceId)}/grants`, {
|
|
88
|
+
method: "POST",
|
|
89
|
+
body: JSON.stringify({
|
|
90
|
+
agentId,
|
|
91
|
+
conversationId
|
|
92
|
+
})
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
async function revokePortalAccess(auth, { deviceId, agentId }) {
|
|
96
|
+
await portalFetch(auth, `/api/v1/portal/devices/${encodeURIComponent(deviceId)}/grants/${encodeURIComponent(agentId)}`, { method: "DELETE" });
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* The device row for a given machine identity. Matching is by `machineName`
|
|
100
|
+
* equality — the stable handle the machine registers under, not the display
|
|
101
|
+
* label.
|
|
102
|
+
*/
|
|
103
|
+
function findThisDevice(devices, machineName) {
|
|
104
|
+
return devices.find((device) => device.machineName === machineName) ?? null;
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* One-time grant migration onto the merged device. Earlier CLI builds
|
|
108
|
+
* registered a separate `<machineName>-cli` device, so a user's existing
|
|
109
|
+
* approvals hang off that row; the merged device would start with zero grants
|
|
110
|
+
* and every already-authorized agent would ask again. Copy any grant the
|
|
111
|
+
* merged device is missing (the grant endpoint upserts, so re-runs are
|
|
112
|
+
* no-ops). The legacy row is left in place — an old CLI build may still
|
|
113
|
+
* connect under it. Returns how many grants were copied.
|
|
114
|
+
*/
|
|
115
|
+
async function unifyLegacyCliGrants(auth, machineName) {
|
|
116
|
+
const { devices } = await fetchPortalDevices(auth);
|
|
117
|
+
const merged = findThisDevice(devices, machineName);
|
|
118
|
+
const legacy = findThisDevice(devices, `${machineName}-cli`);
|
|
119
|
+
if (!merged || !legacy) return 0;
|
|
120
|
+
const have = new Set(merged.grantedAgentIds);
|
|
121
|
+
const missing = legacy.grantedAgentIds.filter((id) => !have.has(id));
|
|
122
|
+
for (const agentId of missing) await grantPortalAccess(auth, {
|
|
123
|
+
deviceId: merged.id,
|
|
124
|
+
agentId,
|
|
125
|
+
conversationId: null
|
|
126
|
+
});
|
|
127
|
+
return missing.length;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
//#endregion
|
|
131
|
+
export { mintPortalDeviceToken as a, unifyLegacyCliGrants as c, grantPortalAccess as i, fetchPortalDevices as n, registerPortalDevice as o, findThisDevice as r, revokePortalAccess as s, fetchForwardTarget as t };
|
package/dist/js/bin.mjs
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { C as ensureActiveOrganization, D as setActiveWorkspace, E as listWorkspaces, O as name, T as getSessionIdentity, a as installCrashHandler, k as version, t as maybeStartProfiling, u as brandHelpArt, w as getActiveWorkspaceId, x as themes } from "./profiler-
|
|
2
|
+
import { C as ensureActiveOrganization, D as setActiveWorkspace, E as listWorkspaces, O as name, T as getSessionIdentity, a as installCrashHandler, k as version, t as maybeStartProfiling, u as brandHelpArt, w as getActiveWorkspaceId, x as themes } from "./profiler-DawY0V0Z.mjs";
|
|
3
3
|
import { A as getStoredApiKeyWorkspaceName, C as getLastSeenVersion, F as resolveManagementAuth, I as resolveSession, M as resolveAppUrl, N as resolveChatAuth, O as getShareMachineDefault, P as resolveConfig, R as saveConfig, S as getConfigPath, T as getPromptHistoryPath, V as setLastSeenVersion, _ as API_KEY_PREFIX, b as PREFERENCES, g as API_KEY_FAMILY_PREFIX, h as API_KEYS_URL, i as resolveAgent, j as getUpdateCheckDisabled, k as getStoredApiKeyId, v as DEFAULT_API_URL, w as getPreference, x as deleteConfig, z as saveSession } from "./print-BpuyEfWX.mjs";
|
|
4
4
|
import { n as printError, r as printTable, t as output } from "./output-DYzzdXYV.mjs";
|
|
5
5
|
import { t as HttpError } from "./http-error-DzyrsLAZ.mjs";
|
|
6
6
|
import { t as createRestClient } from "./rest-I3imNduB.mjs";
|
|
7
7
|
import "./billing-blocked-2wju4gC_.mjs";
|
|
8
|
-
import {
|
|
9
|
-
import { i as queryDaemonStatus, l as PORTAL_DAEMON_FLAG, n as ensureDaemonRunning, s as stopDaemon, u as daemonPaths } from "./daemon-
|
|
10
|
-
import {
|
|
8
|
+
import { r as resolveMachineIdentity } from "./client-DabRpc_T.mjs";
|
|
9
|
+
import { i as queryDaemonStatus, l as PORTAL_DAEMON_FLAG, n as ensureDaemonRunning, s as stopDaemon, u as daemonPaths } from "./daemon-k2kVkJ8D.mjs";
|
|
10
|
+
import { i as grantPortalAccess, n as fetchPortalDevices, o as registerPortalDevice, r as findThisDevice, s as revokePortalAccess } from "./api-DG5W6iwx.mjs";
|
|
11
|
+
import { t as SandboxStream } from "./client-c4c5MmgN.mjs";
|
|
11
12
|
import { hideBin } from "yargs/helpers";
|
|
12
13
|
import yargs from "yargs";
|
|
13
14
|
import os, { hostname, tmpdir } from "node:os";
|
|
@@ -1516,7 +1517,7 @@ const importCommand = {
|
|
|
1516
1517
|
process.exit(1);
|
|
1517
1518
|
}
|
|
1518
1519
|
}
|
|
1519
|
-
const { runChat } = await import("./boot-
|
|
1520
|
+
const { runChat } = await import("./boot-BCClEpCM.mjs");
|
|
1520
1521
|
await runChat({
|
|
1521
1522
|
appUrl,
|
|
1522
1523
|
sessionToken: session.value.sessionToken,
|
|
@@ -1543,7 +1544,7 @@ async function runImportPrintMode({ argv, appUrl }) {
|
|
|
1543
1544
|
printError(`${session.error.message} For non-interactive use, run \`skydive auth login --web\` first, or set SKYDIVE_SESSION_TOKEN.`);
|
|
1544
1545
|
process.exit(1);
|
|
1545
1546
|
}
|
|
1546
|
-
const { connectMachineShare } = await import("./print-share-
|
|
1547
|
+
const { connectMachineShare } = await import("./print-share-CKLPmsg0.mjs");
|
|
1547
1548
|
const machineShare = await connectMachineShare({
|
|
1548
1549
|
appUrl,
|
|
1549
1550
|
sessionToken: session.value.sessionToken,
|
|
@@ -1928,7 +1929,7 @@ const chatCommand = {
|
|
|
1928
1929
|
sessionToken: auth.value.token,
|
|
1929
1930
|
agentSelector: argv.agent ?? null
|
|
1930
1931
|
});
|
|
1931
|
-
const { runChat } = await import("./boot-
|
|
1932
|
+
const { runChat } = await import("./boot-BCClEpCM.mjs");
|
|
1932
1933
|
await runChat({
|
|
1933
1934
|
appUrl: auth.value.appUrl,
|
|
1934
1935
|
sessionToken: auth.value.token,
|
|
@@ -2020,7 +2021,7 @@ async function runPrintMode({ argv, appUrl }) {
|
|
|
2020
2021
|
process.exit(1);
|
|
2021
2022
|
}
|
|
2022
2023
|
} else {
|
|
2023
|
-
const { connectMachineShare } = await import("./print-share-
|
|
2024
|
+
const { connectMachineShare } = await import("./print-share-CKLPmsg0.mjs");
|
|
2024
2025
|
machineShare = await connectMachineShare({
|
|
2025
2026
|
appUrl: auth.value.appUrl,
|
|
2026
2027
|
sessionToken: auth.value.token,
|
|
@@ -2287,7 +2288,7 @@ const switchCommand = {
|
|
|
2287
2288
|
printError("The workspace picker needs the Bun runtime and it could not be set up automatically. Pass a workspace slug instead, or install Bun and retry.");
|
|
2288
2289
|
process.exit(1);
|
|
2289
2290
|
}
|
|
2290
|
-
const { runWorkspacePicker } = await import("./boot-
|
|
2291
|
+
const { runWorkspacePicker } = await import("./boot-BCClEpCM.mjs");
|
|
2291
2292
|
await runWorkspacePicker(session);
|
|
2292
2293
|
return;
|
|
2293
2294
|
}
|
|
@@ -2366,7 +2367,7 @@ const openCommand = {
|
|
|
2366
2367
|
const agent = argv.agent ? resolveAgent((await fetchPortalDevices(session)).agents, argv.agent) : null;
|
|
2367
2368
|
const cwd = argv.cwd ? path.resolve(argv.cwd) : process.cwd();
|
|
2368
2369
|
const { machineName } = await resolveMachineIdentity();
|
|
2369
|
-
const { PortalDaemonClient } = await import("./daemon-client-
|
|
2370
|
+
const { PortalDaemonClient } = await import("./daemon-client-C2BvZgKO.mjs");
|
|
2370
2371
|
let lastLine = "";
|
|
2371
2372
|
let signalConnected;
|
|
2372
2373
|
const connected = new Promise((resolve) => {
|
|
@@ -2593,10 +2594,45 @@ const daemonCommand = {
|
|
|
2593
2594
|
builder: (y) => y.command(daemonStatusCommand).command(daemonStopCommand).command(daemonStartCommand).command(daemonRestartCommand).command(daemonLogsCommand).demandCommand(1, "Specify a subcommand: status, stop, start, restart, logs"),
|
|
2594
2595
|
handler: () => {}
|
|
2595
2596
|
};
|
|
2597
|
+
const forwardCommand = {
|
|
2598
|
+
command: "forward <port>",
|
|
2599
|
+
describe: "Forward localhost:<port> to the agent sandbox's port, so a dev server running in the sandbox is reachable at http://localhost:<port> (stays in the foreground; ctrl+c to stop)",
|
|
2600
|
+
builder: (y) => y.positional("port", {
|
|
2601
|
+
type: "number",
|
|
2602
|
+
demandOption: true,
|
|
2603
|
+
describe: "Sandbox port to forward to (e.g. 3000 for a dev server)"
|
|
2604
|
+
}).option("agent", {
|
|
2605
|
+
type: "string",
|
|
2606
|
+
describe: "Agent, by id or name (defaults when the account has one)"
|
|
2607
|
+
}).option("local-port", {
|
|
2608
|
+
type: "number",
|
|
2609
|
+
describe: "Local port to listen on (default: same as <port>)"
|
|
2610
|
+
}),
|
|
2611
|
+
handler: async (argv) => {
|
|
2612
|
+
const session = requireSession(argv);
|
|
2613
|
+
const agent = resolveAgent((await fetchPortalDevices(session)).agents, argv.agent ?? null);
|
|
2614
|
+
const targetPort = argv.port;
|
|
2615
|
+
const localPort = argv["local-port"] ?? targetPort;
|
|
2616
|
+
const { startForward } = await import("./forward-18QoL5dO.mjs");
|
|
2617
|
+
const listener = await startForward({
|
|
2618
|
+
auth: session,
|
|
2619
|
+
agentId: agent.id,
|
|
2620
|
+
localPort,
|
|
2621
|
+
targetPort,
|
|
2622
|
+
log: (msg) => console.error(msg)
|
|
2623
|
+
});
|
|
2624
|
+
console.log(`Forwarding http://localhost:${listener.port} -> ${agent.name}'s sandbox port ${targetPort}. ctrl+c to stop.`);
|
|
2625
|
+
await new Promise((resolve) => {
|
|
2626
|
+
process.once("SIGINT", () => resolve());
|
|
2627
|
+
process.once("SIGTERM", () => resolve());
|
|
2628
|
+
});
|
|
2629
|
+
await listener.close();
|
|
2630
|
+
}
|
|
2631
|
+
};
|
|
2596
2632
|
const portalCommand = {
|
|
2597
2633
|
command: "portal",
|
|
2598
2634
|
describe: "Open this machine's portal to agents and manage their access",
|
|
2599
|
-
builder: (y) => y.command(openCommand).command(grantCommand).command(revokeCommand).command(statusCommand).command(daemonCommand).demandCommand(1, "Specify a subcommand: open, grant, revoke, status, daemon"),
|
|
2635
|
+
builder: (y) => y.command(openCommand).command(grantCommand).command(revokeCommand).command(statusCommand).command(forwardCommand).command(daemonCommand).demandCommand(1, "Specify a subcommand: open, grant, revoke, status, forward, daemon"),
|
|
2600
2636
|
handler: () => {}
|
|
2601
2637
|
};
|
|
2602
2638
|
|
|
@@ -2711,7 +2747,7 @@ async function runPty({ session, agentId, agentName }) {
|
|
|
2711
2747
|
return 1;
|
|
2712
2748
|
}
|
|
2713
2749
|
console.error(`Connecting to ${agentName}'s sandbox… (Ctrl-] detaches)`);
|
|
2714
|
-
const { runRawPtyPassthrough } = await import("./raw-pty-
|
|
2750
|
+
const { runRawPtyPassthrough } = await import("./raw-pty-DmdUf4_w.mjs");
|
|
2715
2751
|
const result = await runRawPtyPassthrough({
|
|
2716
2752
|
stdin: process.stdin,
|
|
2717
2753
|
stdout: process.stdout,
|
|
@@ -4170,7 +4206,7 @@ if (process.argv.includes(UPDATE_WORKER_FLAG)) {
|
|
|
4170
4206
|
process.exit(0);
|
|
4171
4207
|
}
|
|
4172
4208
|
if (process.argv.includes(PORTAL_DAEMON_FLAG)) {
|
|
4173
|
-
const { runPortalDaemon } = await import("./daemon-
|
|
4209
|
+
const { runPortalDaemon } = await import("./daemon-BxU59xie.mjs");
|
|
4174
4210
|
runPortalDaemon(process.argv);
|
|
4175
4211
|
} else runCli();
|
|
4176
4212
|
function runCli() {
|
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { D as setActiveWorkspace, E as listWorkspaces, S as themesForMode, _ as themeForMode, a as installCrashHandler, b as themeVersion, c as MARK_CELLS, d as DEFAULT_THEME_ID, f as applyTheme, g as theme, h as noColorRequested, i as writeArtifact, l as WORDMARK, m as monoTheme, n as profilingEnabled, o as buildCrashReport, p as findTheme, r as record, s as writeCrashReport, v as themeMode, w as getActiveWorkspaceId, y as themeModeFromColorFgBg } from "./profiler-
|
|
2
|
+
import { D as setActiveWorkspace, E as listWorkspaces, S as themesForMode, _ as themeForMode, a as installCrashHandler, b as themeVersion, c as MARK_CELLS, d as DEFAULT_THEME_ID, f as applyTheme, g as theme, h as noColorRequested, i as writeArtifact, l as WORDMARK, m as monoTheme, n as profilingEnabled, o as buildCrashReport, p as findTheme, r as record, s as writeCrashReport, v as themeMode, w as getActiveWorkspaceId, y as themeModeFromColorFgBg } from "./profiler-DawY0V0Z.mjs";
|
|
3
3
|
import { B as saveTheme, D as getSavedTheme, E as getReviewStateDir, L as resolveWebUrl, S as getConfigPath, c as cardActionErrorMessage, d as reconcileMaskedInput, f as resolveConnectUrl, i as resolveAgent, l as parseExternalOauthConnectParams, m as specKeyFor, p as parseConnectCard, s as MASK_CHAR, u as parseOauthConnectParams, v as DEFAULT_API_URL, y as DEFAULT_APP_URL } from "./print-BpuyEfWX.mjs";
|
|
4
4
|
import { t as HttpError } from "./http-error-DzyrsLAZ.mjs";
|
|
5
5
|
import { a as isRecord, i as errorMessage, n as errorDetail, r as sendErrorMessage, t as createRestClient } from "./rest-I3imNduB.mjs";
|
|
6
6
|
import { i as billingBlockedOutcomeFromSendResponse } from "./billing-blocked-2wju4gC_.mjs";
|
|
7
|
-
import { t as PortalClient } from "./client-
|
|
8
|
-
import "./daemon-
|
|
9
|
-
import
|
|
10
|
-
import { t as
|
|
11
|
-
import { t as
|
|
7
|
+
import { t as PortalClient } from "./client-DabRpc_T.mjs";
|
|
8
|
+
import "./daemon-k2kVkJ8D.mjs";
|
|
9
|
+
import "./api-DG5W6iwx.mjs";
|
|
10
|
+
import { t as SandboxStream } from "./client-c4c5MmgN.mjs";
|
|
11
|
+
import { t as PortalDaemonClient } from "./daemon-client-fxf1A25Z.mjs";
|
|
12
|
+
import { t as runRawPtyPassthrough } from "./raw-pty-DY4KelZW.mjs";
|
|
12
13
|
import * as os$1 from "node:os";
|
|
13
14
|
import { homedir, platform, release, tmpdir } from "node:os";
|
|
14
15
|
import path, { basename, extname, isAbsolute, join, win32 } from "node:path";
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
2
|
+
import { a as mintPortalDeviceToken, c as unifyLegacyCliGrants, i as grantPortalAccess, n as fetchPortalDevices, r as findThisDevice } from "./api-DG5W6iwx.mjs";
|
|
3
3
|
import os from "node:os";
|
|
4
4
|
import { z } from "zod";
|
|
5
5
|
import { execFile, spawn } from "node:child_process";
|
|
6
|
-
import
|
|
6
|
+
import net from "node:net";
|
|
7
|
+
import { WebSocket, createWebSocketStream } from "ws";
|
|
7
8
|
|
|
8
9
|
//#region ../portal-daemon/src/machine.ts
|
|
9
10
|
/**
|
|
@@ -134,6 +135,18 @@ const ctrlMessageSchema = z.discriminatedUnion("t", [
|
|
|
134
135
|
conversationId: z.string().nullable().optional()
|
|
135
136
|
}),
|
|
136
137
|
z.object({ t: z.literal("stdin_eof") }),
|
|
138
|
+
z.object({
|
|
139
|
+
t: z.literal("reverse_listen"),
|
|
140
|
+
listenPort: z.number().int().positive(),
|
|
141
|
+
targetPort: z.number().int().positive(),
|
|
142
|
+
dialOrigin: z.string().min(1),
|
|
143
|
+
token: z.string().min(1)
|
|
144
|
+
}),
|
|
145
|
+
z.object({ t: z.literal("reverse_listening") }),
|
|
146
|
+
z.object({
|
|
147
|
+
t: z.literal("reverse_token"),
|
|
148
|
+
token: z.string().min(1)
|
|
149
|
+
}),
|
|
137
150
|
z.object({ t: z.literal("pause") }),
|
|
138
151
|
z.object({ t: z.literal("resume") }),
|
|
139
152
|
z.object({ t: z.literal("cancel") }),
|
|
@@ -169,6 +182,62 @@ function decodeFrame(frame) {
|
|
|
169
182
|
};
|
|
170
183
|
}
|
|
171
184
|
|
|
185
|
+
//#endregion
|
|
186
|
+
//#region ../portal-daemon/src/reverse-listener.ts
|
|
187
|
+
/**
|
|
188
|
+
* The desktop half of an agent-initiated expose tunnel (`platform portal
|
|
189
|
+
* expose`): listen on the local loopback and pipe each accepted TCP
|
|
190
|
+
* connection to the agent sandbox's daemon (`/portal/tcp`) through the
|
|
191
|
+
* agent-webserver edge Worker — the same per-connection dial-and-pipe as the
|
|
192
|
+
* user-initiated `skydive portal forward`, just started from a directive
|
|
193
|
+
* instead of a terminal. Tunnel bytes never touch the directive stream that
|
|
194
|
+
* created this listener; it only anchors the lifetime and carries the token.
|
|
195
|
+
*
|
|
196
|
+
* `target()` is read per connection so a token refresh (or any future
|
|
197
|
+
* retarget) applies to the next dial without touching established pipes.
|
|
198
|
+
*/
|
|
199
|
+
var ReverseListener = class {
|
|
200
|
+
server = null;
|
|
201
|
+
conns = /* @__PURE__ */ new Set();
|
|
202
|
+
constructor(opts) {
|
|
203
|
+
this.opts = opts;
|
|
204
|
+
}
|
|
205
|
+
start() {
|
|
206
|
+
const server = net.createServer((sock) => {
|
|
207
|
+
this.conns.add(sock);
|
|
208
|
+
sock.once("close", () => this.conns.delete(sock));
|
|
209
|
+
sock.pause();
|
|
210
|
+
const { dialOrigin, targetPort, token } = this.opts.target();
|
|
211
|
+
const ws = new WebSocket(`${dialOrigin.replace(/^http/, "ws")}/portal/tcp?port=${targetPort}`, { headers: { authorization: `Bearer ${token}` } });
|
|
212
|
+
ws.on("open", () => {
|
|
213
|
+
const stream = createWebSocketStream(ws);
|
|
214
|
+
stream.on("error", () => sock.destroy());
|
|
215
|
+
sock.on("error", () => stream.destroy());
|
|
216
|
+
sock.pipe(stream).pipe(sock);
|
|
217
|
+
sock.resume();
|
|
218
|
+
});
|
|
219
|
+
ws.on("error", (err) => {
|
|
220
|
+
this.opts.log(`expose: tunnel connect failed: ${err.message}`);
|
|
221
|
+
sock.destroy();
|
|
222
|
+
});
|
|
223
|
+
ws.on("unexpected-response", (_req, res) => {
|
|
224
|
+
this.opts.log(`expose: tunnel rejected (HTTP ${res.statusCode ?? "?"})`);
|
|
225
|
+
res.destroy();
|
|
226
|
+
sock.destroy();
|
|
227
|
+
});
|
|
228
|
+
});
|
|
229
|
+
server.once("error", (err) => this.opts.onListening(err));
|
|
230
|
+
server.listen(this.opts.listenPort, "127.0.0.1", () => this.opts.onListening(null));
|
|
231
|
+
this.server = server;
|
|
232
|
+
}
|
|
233
|
+
stop() {
|
|
234
|
+
this.server?.close();
|
|
235
|
+
this.server = null;
|
|
236
|
+
for (const sock of this.conns) sock.destroy();
|
|
237
|
+
this.conns.clear();
|
|
238
|
+
}
|
|
239
|
+
};
|
|
240
|
+
|
|
172
241
|
//#endregion
|
|
173
242
|
//#region ../portal-daemon/src/exec.ts
|
|
174
243
|
/**
|
|
@@ -181,6 +250,7 @@ function decodeFrame(frame) {
|
|
|
181
250
|
*/
|
|
182
251
|
var JobManager = class {
|
|
183
252
|
jobs = /* @__PURE__ */ new Map();
|
|
253
|
+
reverse = /* @__PURE__ */ new Map();
|
|
184
254
|
constructor(opts) {
|
|
185
255
|
this.opts = opts;
|
|
186
256
|
}
|
|
@@ -200,12 +270,35 @@ var JobManager = class {
|
|
|
200
270
|
job.child.kill("SIGKILL");
|
|
201
271
|
}
|
|
202
272
|
this.jobs.clear();
|
|
273
|
+
for (const rev of this.reverse.values()) {
|
|
274
|
+
rev.settled = true;
|
|
275
|
+
rev.listener.stop();
|
|
276
|
+
}
|
|
277
|
+
this.reverse.clear();
|
|
203
278
|
}
|
|
204
279
|
handleCtrl(id, msg) {
|
|
205
280
|
switch (msg.t) {
|
|
206
281
|
case "open":
|
|
207
282
|
this.startJob(id, msg.argv, msg.env, msg.conversationId ?? null);
|
|
208
283
|
return;
|
|
284
|
+
case "reverse_listen":
|
|
285
|
+
this.startReverse(id, {
|
|
286
|
+
listenPort: msg.listenPort,
|
|
287
|
+
target: {
|
|
288
|
+
dialOrigin: msg.dialOrigin,
|
|
289
|
+
targetPort: msg.targetPort,
|
|
290
|
+
token: msg.token
|
|
291
|
+
}
|
|
292
|
+
});
|
|
293
|
+
return;
|
|
294
|
+
case "reverse_token": {
|
|
295
|
+
const rev = this.reverse.get(id);
|
|
296
|
+
if (rev) rev.target = {
|
|
297
|
+
...rev.target,
|
|
298
|
+
token: msg.token
|
|
299
|
+
};
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
209
302
|
case "stdin_eof":
|
|
210
303
|
this.jobs.get(id)?.child.stdin.end();
|
|
211
304
|
return;
|
|
@@ -217,12 +310,51 @@ var JobManager = class {
|
|
|
217
310
|
return;
|
|
218
311
|
case "cancel":
|
|
219
312
|
this.jobs.get(id)?.child.kill("SIGKILL");
|
|
313
|
+
this.stopReverse(id);
|
|
220
314
|
return;
|
|
221
315
|
case "close":
|
|
222
|
-
case "error":
|
|
316
|
+
case "error":
|
|
317
|
+
case "reverse_listening": return;
|
|
223
318
|
default: return msg;
|
|
224
319
|
}
|
|
225
320
|
}
|
|
321
|
+
startReverse(id, { listenPort, target }) {
|
|
322
|
+
const rev = {
|
|
323
|
+
settled: false,
|
|
324
|
+
target,
|
|
325
|
+
seq: 0,
|
|
326
|
+
listener: new ReverseListener({
|
|
327
|
+
listenPort,
|
|
328
|
+
target: () => rev.target,
|
|
329
|
+
log: (msg) => {
|
|
330
|
+
if (rev.settled) return;
|
|
331
|
+
this.opts.send(encodeData(id, STREAM.stderr, rev.seq, Buffer.from(msg)));
|
|
332
|
+
rev.seq = rev.seq + 1 >>> 0;
|
|
333
|
+
},
|
|
334
|
+
onListening: (err) => {
|
|
335
|
+
if (err) {
|
|
336
|
+
rev.settled = true;
|
|
337
|
+
this.reverse.delete(id);
|
|
338
|
+
this.opts.send(encodeCtrl(id, {
|
|
339
|
+
t: "error",
|
|
340
|
+
message: `reverse listen failed: ${err.message}`
|
|
341
|
+
}));
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
this.opts.send(encodeCtrl(id, { t: "reverse_listening" }));
|
|
345
|
+
}
|
|
346
|
+
})
|
|
347
|
+
};
|
|
348
|
+
this.reverse.set(id, rev);
|
|
349
|
+
rev.listener.start();
|
|
350
|
+
}
|
|
351
|
+
stopReverse(id) {
|
|
352
|
+
const rev = this.reverse.get(id);
|
|
353
|
+
if (!rev) return;
|
|
354
|
+
rev.settled = true;
|
|
355
|
+
this.reverse.delete(id);
|
|
356
|
+
rev.listener.stop();
|
|
357
|
+
}
|
|
226
358
|
setPaused(id, paused) {
|
|
227
359
|
const job = this.jobs.get(id);
|
|
228
360
|
if (!job) return;
|
|
@@ -296,115 +428,6 @@ var JobManager = class {
|
|
|
296
428
|
}
|
|
297
429
|
};
|
|
298
430
|
|
|
299
|
-
//#endregion
|
|
300
|
-
//#region ../portal-daemon/src/api.ts
|
|
301
|
-
/**
|
|
302
|
-
* The portal's session-authed REST surface, shared by `PortalClient` (the
|
|
303
|
-
* TUI/`portal open` connection) and the `skydive portal` management
|
|
304
|
-
* commands, so the endpoint contracts and response schemas live in exactly
|
|
305
|
-
* one place.
|
|
306
|
-
*/
|
|
307
|
-
const deviceSchema = z.object({
|
|
308
|
-
id: z.string(),
|
|
309
|
-
machineName: z.string(),
|
|
310
|
-
friendlyName: z.string(),
|
|
311
|
-
connected: z.boolean(),
|
|
312
|
-
lastSeen: z.string().nullable(),
|
|
313
|
-
grantedAgentIds: z.array(z.string())
|
|
314
|
-
});
|
|
315
|
-
const devicesResponseSchema = z.object({
|
|
316
|
-
devices: z.array(deviceSchema),
|
|
317
|
-
agents: z.array(z.object({
|
|
318
|
-
id: z.string(),
|
|
319
|
-
name: z.string()
|
|
320
|
-
}))
|
|
321
|
-
});
|
|
322
|
-
const deviceTokenSchema = z.object({ token: z.string().min(1) });
|
|
323
|
-
async function portalFetch(auth, path, init) {
|
|
324
|
-
const res = await fetch(`${auth.appUrl}${path}`, {
|
|
325
|
-
method: init.method,
|
|
326
|
-
headers: {
|
|
327
|
-
authorization: `Bearer ${auth.sessionToken}`,
|
|
328
|
-
accept: "application/json",
|
|
329
|
-
...init.body ? { "content-type": "application/json" } : {}
|
|
330
|
-
},
|
|
331
|
-
...init.body ? { body: init.body } : {}
|
|
332
|
-
});
|
|
333
|
-
if (!res.ok) {
|
|
334
|
-
const body = await res.text().catch(() => "");
|
|
335
|
-
throw new HttpError(res.status, body);
|
|
336
|
-
}
|
|
337
|
-
return res.json();
|
|
338
|
-
}
|
|
339
|
-
async function fetchPortalDevices(auth) {
|
|
340
|
-
const json = await portalFetch(auth, "/api/v1/portal/devices", { method: "GET" });
|
|
341
|
-
return devicesResponseSchema.parse(json);
|
|
342
|
-
}
|
|
343
|
-
const registerResponseSchema = z.object({ device: z.object({ id: z.string() }) });
|
|
344
|
-
/**
|
|
345
|
-
* Register this machine's device row without connecting. Connecting registers
|
|
346
|
-
* as a side effect; this covers granting an agent on a machine that has never
|
|
347
|
-
* shared yet (the grant references the device row).
|
|
348
|
-
*/
|
|
349
|
-
async function registerPortalDevice(auth, { machineName, friendlyName }) {
|
|
350
|
-
const json = await portalFetch(auth, "/api/v1/portal/devices", {
|
|
351
|
-
method: "POST",
|
|
352
|
-
body: JSON.stringify({
|
|
353
|
-
machineName,
|
|
354
|
-
friendlyName
|
|
355
|
-
})
|
|
356
|
-
});
|
|
357
|
-
return registerResponseSchema.parse(json).device;
|
|
358
|
-
}
|
|
359
|
-
/** Short-lived token the machine presents when dialing the portal WebSocket. */
|
|
360
|
-
async function mintPortalDeviceToken(auth) {
|
|
361
|
-
const json = await portalFetch(auth, "/api/v1/portal/device-token", { method: "POST" });
|
|
362
|
-
return deviceTokenSchema.parse(json).token;
|
|
363
|
-
}
|
|
364
|
-
async function grantPortalAccess(auth, { deviceId, agentId, conversationId }) {
|
|
365
|
-
await portalFetch(auth, `/api/v1/portal/devices/${encodeURIComponent(deviceId)}/grants`, {
|
|
366
|
-
method: "POST",
|
|
367
|
-
body: JSON.stringify({
|
|
368
|
-
agentId,
|
|
369
|
-
conversationId
|
|
370
|
-
})
|
|
371
|
-
});
|
|
372
|
-
}
|
|
373
|
-
async function revokePortalAccess(auth, { deviceId, agentId }) {
|
|
374
|
-
await portalFetch(auth, `/api/v1/portal/devices/${encodeURIComponent(deviceId)}/grants/${encodeURIComponent(agentId)}`, { method: "DELETE" });
|
|
375
|
-
}
|
|
376
|
-
/**
|
|
377
|
-
* The device row for a given machine identity. Matching is by `machineName`
|
|
378
|
-
* equality — the stable handle the machine registers under, not the display
|
|
379
|
-
* label.
|
|
380
|
-
*/
|
|
381
|
-
function findThisDevice(devices, machineName) {
|
|
382
|
-
return devices.find((device) => device.machineName === machineName) ?? null;
|
|
383
|
-
}
|
|
384
|
-
/**
|
|
385
|
-
* One-time grant migration onto the merged device. Earlier CLI builds
|
|
386
|
-
* registered a separate `<machineName>-cli` device, so a user's existing
|
|
387
|
-
* approvals hang off that row; the merged device would start with zero grants
|
|
388
|
-
* and every already-authorized agent would ask again. Copy any grant the
|
|
389
|
-
* merged device is missing (the grant endpoint upserts, so re-runs are
|
|
390
|
-
* no-ops). The legacy row is left in place — an old CLI build may still
|
|
391
|
-
* connect under it. Returns how many grants were copied.
|
|
392
|
-
*/
|
|
393
|
-
async function unifyLegacyCliGrants(auth, machineName) {
|
|
394
|
-
const { devices } = await fetchPortalDevices(auth);
|
|
395
|
-
const merged = findThisDevice(devices, machineName);
|
|
396
|
-
const legacy = findThisDevice(devices, `${machineName}-cli`);
|
|
397
|
-
if (!merged || !legacy) return 0;
|
|
398
|
-
const have = new Set(merged.grantedAgentIds);
|
|
399
|
-
const missing = legacy.grantedAgentIds.filter((id) => !have.has(id));
|
|
400
|
-
for (const agentId of missing) await grantPortalAccess(auth, {
|
|
401
|
-
deviceId: merged.id,
|
|
402
|
-
agentId,
|
|
403
|
-
conversationId: null
|
|
404
|
-
});
|
|
405
|
-
return missing.length;
|
|
406
|
-
}
|
|
407
|
-
|
|
408
431
|
//#endregion
|
|
409
432
|
//#region ../portal-daemon/src/client.ts
|
|
410
433
|
const INITIAL_BACKOFF_MS = 500;
|
|
@@ -627,4 +650,4 @@ function sleep(ms) {
|
|
|
627
650
|
}
|
|
628
651
|
|
|
629
652
|
//#endregion
|
|
630
|
-
export {
|
|
653
|
+
export { isRecord as n, resolveMachineIdentity as r, PortalClient as t };
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import "./client-
|
|
3
|
-
import { a as runPortalDaemon, i as queryDaemonStatus, n as ensureDaemonRunning, o as startPortalDaemon, r as isDaemonListening, s as stopDaemon, t as PortalDaemon } from "./daemon-
|
|
2
|
+
import "./client-DabRpc_T.mjs";
|
|
3
|
+
import { a as runPortalDaemon, i as queryDaemonStatus, n as ensureDaemonRunning, o as startPortalDaemon, r as isDaemonListening, s as stopDaemon, t as PortalDaemon } from "./daemon-k2kVkJ8D.mjs";
|
|
4
|
+
import "./api-DG5W6iwx.mjs";
|
|
4
5
|
|
|
5
6
|
export { runPortalDaemon };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { c as LOCAL_PROTOCOL_VERSION, d as encodeLine, f as makeLineParser, n as ensureDaemonRunning, p as parseDaemonMessage, u as daemonPaths } from "./daemon-
|
|
2
|
+
import { c as LOCAL_PROTOCOL_VERSION, d as encodeLine, f as makeLineParser, n as ensureDaemonRunning, p as parseDaemonMessage, u as daemonPaths } from "./daemon-k2kVkJ8D.mjs";
|
|
3
3
|
import { randomUUID } from "node:crypto";
|
|
4
4
|
import { connect } from "node:net";
|
|
5
5
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
2
|
+
import { n as isRecord, t as PortalClient } from "./client-DabRpc_T.mjs";
|
|
3
3
|
import os from "node:os";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { z } from "zod";
|
|
@@ -20,7 +20,7 @@ import { access, appendFile, mkdir, readFile, unlink, writeFile } from "node:fs/
|
|
|
20
20
|
* the commit time of the built tree is one monotonic clock they share.
|
|
21
21
|
*/
|
|
22
22
|
function portalDaemonBuild() {
|
|
23
|
-
return "
|
|
23
|
+
return "1786596877";
|
|
24
24
|
}
|
|
25
25
|
/**
|
|
26
26
|
* Whether a client carrying `mine` should replace a running daemon carrying
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { t as fetchForwardTarget } from "./api-DG5W6iwx.mjs";
|
|
3
|
+
import net from "node:net";
|
|
4
|
+
import { WebSocket, createWebSocketStream } from "ws";
|
|
5
|
+
|
|
6
|
+
//#region src/chat/portal/forward.ts
|
|
7
|
+
const TARGET_REFRESH_SAFETY_MS = 12e4;
|
|
8
|
+
/**
|
|
9
|
+
* The reverse portal's client half: listen on the local machine's loopback and
|
|
10
|
+
* pipe each TCP connection to the agent sandbox's daemon (`/portal/tcp`),
|
|
11
|
+
* which pipes to the sandbox's own loopback. The daemon is reached through the
|
|
12
|
+
* agent-webserver edge Worker (sandboxes have public ingress disabled; the
|
|
13
|
+
* Worker owns boot-resolution and injects the sandbox edge-auth token), so a
|
|
14
|
+
* sandbox recycle just costs the next connection a cold-start wait rather
|
|
15
|
+
* than invalidating the forward.
|
|
16
|
+
*/
|
|
17
|
+
async function startForward({ auth, agentId, localPort, targetPort, log }) {
|
|
18
|
+
let target = await fetchForwardTarget(auth, agentId);
|
|
19
|
+
let mintedAt = Date.now();
|
|
20
|
+
async function freshTarget() {
|
|
21
|
+
const ttlMs = target.expiresInSeconds * 1e3;
|
|
22
|
+
if (Date.now() - mintedAt > ttlMs - TARGET_REFRESH_SAFETY_MS) {
|
|
23
|
+
target = await fetchForwardTarget(auth, agentId);
|
|
24
|
+
mintedAt = Date.now();
|
|
25
|
+
}
|
|
26
|
+
return target;
|
|
27
|
+
}
|
|
28
|
+
const server = net.createServer((sock) => {
|
|
29
|
+
sock.pause();
|
|
30
|
+
(async () => {
|
|
31
|
+
let resolved;
|
|
32
|
+
try {
|
|
33
|
+
resolved = await freshTarget();
|
|
34
|
+
} catch (err) {
|
|
35
|
+
log(`forward: token refresh failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
36
|
+
sock.destroy();
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
const ws = new WebSocket(`${resolved.daemonOrigin.replace(/^http/, "ws")}/portal/tcp?port=${targetPort}`, { headers: { authorization: `Bearer ${resolved.token}` } });
|
|
40
|
+
ws.on("open", () => {
|
|
41
|
+
const stream = createWebSocketStream(ws);
|
|
42
|
+
stream.on("error", () => sock.destroy());
|
|
43
|
+
sock.on("error", () => stream.destroy());
|
|
44
|
+
sock.pipe(stream).pipe(sock);
|
|
45
|
+
sock.resume();
|
|
46
|
+
});
|
|
47
|
+
ws.on("error", (err) => {
|
|
48
|
+
log(`forward: tunnel connect failed: ${err.message}`);
|
|
49
|
+
sock.destroy();
|
|
50
|
+
});
|
|
51
|
+
})();
|
|
52
|
+
});
|
|
53
|
+
await new Promise((resolve, reject) => {
|
|
54
|
+
server.once("error", reject);
|
|
55
|
+
server.listen(localPort, "127.0.0.1", () => {
|
|
56
|
+
server.removeListener("error", reject);
|
|
57
|
+
resolve();
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
const addr = server.address();
|
|
61
|
+
return {
|
|
62
|
+
port: addr && typeof addr === "object" ? addr.port : localPort,
|
|
63
|
+
close: () => new Promise((resolve) => server.close(() => resolve()))
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
//#endregion
|
|
68
|
+
export { startForward };
|
|
@@ -14,7 +14,7 @@ import { n as printError } from "./output-DYzzdXYV.mjs";
|
|
|
14
14
|
* the reply (and to --json).
|
|
15
15
|
*/
|
|
16
16
|
async function connectMachineShare({ appUrl, sessionToken, timeoutHint }) {
|
|
17
|
-
const { PortalClient } = await import("./client-
|
|
17
|
+
const { PortalClient } = await import("./client-BuU34IVE.mjs");
|
|
18
18
|
let signalConnected;
|
|
19
19
|
const connected = new Promise((resolve) => {
|
|
20
20
|
signalConnected = resolve;
|
package/package.json
CHANGED
|
File without changes
|