@span-io/agent-link 0.1.3 → 0.1.5
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/bootstrap.js +192 -0
- package/dist/config.js +6 -0
- package/dist/index.js +24 -6
- package/dist/process-runner.js +35 -5
- package/dist/transport.js +41 -3
- package/package.json +1 -1
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import { spawn } from "child_process";
|
|
2
|
+
import process from "process";
|
|
3
|
+
import { mkdir } from "fs/promises";
|
|
4
|
+
const DEFAULT_TIMEOUT_MS = 10 * 60 * 1000;
|
|
5
|
+
const MAX_TIMEOUT_MS = 20 * 60 * 1000;
|
|
6
|
+
const DEFAULT_ALLOWED_COMMANDS = ["npx", "npm"];
|
|
7
|
+
const DEFAULT_ALLOWED_ENV = ["CI", "NODE_ENV", "NPM_CONFIG_YES"];
|
|
8
|
+
const MAX_ARGS = 128;
|
|
9
|
+
const MAX_ARG_LEN = 4096;
|
|
10
|
+
export function parseCsvList(value, fallback) {
|
|
11
|
+
if (!value || !value.trim()) {
|
|
12
|
+
return new Set(fallback);
|
|
13
|
+
}
|
|
14
|
+
const entries = value
|
|
15
|
+
.split(",")
|
|
16
|
+
.map((part) => part.trim())
|
|
17
|
+
.filter((part) => part.length > 0);
|
|
18
|
+
return new Set(entries.length > 0 ? entries : fallback);
|
|
19
|
+
}
|
|
20
|
+
export function sanitizeBootstrapEnv(input, allowed) {
|
|
21
|
+
if (!input) {
|
|
22
|
+
return {};
|
|
23
|
+
}
|
|
24
|
+
const sanitized = {};
|
|
25
|
+
for (const [key, value] of Object.entries(input)) {
|
|
26
|
+
if (!allowed.has(key)) {
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
if (typeof value !== "string") {
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
const trimmed = value.trim();
|
|
33
|
+
if (!trimmed || trimmed.length > MAX_ARG_LEN) {
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
sanitized[key] = trimmed;
|
|
37
|
+
}
|
|
38
|
+
return sanitized;
|
|
39
|
+
}
|
|
40
|
+
export function normalizeBootstrapPayload(payload, commandAllowlist, envAllowlist) {
|
|
41
|
+
const runId = typeof payload?.runId === "string" ? payload.runId.trim() : "";
|
|
42
|
+
if (!runId) {
|
|
43
|
+
return { ok: false, error: "Missing runId." };
|
|
44
|
+
}
|
|
45
|
+
const workingDirectory = typeof payload?.workingDirectory === "string" ? payload.workingDirectory.trim() : "";
|
|
46
|
+
if (!workingDirectory) {
|
|
47
|
+
return { ok: false, error: "Missing workingDirectory." };
|
|
48
|
+
}
|
|
49
|
+
const command = typeof payload?.command === "string" && payload.command.trim().length > 0
|
|
50
|
+
? payload.command.trim()
|
|
51
|
+
: "npx";
|
|
52
|
+
if (!commandAllowlist.has(command)) {
|
|
53
|
+
return {
|
|
54
|
+
ok: false,
|
|
55
|
+
error: `Bootstrap command \"${command}\" is not allowed.`,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
const argsRaw = Array.isArray(payload?.args) ? payload.args : [];
|
|
59
|
+
const args = argsRaw
|
|
60
|
+
.filter((arg) => typeof arg === "string")
|
|
61
|
+
.map((arg) => arg.trim())
|
|
62
|
+
.filter((arg) => arg.length > 0);
|
|
63
|
+
if (args.length > MAX_ARGS) {
|
|
64
|
+
return { ok: false, error: `Bootstrap args exceed max count (${MAX_ARGS}).` };
|
|
65
|
+
}
|
|
66
|
+
for (const arg of args) {
|
|
67
|
+
if (arg.length > MAX_ARG_LEN) {
|
|
68
|
+
return { ok: false, error: `Bootstrap arg exceeds max length (${MAX_ARG_LEN}).` };
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
const requestedTimeout = typeof payload?.timeoutMs === "number" && Number.isFinite(payload.timeoutMs)
|
|
72
|
+
? payload.timeoutMs
|
|
73
|
+
: DEFAULT_TIMEOUT_MS;
|
|
74
|
+
const timeoutMs = Math.min(Math.max(requestedTimeout, 1), MAX_TIMEOUT_MS);
|
|
75
|
+
const env = sanitizeBootstrapEnv(payload?.env, envAllowlist);
|
|
76
|
+
return {
|
|
77
|
+
ok: true,
|
|
78
|
+
value: {
|
|
79
|
+
runId,
|
|
80
|
+
workingDirectory,
|
|
81
|
+
command,
|
|
82
|
+
args,
|
|
83
|
+
timeoutMs,
|
|
84
|
+
env,
|
|
85
|
+
},
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
export class BootstrapRunner {
|
|
89
|
+
deps;
|
|
90
|
+
activeRunId = null;
|
|
91
|
+
commandAllowlist;
|
|
92
|
+
envAllowlist;
|
|
93
|
+
constructor(deps = {}, options = {}) {
|
|
94
|
+
this.deps = {
|
|
95
|
+
mkdirFn: deps.mkdirFn ?? (async (path) => {
|
|
96
|
+
await mkdir(path, { recursive: true });
|
|
97
|
+
}),
|
|
98
|
+
spawnFn: deps.spawnFn ??
|
|
99
|
+
((command, args, options) => spawn(command, args, options)),
|
|
100
|
+
};
|
|
101
|
+
this.commandAllowlist =
|
|
102
|
+
options.commandAllowlist ??
|
|
103
|
+
parseCsvList(process.env.AGENT_LINK_BOOTSTRAP_ALLOWED_COMMANDS, DEFAULT_ALLOWED_COMMANDS);
|
|
104
|
+
this.envAllowlist =
|
|
105
|
+
options.envAllowlist ??
|
|
106
|
+
parseCsvList(process.env.AGENT_LINK_BOOTSTRAP_ALLOWED_ENV, DEFAULT_ALLOWED_ENV);
|
|
107
|
+
}
|
|
108
|
+
async run(payload, transport) {
|
|
109
|
+
const normalized = normalizeBootstrapPayload(payload, this.commandAllowlist, this.envAllowlist);
|
|
110
|
+
if (!normalized.ok) {
|
|
111
|
+
const runId = typeof payload?.runId === "string" ? payload.runId.trim() : "";
|
|
112
|
+
if (runId) {
|
|
113
|
+
transport.sendBootstrap(runId, "error", normalized.error);
|
|
114
|
+
}
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
const request = normalized.value;
|
|
118
|
+
if (this.activeRunId) {
|
|
119
|
+
transport.sendBootstrap(request.runId, "error", `Bootstrap already running for run ${this.activeRunId}.`);
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
this.activeRunId = request.runId;
|
|
123
|
+
try {
|
|
124
|
+
await this.deps.mkdirFn(request.workingDirectory);
|
|
125
|
+
transport.sendBootstrap(request.runId, "started", `Running bootstrap command: ${request.command} ${request.args.join(" ")} (cwd=${request.workingDirectory})`);
|
|
126
|
+
await this.runProcess(request, transport);
|
|
127
|
+
}
|
|
128
|
+
catch (error) {
|
|
129
|
+
const message = error instanceof Error ? error.message : "unknown error";
|
|
130
|
+
transport.sendBootstrap(request.runId, "error", `Bootstrap failed: ${message}`);
|
|
131
|
+
}
|
|
132
|
+
finally {
|
|
133
|
+
if (this.activeRunId === request.runId) {
|
|
134
|
+
this.activeRunId = null;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
async runProcess(request, transport) {
|
|
139
|
+
await new Promise((resolve) => {
|
|
140
|
+
let child;
|
|
141
|
+
try {
|
|
142
|
+
child = this.deps.spawnFn(request.command, request.args, {
|
|
143
|
+
cwd: request.workingDirectory,
|
|
144
|
+
env: {
|
|
145
|
+
...process.env,
|
|
146
|
+
...request.env,
|
|
147
|
+
},
|
|
148
|
+
shell: false,
|
|
149
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
catch (error) {
|
|
153
|
+
const message = error instanceof Error ? error.message : "unknown error";
|
|
154
|
+
transport.sendBootstrap(request.runId, "error", `Bootstrap process error: ${message}`);
|
|
155
|
+
resolve();
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
let settled = false;
|
|
159
|
+
const complete = (status, message) => {
|
|
160
|
+
if (settled) {
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
settled = true;
|
|
164
|
+
clearTimeout(timer);
|
|
165
|
+
transport.sendBootstrap(request.runId, status, message);
|
|
166
|
+
resolve();
|
|
167
|
+
};
|
|
168
|
+
const timer = setTimeout(() => {
|
|
169
|
+
child.kill("SIGKILL");
|
|
170
|
+
complete("error", `Bootstrap timed out after ${request.timeoutMs}ms.`);
|
|
171
|
+
}, request.timeoutMs);
|
|
172
|
+
child.stdout?.setEncoding("utf8");
|
|
173
|
+
child.stderr?.setEncoding("utf8");
|
|
174
|
+
child.stdout?.on("data", (chunk) => {
|
|
175
|
+
transport.sendBootstrap(request.runId, "log", chunk);
|
|
176
|
+
});
|
|
177
|
+
child.stderr?.on("data", (chunk) => {
|
|
178
|
+
transport.sendBootstrap(request.runId, "log", chunk);
|
|
179
|
+
});
|
|
180
|
+
child.on("error", (error) => {
|
|
181
|
+
complete("error", `Bootstrap process error: ${error.message}`);
|
|
182
|
+
});
|
|
183
|
+
child.on("exit", (code, signal) => {
|
|
184
|
+
if (code === 0) {
|
|
185
|
+
complete("complete", "Bootstrap completed successfully.");
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
complete("error", `Bootstrap exited with code ${String(code ?? "null")} signal ${String(signal ?? "null")}.`);
|
|
189
|
+
});
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
}
|
package/dist/config.js
CHANGED
|
@@ -42,4 +42,10 @@ export function saveConfig(config) {
|
|
|
42
42
|
}
|
|
43
43
|
}
|
|
44
44
|
fs.writeFileSync(CONFIG_FILE, JSON.stringify(toSave, null, 2), "utf8");
|
|
45
|
+
try {
|
|
46
|
+
fs.chmodSync(CONFIG_FILE, 0o600);
|
|
47
|
+
}
|
|
48
|
+
catch (error) {
|
|
49
|
+
console.warn("Failed to set secure permissions on config file:", error);
|
|
50
|
+
}
|
|
45
51
|
}
|
package/dist/index.js
CHANGED
|
@@ -6,6 +6,7 @@ import { findAgentsOnPath, resolveAgentBinary } from "./agent.js";
|
|
|
6
6
|
import { spawnAgentProcess as spawnAgentProcessAdvanced } from "./process-runner.js";
|
|
7
7
|
import { LogBuffer } from "./log-buffer.js";
|
|
8
8
|
import { compactPrompt, resolvePromptCompactionPolicy } from "./prompt-compact.js";
|
|
9
|
+
import { BootstrapRunner } from "./bootstrap.js";
|
|
9
10
|
import { NoopTransport, WebSocketTransport } from "./transport.js";
|
|
10
11
|
const args = parseArgs(process.argv.slice(2));
|
|
11
12
|
if (args.list) {
|
|
@@ -45,6 +46,7 @@ const logBuffer = new LogBuffer();
|
|
|
45
46
|
// Update map to hold SpawnedProcess which includes the child
|
|
46
47
|
const activeAgents = new Map();
|
|
47
48
|
const promptPolicy = resolvePromptCompactionPolicy();
|
|
49
|
+
const bootstrapRunner = new BootstrapRunner();
|
|
48
50
|
let transport;
|
|
49
51
|
try {
|
|
50
52
|
transport = await createTransport({
|
|
@@ -61,6 +63,10 @@ catch (error) {
|
|
|
61
63
|
}
|
|
62
64
|
function handleControl(message) {
|
|
63
65
|
const { action, agentId, payload } = message;
|
|
66
|
+
if (action === "bootstrap") {
|
|
67
|
+
void bootstrapRunner.run(payload ?? {}, transport);
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
64
70
|
if (!agentId)
|
|
65
71
|
return;
|
|
66
72
|
switch (action) {
|
|
@@ -161,35 +167,47 @@ function handleControl(message) {
|
|
|
161
167
|
}
|
|
162
168
|
}
|
|
163
169
|
function setupAgentPiping(agentId, proc) {
|
|
170
|
+
const rawFlushSize = Number.parseInt(process.env.AGENT_LINK_STREAM_FLUSH_CHARS ?? "16384", 10);
|
|
171
|
+
const flushSize = Number.isFinite(rawFlushSize) && rawFlushSize > 0 ? rawFlushSize : 16384;
|
|
164
172
|
const setupStream = (stream, name) => {
|
|
165
173
|
if (!stream)
|
|
166
174
|
return;
|
|
167
175
|
let buffer = "";
|
|
168
176
|
stream.setEncoding("utf8");
|
|
177
|
+
const flushChunk = (chunk) => {
|
|
178
|
+
if (chunk.length > 0) {
|
|
179
|
+
transport.sendLog(agentId, name, chunk);
|
|
180
|
+
}
|
|
181
|
+
};
|
|
169
182
|
stream.on("data", (chunk) => {
|
|
170
183
|
buffer += chunk;
|
|
171
184
|
let index = buffer.indexOf("\n");
|
|
172
185
|
while (index >= 0) {
|
|
173
186
|
const line = buffer.slice(0, index + 1);
|
|
174
187
|
buffer = buffer.slice(index + 1);
|
|
175
|
-
|
|
188
|
+
flushChunk(line);
|
|
176
189
|
index = buffer.indexOf("\n");
|
|
177
190
|
}
|
|
191
|
+
// Prevent unbounded memory growth when tools stream without newlines.
|
|
192
|
+
while (buffer.length >= flushSize) {
|
|
193
|
+
const part = buffer.slice(0, flushSize);
|
|
194
|
+
buffer = buffer.slice(flushSize);
|
|
195
|
+
flushChunk(part);
|
|
196
|
+
}
|
|
178
197
|
});
|
|
179
198
|
stream.on("end", () => {
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
}
|
|
199
|
+
flushChunk(buffer);
|
|
200
|
+
buffer = "";
|
|
183
201
|
});
|
|
184
202
|
};
|
|
185
203
|
setupStream(proc.child.stdout, "stdout");
|
|
186
204
|
setupStream(proc.child.stderr, "stderr");
|
|
187
|
-
proc.child.on("exit", (
|
|
205
|
+
proc.child.on("exit", (_code, _signal) => {
|
|
188
206
|
transport.sendStatus(agentId, "exited");
|
|
189
207
|
activeAgents.delete(agentId);
|
|
190
208
|
});
|
|
191
209
|
proc.child.on("error", (error) => {
|
|
192
|
-
transport.sendLog(agentId, "stderr",
|
|
210
|
+
transport.sendLog(agentId, "stderr", "Process error: " + error.message + "\n");
|
|
193
211
|
transport.sendStatus(agentId, "error");
|
|
194
212
|
activeAgents.delete(agentId);
|
|
195
213
|
});
|
package/dist/process-runner.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { mkdirSync } from "fs";
|
|
2
|
+
import path from "path";
|
|
2
3
|
import { spawn } from "child_process";
|
|
3
4
|
const DEFAULT_CODEX_ARGS = "exec --skip-git-repo-check";
|
|
4
5
|
const DEFAULT_GEMINI_ARGS = "";
|
|
@@ -11,7 +12,32 @@ function splitArgs(raw) {
|
|
|
11
12
|
}
|
|
12
13
|
function shellQuote(value) {
|
|
13
14
|
// Simple single-quote wrapping for display purposes
|
|
14
|
-
return `'${value.replace(/'/g, `
|
|
15
|
+
return `'${value.replace(/'/g, `"'"'`)}'`;
|
|
16
|
+
}
|
|
17
|
+
function parseAllowedRoots() {
|
|
18
|
+
const raw = process.env.AGENT_LINK_ALLOWED_WORKDIR_ROOTS?.trim();
|
|
19
|
+
if (!raw) {
|
|
20
|
+
return [];
|
|
21
|
+
}
|
|
22
|
+
return raw
|
|
23
|
+
.split(",")
|
|
24
|
+
.map((entry) => entry.trim())
|
|
25
|
+
.filter(Boolean)
|
|
26
|
+
.map((entry) => path.resolve(entry));
|
|
27
|
+
}
|
|
28
|
+
function isUnderRoot(candidate, root) {
|
|
29
|
+
const rel = path.relative(root, candidate);
|
|
30
|
+
return rel === "" || (!rel.startsWith("..") && !path.isAbsolute(rel));
|
|
31
|
+
}
|
|
32
|
+
function assertAllowedDirectory(directory, allowedRoots, label) {
|
|
33
|
+
if (allowedRoots.length === 0) {
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
const resolved = path.resolve(directory);
|
|
37
|
+
const allowed = allowedRoots.some((root) => isUnderRoot(resolved, root));
|
|
38
|
+
if (!allowed) {
|
|
39
|
+
throw new Error(`${label} '${resolved}' is outside AGENT_LINK_ALLOWED_WORKDIR_ROOTS (${allowedRoots.join(", ")}).`);
|
|
40
|
+
}
|
|
15
41
|
}
|
|
16
42
|
function collectDirectoriesFromArgs(args) {
|
|
17
43
|
const directories = [];
|
|
@@ -43,17 +69,19 @@ function collectDirectoriesFromArgs(args) {
|
|
|
43
69
|
}
|
|
44
70
|
return directories;
|
|
45
71
|
}
|
|
46
|
-
function ensureDirectoriesExist(rawDirs) {
|
|
72
|
+
function ensureDirectoriesExist(rawDirs, allowedRoots) {
|
|
47
73
|
const deduped = new Set(rawDirs
|
|
48
74
|
.map((entry) => entry.trim())
|
|
49
75
|
.filter((entry) => entry.length > 0));
|
|
50
76
|
for (const dir of deduped) {
|
|
77
|
+
const resolved = path.resolve(dir);
|
|
78
|
+
assertAllowedDirectory(resolved, allowedRoots, "Requested directory");
|
|
51
79
|
try {
|
|
52
|
-
mkdirSync(
|
|
80
|
+
mkdirSync(resolved, { recursive: true });
|
|
53
81
|
}
|
|
54
82
|
catch (error) {
|
|
55
83
|
const message = error instanceof Error ? error.message : String(error);
|
|
56
|
-
throw new Error(`Failed to create working directory '${
|
|
84
|
+
throw new Error(`Failed to create working directory '${resolved}': ${message}`);
|
|
57
85
|
}
|
|
58
86
|
}
|
|
59
87
|
}
|
|
@@ -133,16 +161,18 @@ export function spawnAgentProcess(config) {
|
|
|
133
161
|
const startedAt = new Date().toISOString();
|
|
134
162
|
const isCodexModel = !agent.model.startsWith("gemini-") && !agent.model.startsWith("claude-");
|
|
135
163
|
const sanitizedOptions = optionsArgs;
|
|
164
|
+
const allowedRoots = parseAllowedRoots();
|
|
136
165
|
const spawnWithMode = (promptModeOverride) => {
|
|
137
166
|
const { command, args, promptMode } = buildCommand({ ...config, optionsArgs: sanitizedOptions }, promptModeOverride);
|
|
138
167
|
// Ensure requested project/working directories exist before launching CLI.
|
|
139
|
-
ensureDirectoriesExist(collectDirectoriesFromArgs(args));
|
|
168
|
+
ensureDirectoriesExist(collectDirectoriesFromArgs(args), allowedRoots);
|
|
140
169
|
const ttyMode = process.env.CODEX_TTY_MODE ?? DEFAULT_TTY_MODE;
|
|
141
170
|
const hasExec = args.includes("exec");
|
|
142
171
|
const useScriptWrapper = ttyMode === "script" || (ttyMode === "auto" && hasExec);
|
|
143
172
|
const ttyTerm = process.env.CODEX_TTY_TERM ?? DEFAULT_TTY_TERM;
|
|
144
173
|
const defaultCwd = process.cwd(); // Client uses current working dir
|
|
145
174
|
const workingDir = process.env.CODEX_CWD ?? defaultCwd;
|
|
175
|
+
assertAllowedDirectory(workingDir, allowedRoots, "Spawn cwd");
|
|
146
176
|
// For logging/display
|
|
147
177
|
const commandString = [command, ...args].map(shellQuote).join(" ");
|
|
148
178
|
const platform = process.platform;
|
package/dist/transport.js
CHANGED
|
@@ -1,5 +1,27 @@
|
|
|
1
1
|
import { encodeEnvelope, nowIso } from "./protocol.js";
|
|
2
2
|
import os from "os";
|
|
3
|
+
const LOCAL_HOSTS = new Set(["localhost", "127.0.0.1", "::1"]);
|
|
4
|
+
function isTruthyEnv(value) {
|
|
5
|
+
const normalized = value?.trim().toLowerCase();
|
|
6
|
+
return normalized === "1" || normalized === "true" || normalized === "yes" || normalized === "on";
|
|
7
|
+
}
|
|
8
|
+
function assertSecureTransport(url) {
|
|
9
|
+
const allowInsecure = isTruthyEnv(process.env.AGENT_LINK_ALLOW_INSECURE_TRANSPORT);
|
|
10
|
+
const isLocal = LOCAL_HOSTS.has(url.hostname.toLowerCase());
|
|
11
|
+
if (url.protocol === "https:" || url.protocol === "wss:") {
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
if ((url.protocol === "http:" || url.protocol === "ws:") && (allowInsecure || isLocal)) {
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
throw new Error("Insecure transport is blocked. Use https:// (or set AGENT_LINK_ALLOW_INSECURE_TRANSPORT=1 for explicit local override).");
|
|
18
|
+
}
|
|
19
|
+
function toDisplayUrl(url) {
|
|
20
|
+
const clone = new URL(url.toString());
|
|
21
|
+
clone.search = "";
|
|
22
|
+
clone.hash = "";
|
|
23
|
+
return clone.toString();
|
|
24
|
+
}
|
|
3
25
|
export class NoopTransport {
|
|
4
26
|
async connect() {
|
|
5
27
|
return undefined;
|
|
@@ -10,6 +32,9 @@ export class NoopTransport {
|
|
|
10
32
|
sendStatus(_agentId, _state) {
|
|
11
33
|
return undefined;
|
|
12
34
|
}
|
|
35
|
+
sendBootstrap(_runId, _status, _message) {
|
|
36
|
+
return undefined;
|
|
37
|
+
}
|
|
13
38
|
close() {
|
|
14
39
|
return undefined;
|
|
15
40
|
}
|
|
@@ -52,6 +77,7 @@ export class WebSocketTransport {
|
|
|
52
77
|
return;
|
|
53
78
|
}
|
|
54
79
|
const url = new URL("/api/ws", serverUrl);
|
|
80
|
+
assertSecureTransport(url);
|
|
55
81
|
url.protocol = url.protocol === "https:" ? "wss:" : "ws:";
|
|
56
82
|
url.searchParams.set("token", token);
|
|
57
83
|
return new Promise((resolve, reject) => {
|
|
@@ -62,7 +88,7 @@ export class WebSocketTransport {
|
|
|
62
88
|
this.socket.onopen = null;
|
|
63
89
|
this.socket.close();
|
|
64
90
|
}
|
|
65
|
-
console.log(`Connecting to ${url
|
|
91
|
+
console.log(`Connecting to ${toDisplayUrl(url)}...`);
|
|
66
92
|
const socket = new WebSocket(url.toString());
|
|
67
93
|
this.socket = socket;
|
|
68
94
|
const onOpen = () => {
|
|
@@ -82,7 +108,7 @@ export class WebSocketTransport {
|
|
|
82
108
|
};
|
|
83
109
|
resolve();
|
|
84
110
|
};
|
|
85
|
-
const onFail = (
|
|
111
|
+
const onFail = (_err) => {
|
|
86
112
|
console.error("WebSocket connection failed to open.");
|
|
87
113
|
reject(new Error("WebSocket connection failed"));
|
|
88
114
|
};
|
|
@@ -112,7 +138,7 @@ export class WebSocketTransport {
|
|
|
112
138
|
this.options.onAck(message.id);
|
|
113
139
|
}
|
|
114
140
|
}
|
|
115
|
-
catch
|
|
141
|
+
catch {
|
|
116
142
|
// ignore malformed
|
|
117
143
|
}
|
|
118
144
|
};
|
|
@@ -150,6 +176,18 @@ export class WebSocketTransport {
|
|
|
150
176
|
payload: { state },
|
|
151
177
|
}));
|
|
152
178
|
}
|
|
179
|
+
sendBootstrap(runId, status, message) {
|
|
180
|
+
if (!this.socket || this.socket.readyState !== WebSocket.OPEN)
|
|
181
|
+
return;
|
|
182
|
+
this.socket.send(JSON.stringify({
|
|
183
|
+
type: "bootstrap",
|
|
184
|
+
payload: {
|
|
185
|
+
runId,
|
|
186
|
+
status,
|
|
187
|
+
message,
|
|
188
|
+
},
|
|
189
|
+
}));
|
|
190
|
+
}
|
|
153
191
|
close() {
|
|
154
192
|
this.isExplicitClose = true;
|
|
155
193
|
this.stopPingInterval();
|