@tonyclaw/agent-inspector 3.0.43 → 3.0.45
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/.output/cli.js +20744 -16945
- package/.output/nitro.json +1 -1
- package/.output/public/assets/{CompareDrawer-BAXxwTwT.js → CompareDrawer-B3_TLTJh.js} +1 -1
- package/.output/public/assets/{InspectorPet-xYuGV4Qz.js → InspectorPet-Cpn6hv4P.js} +1 -1
- package/.output/public/assets/{ProxyViewerContainer-B6GZnUWi.js → ProxyViewerContainer-BEAhuGOJ.js} +27 -27
- package/.output/public/assets/{ReplayDialog-D73pyIg3.js → ReplayDialog-B3EKVdJk.js} +1 -1
- package/.output/public/assets/{RequestAnatomy-CSDta_nu.js → RequestAnatomy-COOQjyUC.js} +1 -1
- package/.output/public/assets/{ResponseView-C4aIUPby.js → ResponseView-DBCjkwye.js} +1 -1
- package/.output/public/assets/{StreamingChunkSequence-D4xxosLx.js → StreamingChunkSequence-CuXhDgMo.js} +1 -1
- package/.output/public/assets/{_sessionId-CZHbJyWO.js → _sessionId-BJlM3nmt.js} +1 -1
- package/.output/public/assets/{_sessionId-DvuNLzSE.js → _sessionId-zbgYl7ZJ.js} +1 -1
- package/.output/public/assets/{index-DZ_wpqQM.js → index-BIK4bPTv.js} +1 -1
- package/.output/public/assets/{index-B17u7JMC.js → index-BZo_8koU.js} +15 -15
- package/.output/public/assets/{index-C0g79BAs.js → index-D2Qw6Ri2.js} +1 -1
- package/.output/public/assets/{index-BWjeqxbk.css → index-ZWschkCh.css} +1 -1
- package/.output/public/assets/{index-CmEO8ykj.js → index-xF_NY8Yb.js} +5 -5
- package/.output/public/assets/{json-viewer-CuWU-H6K.js → json-viewer-B-Ao6pwW.js} +1 -1
- package/.output/public/assets/{jszip.min-DPJI_UKW.js → jszip.min-B_rfAUCe.js} +1 -1
- package/.output/server/_libs/modelcontextprotocol__core.mjs +25 -25
- package/.output/server/_libs/modelcontextprotocol__server.mjs +2 -2
- package/.output/server/_libs/zod.mjs +6 -6
- package/.output/server/{_sessionId-Cun21KZF.mjs → _sessionId-CJ99gZFV.mjs} +3 -1
- package/.output/server/{_sessionId-CoFyeOti.mjs → _sessionId-DEjScBpK.mjs} +5 -3
- package/.output/server/_ssr/{CompareDrawer-Bgc2TZh_.mjs → CompareDrawer-DtOIMeQB.mjs} +4 -2
- package/.output/server/_ssr/{InspectorPet-DyK2ojeh.mjs → InspectorPet-CXc4oCBQ.mjs} +3 -1
- package/.output/server/_ssr/{ProxyViewerContainer-hvn6oVWD.mjs → ProxyViewerContainer-DVPlwfkt.mjs} +52 -27
- package/.output/server/_ssr/{ReplayDialog-KpnrGtZ-.mjs → ReplayDialog-BQEA95qC.mjs} +6 -4
- package/.output/server/_ssr/{RequestAnatomy-uCsfucn8.mjs → RequestAnatomy-BMyd8B7Q.mjs} +4 -2
- package/.output/server/_ssr/{ResponseView-C8UPqoPO.mjs → ResponseView-qrTaEIM8.mjs} +4 -2
- package/.output/server/_ssr/{StreamingChunkSequence-BP28I9sh.mjs → StreamingChunkSequence-BFTL8mIy.mjs} +5 -3
- package/.output/server/_ssr/{index-Du3YNDKx.mjs → index-Bcj1Hcy4.mjs} +4 -2
- package/.output/server/_ssr/{index-BVwVoQlZ.mjs → index-DW-M7XUG.mjs} +3 -1
- package/.output/server/_ssr/index.mjs +4 -3
- package/.output/server/_ssr/{json-viewer-DrE0vtlB.mjs → json-viewer-DiI0Yjsn.mjs} +4 -2
- package/.output/server/_ssr/publicBasePathContext-5264L3rX.mjs +148 -0
- package/.output/server/_ssr/{router-C6nWUZHK.mjs → router-BJ_ltLim.mjs} +2432 -475
- package/.output/server/_ssr/start-C6MjPjxp.mjs +57 -0
- package/.output/server/_tanstack-start-manifest_v-C6pXXX9R.mjs +4 -0
- package/.output/server/index.mjs +99 -99
- package/.output/workers/logFinalizer.worker.js +4 -0
- package/.output/workers/sessionWorkerEntry.js +4 -0
- package/README.md +221 -25
- package/docs/instance-control.openapi.yaml +306 -0
- package/package.json +4 -1
- package/src/cli/alias.ts +440 -0
- package/src/cli/instance.ts +1037 -0
- package/src/cli/instanceArgs.ts +266 -0
- package/src/cli/instanceControl.ts +183 -0
- package/src/cli/instanceModel.ts +275 -0
- package/src/cli/instanceRegistry.ts +740 -0
- package/src/cli/onboard.ts +123 -35
- package/src/cli/onboardTarget.ts +179 -0
- package/src/cli/templates/codex-skill-onboard.ts +76 -28
- package/src/cli/templates/skill-onboard.ts +93 -59
- package/src/cli.ts +154 -16
- package/src/components/ProxyViewer.tsx +13 -5
- package/src/components/providers/SettingsDialog.tsx +13 -8
- package/src/lib/basePath.ts +8 -2
- package/src/lib/codingAgentEndpoints.ts +16 -0
- package/src/lib/instanceContract.ts +220 -0
- package/src/lib/managedInstance.ts +104 -0
- package/src/lib/publicBasePathContext.ts +107 -0
- package/src/mcp/instanceHandlers.ts +149 -0
- package/src/mcp/loopback.ts +16 -5
- package/src/mcp/mode.ts +25 -0
- package/src/mcp/server.ts +258 -18
- package/src/mcp/toolHandlers.ts +2 -2
- package/src/proxy/identityProxy.ts +656 -36
- package/src/proxy/runtimeShutdown.ts +73 -1
- package/src/proxy/sessionInfo.ts +5 -4
- package/src/routes/api/-instances.ts +109 -0
- package/src/routes/api/instances.$name.connection.ts +12 -0
- package/src/routes/api/instances.$name.restart.ts +23 -0
- package/src/routes/api/instances.$name.start.ts +18 -0
- package/src/routes/api/instances.$name.stop.ts +23 -0
- package/src/routes/api/instances.$name.ts +12 -0
- package/src/routes/api/instances.ts +16 -0
- package/src/start.ts +10 -0
- package/.output/server/_ssr/start-HYkvq4Ni.mjs +0 -4
- package/.output/server/_tanstack-start-manifest_v-rshlRaiN.mjs +0 -4
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
import { normalizeBasePath } from "../lib/basePath";
|
|
2
|
+
import { isValidBindHost } from "./networkHints";
|
|
3
|
+
|
|
4
|
+
export type InstanceCaptureMode = "simple" | "full";
|
|
5
|
+
|
|
6
|
+
export type InstanceStartOverrides = {
|
|
7
|
+
port: number | null;
|
|
8
|
+
basePath: string | null;
|
|
9
|
+
host: string | null;
|
|
10
|
+
captureMode: InstanceCaptureMode | null;
|
|
11
|
+
dataDir: string | null;
|
|
12
|
+
open: boolean;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export type InstanceAction =
|
|
16
|
+
| { kind: "start"; name: string; overrides: InstanceStartOverrides; json: boolean }
|
|
17
|
+
| { kind: "list"; json: boolean }
|
|
18
|
+
| { kind: "status"; name: string; json: boolean }
|
|
19
|
+
| { kind: "connect"; name: string; json: boolean }
|
|
20
|
+
| { kind: "stop"; name: string; json: boolean }
|
|
21
|
+
| { kind: "restart"; name: string; overrides: InstanceStartOverrides; json: boolean };
|
|
22
|
+
|
|
23
|
+
export type InstanceArgsResult =
|
|
24
|
+
| { kind: "help" }
|
|
25
|
+
| { kind: "error"; message: string }
|
|
26
|
+
| { kind: "ok"; action: InstanceAction };
|
|
27
|
+
|
|
28
|
+
function parsePort(value: string | undefined): number | null {
|
|
29
|
+
if (value === undefined || value.trim().length === 0) return null;
|
|
30
|
+
const parsed = Number(value);
|
|
31
|
+
return Number.isInteger(parsed) && parsed >= 1 && parsed <= 65_535 ? parsed : null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function parseMode(value: string | undefined): InstanceCaptureMode | null {
|
|
35
|
+
return value === "simple" || value === "full" ? value : null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function parseBasePath(value: string | undefined): string | null {
|
|
39
|
+
if (
|
|
40
|
+
value === undefined ||
|
|
41
|
+
!value.startsWith("/") ||
|
|
42
|
+
value.includes("?") ||
|
|
43
|
+
value.includes("#") ||
|
|
44
|
+
value.includes("\\")
|
|
45
|
+
) {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
return normalizeBasePath(value);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function optionWithEquals(arg: string): { name: string; value: string } | null {
|
|
52
|
+
if (!arg.startsWith("--") || !arg.includes("=")) return null;
|
|
53
|
+
const separator = arg.indexOf("=");
|
|
54
|
+
return { name: arg.slice(0, separator), value: arg.slice(separator + 1) };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function emptyOverrides(): InstanceStartOverrides {
|
|
58
|
+
return {
|
|
59
|
+
port: null,
|
|
60
|
+
basePath: null,
|
|
61
|
+
host: null,
|
|
62
|
+
captureMode: null,
|
|
63
|
+
dataDir: null,
|
|
64
|
+
open: false,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
type MutableOptions = {
|
|
69
|
+
json: boolean;
|
|
70
|
+
help: boolean;
|
|
71
|
+
overrides: InstanceStartOverrides;
|
|
72
|
+
positional: string[];
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
function setValueOption(
|
|
76
|
+
options: MutableOptions,
|
|
77
|
+
name: string,
|
|
78
|
+
value: string | undefined,
|
|
79
|
+
): string | null {
|
|
80
|
+
switch (name) {
|
|
81
|
+
case "--port":
|
|
82
|
+
case "-p": {
|
|
83
|
+
const port = parsePort(value);
|
|
84
|
+
if (port === null) return "instance: --port must be an integer from 1 to 65535";
|
|
85
|
+
options.overrides.port = port;
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
case "--base-path": {
|
|
89
|
+
const basePath = parseBasePath(value);
|
|
90
|
+
if (basePath === null) {
|
|
91
|
+
return "instance: --base-path must be an absolute URL path without a query or hash";
|
|
92
|
+
}
|
|
93
|
+
options.overrides.basePath = basePath;
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
case "--host":
|
|
97
|
+
case "-H": {
|
|
98
|
+
const host = value?.trim() ?? "";
|
|
99
|
+
if (!isValidBindHost(host)) {
|
|
100
|
+
return "instance: --host requires a hostname or IP address without a protocol or path";
|
|
101
|
+
}
|
|
102
|
+
options.overrides.host = host;
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
case "--mode":
|
|
106
|
+
case "--capture-mode": {
|
|
107
|
+
const captureMode = parseMode(value);
|
|
108
|
+
if (captureMode === null) return "instance: --mode must be simple or full";
|
|
109
|
+
options.overrides.captureMode = captureMode;
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
case "--data-dir": {
|
|
113
|
+
const dataDir = value?.trim() ?? "";
|
|
114
|
+
if (dataDir.length === 0) return "instance: --data-dir requires a directory path";
|
|
115
|
+
options.overrides.dataDir = dataDir;
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
default:
|
|
119
|
+
return `instance: unknown option ${name}`;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export function parseInstanceArgs(argv: readonly string[]): InstanceArgsResult {
|
|
124
|
+
const options: MutableOptions = {
|
|
125
|
+
json: false,
|
|
126
|
+
help: false,
|
|
127
|
+
overrides: emptyOverrides(),
|
|
128
|
+
positional: [],
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
for (let index = 0; index < argv.length; index++) {
|
|
132
|
+
const arg = argv[index];
|
|
133
|
+
if (arg === undefined) continue;
|
|
134
|
+
const equalsOption = optionWithEquals(arg);
|
|
135
|
+
if (equalsOption !== null) {
|
|
136
|
+
const error = setValueOption(options, equalsOption.name, equalsOption.value);
|
|
137
|
+
if (error !== null) return { kind: "error", message: error };
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
switch (arg) {
|
|
141
|
+
case "--help":
|
|
142
|
+
case "-h":
|
|
143
|
+
options.help = true;
|
|
144
|
+
break;
|
|
145
|
+
case "--json":
|
|
146
|
+
options.json = true;
|
|
147
|
+
break;
|
|
148
|
+
case "--open":
|
|
149
|
+
options.overrides.open = true;
|
|
150
|
+
break;
|
|
151
|
+
case "--port":
|
|
152
|
+
case "-p":
|
|
153
|
+
case "--base-path":
|
|
154
|
+
case "--host":
|
|
155
|
+
case "-H":
|
|
156
|
+
case "--mode":
|
|
157
|
+
case "--capture-mode":
|
|
158
|
+
case "--data-dir": {
|
|
159
|
+
const error = setValueOption(options, arg, argv[index + 1]);
|
|
160
|
+
if (error !== null) return { kind: "error", message: error };
|
|
161
|
+
index++;
|
|
162
|
+
break;
|
|
163
|
+
}
|
|
164
|
+
default:
|
|
165
|
+
if (arg.startsWith("-")) {
|
|
166
|
+
return { kind: "error", message: `instance: unknown option ${arg}` };
|
|
167
|
+
}
|
|
168
|
+
options.positional.push(arg);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (options.help) return { kind: "help" };
|
|
173
|
+
const command = options.positional[0];
|
|
174
|
+
if (command === "list") {
|
|
175
|
+
if (options.positional.length !== 1) {
|
|
176
|
+
return { kind: "error", message: "instance: list does not accept an instance name" };
|
|
177
|
+
}
|
|
178
|
+
if (
|
|
179
|
+
options.overrides.port !== null ||
|
|
180
|
+
options.overrides.basePath !== null ||
|
|
181
|
+
options.overrides.host !== null ||
|
|
182
|
+
options.overrides.captureMode !== null ||
|
|
183
|
+
options.overrides.dataDir !== null ||
|
|
184
|
+
options.overrides.open
|
|
185
|
+
) {
|
|
186
|
+
return { kind: "error", message: "instance: list accepts only --json" };
|
|
187
|
+
}
|
|
188
|
+
return { kind: "ok", action: { kind: "list", json: options.json } };
|
|
189
|
+
}
|
|
190
|
+
if (
|
|
191
|
+
command === "status" ||
|
|
192
|
+
command === "stop" ||
|
|
193
|
+
command === "connect" ||
|
|
194
|
+
command === "connection"
|
|
195
|
+
) {
|
|
196
|
+
const name = options.positional[1];
|
|
197
|
+
if (name === undefined || options.positional.length !== 2) {
|
|
198
|
+
return { kind: "error", message: `instance: ${command} requires exactly one instance name` };
|
|
199
|
+
}
|
|
200
|
+
if (
|
|
201
|
+
options.overrides.port !== null ||
|
|
202
|
+
options.overrides.basePath !== null ||
|
|
203
|
+
options.overrides.host !== null ||
|
|
204
|
+
options.overrides.captureMode !== null ||
|
|
205
|
+
options.overrides.dataDir !== null ||
|
|
206
|
+
options.overrides.open
|
|
207
|
+
) {
|
|
208
|
+
return {
|
|
209
|
+
kind: "error",
|
|
210
|
+
message: `instance: ${command} accepts only an instance name and --json`,
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
return {
|
|
214
|
+
kind: "ok",
|
|
215
|
+
action: { kind: command === "connection" ? "connect" : command, name, json: options.json },
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
if (command === "start" || command === "restart") {
|
|
219
|
+
const name = options.positional[1];
|
|
220
|
+
if (name === undefined || options.positional.length !== 2) {
|
|
221
|
+
return { kind: "error", message: `instance: ${command} requires exactly one instance name` };
|
|
222
|
+
}
|
|
223
|
+
return {
|
|
224
|
+
kind: "ok",
|
|
225
|
+
action: { kind: command, name, overrides: options.overrides, json: options.json },
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
if (command === undefined) {
|
|
229
|
+
return {
|
|
230
|
+
kind: "error",
|
|
231
|
+
message: "instance: expected start, list, status, connect, stop, or restart",
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
return { kind: "error", message: `instance: unknown command ${command}` };
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
export function instanceHelp(): string {
|
|
238
|
+
return `agent-inspector instance - manage isolated named runtimes
|
|
239
|
+
|
|
240
|
+
Usage:
|
|
241
|
+
agent-inspector instance start <name> [options]
|
|
242
|
+
agent-inspector instance list [--json]
|
|
243
|
+
agent-inspector instance status <name> [--json]
|
|
244
|
+
agent-inspector instance connect <name> [--json]
|
|
245
|
+
agent-inspector instance stop <name> [--json]
|
|
246
|
+
agent-inspector instance restart <name> [options]
|
|
247
|
+
|
|
248
|
+
Start/restart options:
|
|
249
|
+
--port, -p <port> Public ingress port (auto-allocated on first start)
|
|
250
|
+
--base-path <path> Public Base Path (default: /inspector; use / for root)
|
|
251
|
+
--host, -H <host> Bind host (default: 127.0.0.1)
|
|
252
|
+
--mode <simple|full> Capture mode (default: simple)
|
|
253
|
+
--data-dir <path> Isolated data/config directory
|
|
254
|
+
--open Open the UI after readiness
|
|
255
|
+
--json Emit machine-readable output
|
|
256
|
+
--help, -h Show this help
|
|
257
|
+
|
|
258
|
+
Named instances run as hidden background supervisors and persist their configuration. Each one
|
|
259
|
+
uses separate Provider, config, capture, and index storage. The existing \`agent-inspector\` command
|
|
260
|
+
remains the unmanaged default instance. Base Path aliases mount another URL on one runtime; named
|
|
261
|
+
instances start separate runtimes.
|
|
262
|
+
|
|
263
|
+
The connect command prints protocol-correct Anthropic, OpenAI /v1, MCP, health, and UI settings
|
|
264
|
+
without exposing lifecycle or Provider credentials. \`connection\` is accepted as an alias.
|
|
265
|
+
`;
|
|
266
|
+
}
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import {
|
|
2
|
+
MANAGED_INSTANCE_CONTROL_PATH,
|
|
3
|
+
MANAGED_INSTANCE_SHUTDOWN_PATH,
|
|
4
|
+
MANAGED_INSTANCE_TOKEN_HEADER,
|
|
5
|
+
} from "../lib/managedInstance";
|
|
6
|
+
|
|
7
|
+
const DEFAULT_CONTROL_TIMEOUT_MS = 2_000;
|
|
8
|
+
|
|
9
|
+
export type InstanceControlFetch = (input: string, init?: RequestInit) => Promise<Response>;
|
|
10
|
+
|
|
11
|
+
export type ExpectedInstanceControlIdentity = {
|
|
12
|
+
instanceId: string;
|
|
13
|
+
launchId: string;
|
|
14
|
+
controlToken: string;
|
|
15
|
+
host: string;
|
|
16
|
+
publicPort: number;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export type InstanceControlIdentity = {
|
|
20
|
+
schemaVersion: 1;
|
|
21
|
+
instanceId: string;
|
|
22
|
+
launchId: string;
|
|
23
|
+
supervisorPid: number;
|
|
24
|
+
startedAt: string;
|
|
25
|
+
publicPort: number;
|
|
26
|
+
upstreamPort: number;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export type InstanceControlProbe =
|
|
30
|
+
| { kind: "running"; identity: InstanceControlIdentity }
|
|
31
|
+
| { kind: "unreachable"; timedOut: boolean }
|
|
32
|
+
| { kind: "identity-mismatch" }
|
|
33
|
+
| { kind: "invalid-response" };
|
|
34
|
+
|
|
35
|
+
export type InstanceShutdownResult =
|
|
36
|
+
| { kind: "accepted" }
|
|
37
|
+
| { kind: "unreachable"; timedOut: boolean }
|
|
38
|
+
| { kind: "identity-mismatch" }
|
|
39
|
+
| { kind: "rejected" };
|
|
40
|
+
|
|
41
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
42
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function parseIdentity(value: unknown): InstanceControlIdentity | null {
|
|
46
|
+
if (!isRecord(value)) return null;
|
|
47
|
+
const schemaVersion = value["schemaVersion"];
|
|
48
|
+
const instanceId = value["instanceId"];
|
|
49
|
+
const launchId = value["launchId"];
|
|
50
|
+
const supervisorPid = value["supervisorPid"];
|
|
51
|
+
const startedAt = value["startedAt"];
|
|
52
|
+
const publicPort = value["publicPort"];
|
|
53
|
+
const upstreamPort = value["upstreamPort"];
|
|
54
|
+
if (
|
|
55
|
+
schemaVersion !== 1 ||
|
|
56
|
+
typeof instanceId !== "string" ||
|
|
57
|
+
instanceId.length === 0 ||
|
|
58
|
+
typeof launchId !== "string" ||
|
|
59
|
+
launchId.length === 0 ||
|
|
60
|
+
typeof supervisorPid !== "number" ||
|
|
61
|
+
!Number.isInteger(supervisorPid) ||
|
|
62
|
+
supervisorPid < 1 ||
|
|
63
|
+
typeof startedAt !== "string" ||
|
|
64
|
+
!Number.isFinite(Date.parse(startedAt)) ||
|
|
65
|
+
typeof publicPort !== "number" ||
|
|
66
|
+
!Number.isInteger(publicPort) ||
|
|
67
|
+
publicPort < 1 ||
|
|
68
|
+
publicPort > 65_535 ||
|
|
69
|
+
typeof upstreamPort !== "number" ||
|
|
70
|
+
!Number.isInteger(upstreamPort) ||
|
|
71
|
+
upstreamPort < 1 ||
|
|
72
|
+
upstreamPort > 65_535
|
|
73
|
+
) {
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
return {
|
|
77
|
+
schemaVersion: 1,
|
|
78
|
+
instanceId,
|
|
79
|
+
launchId,
|
|
80
|
+
supervisorPid,
|
|
81
|
+
startedAt,
|
|
82
|
+
publicPort,
|
|
83
|
+
upstreamPort,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function hostForUrl(host: string): string {
|
|
88
|
+
if (host.startsWith("[") && host.endsWith("]")) return host;
|
|
89
|
+
return host.includes(":") ? `[${host}]` : host;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function endpoint(identity: ExpectedInstanceControlIdentity, path: string): string {
|
|
93
|
+
return `http://${hostForUrl(identity.host)}:${String(identity.publicPort)}${path}`;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function requestInit(
|
|
97
|
+
identity: ExpectedInstanceControlIdentity,
|
|
98
|
+
controller: AbortController,
|
|
99
|
+
method: "GET" | "POST",
|
|
100
|
+
): RequestInit {
|
|
101
|
+
return {
|
|
102
|
+
method,
|
|
103
|
+
headers: {
|
|
104
|
+
accept: "application/json",
|
|
105
|
+
[MANAGED_INSTANCE_TOKEN_HEADER]: identity.controlToken,
|
|
106
|
+
},
|
|
107
|
+
cache: "no-store",
|
|
108
|
+
signal: controller.signal,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async function responseJson(response: Response): Promise<unknown | null> {
|
|
113
|
+
try {
|
|
114
|
+
const value: unknown = await response.json();
|
|
115
|
+
return value;
|
|
116
|
+
} catch {
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export async function probeInstanceControl(
|
|
122
|
+
expected: ExpectedInstanceControlIdentity,
|
|
123
|
+
fetchImpl: InstanceControlFetch = fetch,
|
|
124
|
+
timeoutMs = DEFAULT_CONTROL_TIMEOUT_MS,
|
|
125
|
+
): Promise<InstanceControlProbe> {
|
|
126
|
+
const controller = new AbortController();
|
|
127
|
+
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
128
|
+
try {
|
|
129
|
+
const response = await fetchImpl(
|
|
130
|
+
endpoint(expected, MANAGED_INSTANCE_CONTROL_PATH),
|
|
131
|
+
requestInit(expected, controller, "GET"),
|
|
132
|
+
);
|
|
133
|
+
if (!response.ok) return { kind: "identity-mismatch" };
|
|
134
|
+
const identity = parseIdentity(await responseJson(response));
|
|
135
|
+
if (identity === null) return { kind: "invalid-response" };
|
|
136
|
+
if (
|
|
137
|
+
identity.instanceId !== expected.instanceId ||
|
|
138
|
+
identity.launchId !== expected.launchId ||
|
|
139
|
+
identity.publicPort !== expected.publicPort
|
|
140
|
+
) {
|
|
141
|
+
return { kind: "identity-mismatch" };
|
|
142
|
+
}
|
|
143
|
+
return { kind: "running", identity };
|
|
144
|
+
} catch (error) {
|
|
145
|
+
return {
|
|
146
|
+
kind: "unreachable",
|
|
147
|
+
timedOut:
|
|
148
|
+
controller.signal.aborted || (error instanceof Error && error.name === "AbortError"),
|
|
149
|
+
};
|
|
150
|
+
} finally {
|
|
151
|
+
clearTimeout(timeout);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export async function requestInstanceShutdown(
|
|
156
|
+
expected: ExpectedInstanceControlIdentity,
|
|
157
|
+
fetchImpl: InstanceControlFetch = fetch,
|
|
158
|
+
timeoutMs = DEFAULT_CONTROL_TIMEOUT_MS,
|
|
159
|
+
): Promise<InstanceShutdownResult> {
|
|
160
|
+
const controller = new AbortController();
|
|
161
|
+
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
162
|
+
try {
|
|
163
|
+
const response = await fetchImpl(
|
|
164
|
+
endpoint(expected, MANAGED_INSTANCE_SHUTDOWN_PATH),
|
|
165
|
+
requestInit(expected, controller, "POST"),
|
|
166
|
+
);
|
|
167
|
+
if (response.status === 202) return { kind: "accepted" };
|
|
168
|
+
if (response.status === 401 || response.status === 403 || response.status === 404) {
|
|
169
|
+
return { kind: "identity-mismatch" };
|
|
170
|
+
}
|
|
171
|
+
return { kind: "rejected" };
|
|
172
|
+
} catch (error) {
|
|
173
|
+
return {
|
|
174
|
+
kind: "unreachable",
|
|
175
|
+
timedOut:
|
|
176
|
+
controller.signal.aborted || (error instanceof Error && error.name === "AbortError"),
|
|
177
|
+
};
|
|
178
|
+
} finally {
|
|
179
|
+
clearTimeout(timeout);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export const instanceControlForTests = { parseIdentity };
|