@tonyclaw/agent-inspector 3.0.46 → 3.0.48
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/backend/nitro.json +1 -1
- package/.output/cli.js +6791 -6006
- package/.output/server/_ssr/index.mjs +1 -1
- package/.output/server/_ssr/{router-D18yUq36.mjs → router-Cj10mv-Z.mjs} +334 -5
- package/.output/server/index.mjs +1 -1
- package/.output/ui/assets/{CompareDrawer-DAUuIJ6G.js → CompareDrawer-uRiXFOCp.js} +1 -1
- package/.output/ui/assets/{InspectorPet-BRBVjOWI.js → InspectorPet-j-r6xk0V.js} +1 -1
- package/.output/ui/assets/{ProxyViewerContainer-D7Sq0ctc.js → ProxyViewerContainer-DnUrkMJF.js} +23 -23
- package/.output/ui/assets/{ReplayDialog-nDLsjOhs.js → ReplayDialog-B2URyHMv.js} +1 -1
- package/.output/ui/assets/{RequestAnatomy-zd4BDgL0.js → RequestAnatomy-CK0M0CJO.js} +1 -1
- package/.output/ui/assets/{ResponseView-AdIXGcSn.js → ResponseView-CYSVA4lT.js} +1 -1
- package/.output/ui/assets/{StreamingChunkSequence-CesTz8He.js → StreamingChunkSequence-CHf54ZUV.js} +1 -1
- package/.output/ui/assets/{_sessionId-QnBomgPD.js → _sessionId-BXULpTNP.js} +1 -1
- package/.output/ui/assets/{_sessionId-BAfzrhSU.js → _sessionId-E4lbj5-I.js} +1 -1
- package/.output/ui/assets/{index-D0rtCN9V.js → index-B4ixI3kG.js} +1 -1
- package/.output/ui/assets/{index-BcsdxBAd.js → index-BJ-71WLq.js} +1 -1
- package/.output/ui/assets/{index-DHpwr08Z.js → index-CW0ab9r3.js} +2 -2
- package/.output/ui/assets/{index-dNuk2dsU.js → index-Ce-lO51c.js} +1 -1
- package/.output/ui/assets/{json-viewer-BvFHglMb.js → json-viewer-C6E8aLaV.js} +1 -1
- package/.output/ui/assets/{jszip.min-B5Z7gNZB.js → jszip.min-DjWdTBbp.js} +1 -1
- package/.output/ui/index.html +1 -1
- package/README.md +53 -0
- package/package.json +17 -3
- package/src/backend/routes/api/-instances.ts +7 -109
- package/src/cli/instance.ts +21 -0
- package/src/cli/instanceApi.ts +213 -0
- package/src/cli/instanceArgs.ts +16 -0
- package/src/cli/instanceControl.ts +10 -0
- package/src/cli/instanceModel.ts +9 -0
- package/src/cli/rustBackendPackage.ts +458 -0
- package/src/cli.ts +171 -14
- package/src/components/ProxyViewer.tsx +4 -13
- package/src/components/proxy-viewer/proxyViewerLogic.ts +9 -0
- package/src/lib/backendImplementation.ts +16 -0
- package/src/lib/instanceContract.ts +3 -0
- package/src/lib/managedInstance.ts +9 -1
- package/src/mcp/instanceHandlers.ts +1 -0
- package/src/proxy/identityProxy.ts +148 -0
- package/src/proxy/sessionArchive.ts +0 -8
- package/src/proxy/sqliteLogIndex.ts +0 -40
package/src/cli/instance.ts
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import { spawn, type ChildProcess } from "node:child_process";
|
|
2
2
|
import { resolve as resolvePath } from "node:path";
|
|
3
|
+
import packageJson from "../../package.json";
|
|
3
4
|
import {
|
|
4
5
|
AGENT_INSPECTOR_BASE_PATH_ENV,
|
|
5
6
|
appendBasePathToOrigin,
|
|
6
7
|
getConfiguredBasePath,
|
|
7
8
|
normalizePublicBasePath,
|
|
8
9
|
} from "../lib/basePath";
|
|
10
|
+
import type { BackendImplementation } from "../lib/backendImplementation";
|
|
9
11
|
import {
|
|
10
12
|
type InstanceConnectionOutput,
|
|
11
13
|
type InstanceOperationOutcome,
|
|
@@ -50,6 +52,7 @@ import {
|
|
|
50
52
|
type PortAvailabilityCheck,
|
|
51
53
|
} from "./instanceRegistry";
|
|
52
54
|
import { probeHostForBindHost, urlForHost } from "./networkHints";
|
|
55
|
+
import { resolvePreferredBackendImplementation } from "./rustBackendPackage";
|
|
53
56
|
|
|
54
57
|
const DEFAULT_INSTANCE_HOST = "127.0.0.1";
|
|
55
58
|
const DEFAULT_INSTANCE_CAPTURE_MODE = "simple";
|
|
@@ -101,6 +104,7 @@ export type InstanceLifecycleDependencies = {
|
|
|
101
104
|
isPortAvailable?: PortAvailabilityCheck;
|
|
102
105
|
launch?: (record: InstanceRecord, open: boolean) => Promise<boolean>;
|
|
103
106
|
openUrl?: (url: string) => void;
|
|
107
|
+
resolveDefaultBackend?: () => BackendImplementation;
|
|
104
108
|
sleep?: (milliseconds: number) => Promise<void>;
|
|
105
109
|
readyTimeoutMs?: number;
|
|
106
110
|
stopTimeoutMs?: number;
|
|
@@ -171,6 +175,7 @@ function statusOutput(
|
|
|
171
175
|
upstreamPort: record.upstreamPort,
|
|
172
176
|
basePath: displayBasePath(record.basePath),
|
|
173
177
|
captureMode: record.captureMode,
|
|
178
|
+
backend: record.backend,
|
|
174
179
|
uiEnabled: record.uiEnabled,
|
|
175
180
|
dataDir: record.dataDir,
|
|
176
181
|
supervisorPid: live.supervisorPid,
|
|
@@ -228,6 +233,7 @@ function connectionOutput(instance: InstanceStatusOutput): InstanceConnectionOut
|
|
|
228
233
|
|
|
229
234
|
function expectedIdentity(record: InstanceRecord): ExpectedInstanceControlIdentity {
|
|
230
235
|
return {
|
|
236
|
+
backend: record.backend,
|
|
231
237
|
instanceId: record.instanceId,
|
|
232
238
|
launchId: record.launch.launchId,
|
|
233
239
|
controlToken: record.launch.controlToken,
|
|
@@ -285,6 +291,8 @@ export function buildInstanceLaunchArgs(record: InstanceRecord, open: boolean):
|
|
|
285
291
|
const args = [
|
|
286
292
|
"--background",
|
|
287
293
|
"--no-open",
|
|
294
|
+
"--backend",
|
|
295
|
+
record.backend,
|
|
288
296
|
"--port",
|
|
289
297
|
String(record.publicPort),
|
|
290
298
|
"--host",
|
|
@@ -310,6 +318,7 @@ async function defaultLaunch(
|
|
|
310
318
|
[AGENT_INSPECTOR_CLI_ENTRY_ENV]: cliEntry,
|
|
311
319
|
AGENT_INSPECTOR_DATA_DIR: record.dataDir,
|
|
312
320
|
[AGENT_INSPECTOR_BASE_PATH_ENV]: displayBasePath(record.basePath),
|
|
321
|
+
[MANAGED_INSTANCE_ENV.backend]: record.backend,
|
|
313
322
|
[MANAGED_INSTANCE_ENV.name]: record.name,
|
|
314
323
|
[MANAGED_INSTANCE_ENV.instanceId]: record.instanceId,
|
|
315
324
|
[MANAGED_INSTANCE_ENV.launchId]: record.launch.launchId,
|
|
@@ -475,6 +484,7 @@ function overridesDiffer(
|
|
|
475
484
|
action: Extract<InstanceAction, { kind: "start" | "restart" }>,
|
|
476
485
|
): boolean {
|
|
477
486
|
const { overrides } = action;
|
|
487
|
+
if (overrides.backend !== null && overrides.backend !== record.backend) return true;
|
|
478
488
|
if (overrides.port !== null && overrides.port !== record.publicPort) return true;
|
|
479
489
|
if (overrides.basePath !== null && overrides.basePath !== record.basePath) return true;
|
|
480
490
|
if (overrides.host !== null && overrides.host !== record.host) return true;
|
|
@@ -507,6 +517,7 @@ function updatedRecord(
|
|
|
507
517
|
upstreamPort: pair.upstreamPort,
|
|
508
518
|
basePath: action.overrides.basePath ?? record.basePath,
|
|
509
519
|
captureMode: action.overrides.captureMode ?? record.captureMode,
|
|
520
|
+
backend: action.overrides.backend ?? record.backend,
|
|
510
521
|
uiEnabled: action.overrides.uiEnabled ?? record.uiEnabled,
|
|
511
522
|
dataDir,
|
|
512
523
|
};
|
|
@@ -542,6 +553,13 @@ function validateInstanceDataDirectory(
|
|
|
542
553
|
return success(assigned.value.path);
|
|
543
554
|
}
|
|
544
555
|
|
|
556
|
+
function resolveDefaultInstanceBackend(deps: InstanceLifecycleDependencies): BackendImplementation {
|
|
557
|
+
return (
|
|
558
|
+
deps.resolveDefaultBackend?.() ??
|
|
559
|
+
resolvePreferredBackendImplementation({ rootVersion: packageJson.version })
|
|
560
|
+
);
|
|
561
|
+
}
|
|
562
|
+
|
|
545
563
|
async function createNewRecord(
|
|
546
564
|
root: string,
|
|
547
565
|
action: Extract<InstanceAction, { kind: "start" }>,
|
|
@@ -569,6 +587,7 @@ async function createNewRecord(
|
|
|
569
587
|
upstreamPort: pairResult.value.upstreamPort,
|
|
570
588
|
basePath: action.overrides.basePath ?? getConfiguredBasePath(process.env),
|
|
571
589
|
captureMode: action.overrides.captureMode ?? DEFAULT_INSTANCE_CAPTURE_MODE,
|
|
590
|
+
backend: action.overrides.backend ?? resolveDefaultInstanceBackend(deps),
|
|
572
591
|
uiEnabled: action.overrides.uiEnabled ?? true,
|
|
573
592
|
dataDir: dataDirResult.value,
|
|
574
593
|
});
|
|
@@ -808,6 +827,7 @@ function requestActionOverrides(
|
|
|
808
827
|
input: InstanceStartRequest,
|
|
809
828
|
): Extract<InstanceAction, { kind: "start" }>["overrides"] {
|
|
810
829
|
return {
|
|
830
|
+
backend: input.backend ?? null,
|
|
811
831
|
port: input.port ?? null,
|
|
812
832
|
basePath: input.basePath === undefined ? null : normalizePublicBasePath(input.basePath),
|
|
813
833
|
host: input.host ?? null,
|
|
@@ -901,6 +921,7 @@ function formatStatus(output: InstanceStatusOutput): string {
|
|
|
901
921
|
Port pair: ${String(output.publicPort)} / ${String(output.upstreamPort)} (private)
|
|
902
922
|
Base Path: ${output.basePath}
|
|
903
923
|
Capture mode: ${output.captureMode}
|
|
924
|
+
Backend: ${output.backend}
|
|
904
925
|
Data directory: ${output.dataDir}
|
|
905
926
|
Supervisor PID: ${pid}
|
|
906
927
|
State reason: ${reason}
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import { type z } from "zod";
|
|
2
|
+
import {
|
|
3
|
+
getNamedInstance,
|
|
4
|
+
getNamedInstanceConnection,
|
|
5
|
+
type InstanceLifecycleError,
|
|
6
|
+
type InstanceLifecycleResult,
|
|
7
|
+
listNamedInstances,
|
|
8
|
+
restartNamedInstance,
|
|
9
|
+
startNamedInstance,
|
|
10
|
+
stopNamedInstance,
|
|
11
|
+
} from "./instance";
|
|
12
|
+
import {
|
|
13
|
+
type InstancePublicErrorCode,
|
|
14
|
+
INSTANCE_API_SCHEMA_VERSION,
|
|
15
|
+
InstanceEmptyRequestSchema,
|
|
16
|
+
InstanceStartRequestSchema,
|
|
17
|
+
instanceErrorOutput,
|
|
18
|
+
} from "../lib/instanceContract";
|
|
19
|
+
import { readManagedInstanceLaunch } from "../lib/managedInstance";
|
|
20
|
+
|
|
21
|
+
const NO_STORE_HEADERS = { "cache-control": "no-store" };
|
|
22
|
+
|
|
23
|
+
type ParsedBody<T> = { ok: true; value: T } | { ok: false; response: Response };
|
|
24
|
+
|
|
25
|
+
function errorStatus(code: InstancePublicErrorCode): number {
|
|
26
|
+
switch (code) {
|
|
27
|
+
case "argument_invalid":
|
|
28
|
+
case "data_dir_invalid":
|
|
29
|
+
case "name_invalid":
|
|
30
|
+
case "port_invalid":
|
|
31
|
+
case "record_invalid":
|
|
32
|
+
return 400;
|
|
33
|
+
case "instance_not_found":
|
|
34
|
+
return 404;
|
|
35
|
+
case "config_mismatch":
|
|
36
|
+
case "data_dir_conflict":
|
|
37
|
+
case "identity_mismatch":
|
|
38
|
+
case "port_conflict":
|
|
39
|
+
case "self_lifecycle_unsupported":
|
|
40
|
+
return 409;
|
|
41
|
+
case "lock_not_owned":
|
|
42
|
+
case "lock_timeout":
|
|
43
|
+
return 423;
|
|
44
|
+
case "launch_failed":
|
|
45
|
+
case "launch_unavailable":
|
|
46
|
+
case "port_exhausted":
|
|
47
|
+
case "registry_io":
|
|
48
|
+
case "restart_failed_degraded":
|
|
49
|
+
case "restart_failed_rolled_back":
|
|
50
|
+
case "shutdown_failed":
|
|
51
|
+
return 503;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function instanceErrorResponse(error: InstanceLifecycleError): Response {
|
|
56
|
+
return Response.json(instanceErrorOutput(error), {
|
|
57
|
+
status: errorStatus(error.code),
|
|
58
|
+
headers: NO_STORE_HEADERS,
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function instanceResultResponse<T>(
|
|
63
|
+
result: InstanceLifecycleResult<T>,
|
|
64
|
+
successStatus = 200,
|
|
65
|
+
): Response {
|
|
66
|
+
return result.kind === "error"
|
|
67
|
+
? instanceErrorResponse(result.error)
|
|
68
|
+
: Response.json(result.value, { status: successStatus, headers: NO_STORE_HEADERS });
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export async function parseInstanceBody<T extends z.ZodType>(
|
|
72
|
+
request: Request,
|
|
73
|
+
schema: T,
|
|
74
|
+
): Promise<ParsedBody<z.output<T>>> {
|
|
75
|
+
let value: unknown;
|
|
76
|
+
try {
|
|
77
|
+
const text = await request.text();
|
|
78
|
+
value = text.trim().length === 0 ? {} : JSON.parse(text);
|
|
79
|
+
} catch {
|
|
80
|
+
return {
|
|
81
|
+
ok: false,
|
|
82
|
+
response: instanceErrorResponse({
|
|
83
|
+
code: "argument_invalid",
|
|
84
|
+
message: "Request body must be valid JSON.",
|
|
85
|
+
retryable: false,
|
|
86
|
+
}),
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
const parsed = schema.safeParse(value);
|
|
90
|
+
if (!parsed.success) {
|
|
91
|
+
return {
|
|
92
|
+
ok: false,
|
|
93
|
+
response: instanceErrorResponse({
|
|
94
|
+
code: "argument_invalid",
|
|
95
|
+
message: `Invalid instance request: ${parsed.error.issues[0]?.message ?? "invalid body"}`,
|
|
96
|
+
retryable: false,
|
|
97
|
+
}),
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
return { ok: true, value: parsed.data };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function selfLifecycleError(rawName: string): InstanceLifecycleError | null {
|
|
104
|
+
const current = readManagedInstanceLaunch(process.env);
|
|
105
|
+
return current.kind === "managed" && current.launch.name.toLowerCase() === rawName.toLowerCase()
|
|
106
|
+
? {
|
|
107
|
+
code: "self_lifecycle_unsupported",
|
|
108
|
+
message:
|
|
109
|
+
"A managed runtime cannot stop or restart itself through its own REST/MCP transport; use the CLI or a different controller instance.",
|
|
110
|
+
retryable: false,
|
|
111
|
+
}
|
|
112
|
+
: null;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function instanceListResponse(instances: readonly unknown[]): Response {
|
|
116
|
+
return Response.json(
|
|
117
|
+
{ schemaVersion: INSTANCE_API_SCHEMA_VERSION, instances },
|
|
118
|
+
{ headers: NO_STORE_HEADERS },
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function methodNotAllowed(allow: string): Response {
|
|
123
|
+
return Response.json(
|
|
124
|
+
{ error: "Method not allowed" },
|
|
125
|
+
{ status: 405, headers: { ...NO_STORE_HEADERS, allow } },
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function endpointNotFound(): Response {
|
|
130
|
+
return Response.json(
|
|
131
|
+
{ error: "Instance endpoint not found" },
|
|
132
|
+
{ status: 404, headers: NO_STORE_HEADERS },
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function decodedName(value: string): string | null {
|
|
137
|
+
try {
|
|
138
|
+
return decodeURIComponent(value);
|
|
139
|
+
} catch {
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Framework-neutral named-instance API owned by the Node supervisor plane.
|
|
146
|
+
*
|
|
147
|
+
* The protected ingress uses this when a Rust backend is selected, keeping process lifecycle and
|
|
148
|
+
* OS-specific background behavior in the existing CLI instead of reimplementing it in Rust.
|
|
149
|
+
*/
|
|
150
|
+
export async function handleNamedInstanceApi(request: Request): Promise<Response> {
|
|
151
|
+
const url = new URL(request.url);
|
|
152
|
+
const segments = url.pathname.split("/").filter((segment) => segment.length > 0);
|
|
153
|
+
if (segments.length === 2 && segments[0] === "api" && segments[1] === "instances") {
|
|
154
|
+
if (request.method !== "GET") return methodNotAllowed("GET");
|
|
155
|
+
const result = await listNamedInstances();
|
|
156
|
+
return result.kind === "error"
|
|
157
|
+
? instanceResultResponse(result)
|
|
158
|
+
: instanceListResponse(result.value);
|
|
159
|
+
}
|
|
160
|
+
if (segments.length < 3 || segments[0] !== "api" || segments[1] !== "instances") {
|
|
161
|
+
return endpointNotFound();
|
|
162
|
+
}
|
|
163
|
+
const name = decodedName(segments[2] ?? "");
|
|
164
|
+
if (name === null) {
|
|
165
|
+
return instanceErrorResponse({
|
|
166
|
+
code: "name_invalid",
|
|
167
|
+
message: "Instance name is not valid URL encoding.",
|
|
168
|
+
retryable: false,
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
if (segments.length === 3) {
|
|
172
|
+
return request.method === "GET"
|
|
173
|
+
? instanceResultResponse(await getNamedInstance(name))
|
|
174
|
+
: methodNotAllowed("GET");
|
|
175
|
+
}
|
|
176
|
+
if (segments.length !== 4) return endpointNotFound();
|
|
177
|
+
const action = segments[3];
|
|
178
|
+
if (action === undefined) return endpointNotFound();
|
|
179
|
+
switch (action) {
|
|
180
|
+
case "connection":
|
|
181
|
+
return request.method === "GET"
|
|
182
|
+
? instanceResultResponse(await getNamedInstanceConnection(name))
|
|
183
|
+
: methodNotAllowed("GET");
|
|
184
|
+
case "start": {
|
|
185
|
+
if (request.method !== "POST") return methodNotAllowed("POST");
|
|
186
|
+
const body = await parseInstanceBody(request, InstanceStartRequestSchema);
|
|
187
|
+
if (!body.ok) return body.response;
|
|
188
|
+
const result = await startNamedInstance(name, body.value);
|
|
189
|
+
const status = result.kind === "success" && result.value.outcome === "created" ? 201 : 200;
|
|
190
|
+
return instanceResultResponse(result, status);
|
|
191
|
+
}
|
|
192
|
+
case "stop": {
|
|
193
|
+
if (request.method !== "POST") return methodNotAllowed("POST");
|
|
194
|
+
const body = await parseInstanceBody(request, InstanceEmptyRequestSchema);
|
|
195
|
+
if (!body.ok) return body.response;
|
|
196
|
+
const selfError = selfLifecycleError(name);
|
|
197
|
+
return selfError === null
|
|
198
|
+
? instanceResultResponse(await stopNamedInstance(name))
|
|
199
|
+
: instanceErrorResponse(selfError);
|
|
200
|
+
}
|
|
201
|
+
case "restart": {
|
|
202
|
+
if (request.method !== "POST") return methodNotAllowed("POST");
|
|
203
|
+
const body = await parseInstanceBody(request, InstanceStartRequestSchema);
|
|
204
|
+
if (!body.ok) return body.response;
|
|
205
|
+
const selfError = selfLifecycleError(name);
|
|
206
|
+
return selfError === null
|
|
207
|
+
? instanceResultResponse(await restartNamedInstance(name, body.value))
|
|
208
|
+
: instanceErrorResponse(selfError);
|
|
209
|
+
}
|
|
210
|
+
default:
|
|
211
|
+
return endpointNotFound();
|
|
212
|
+
}
|
|
213
|
+
}
|
package/src/cli/instanceArgs.ts
CHANGED
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
import { normalizePublicBasePath } from "../lib/basePath";
|
|
2
|
+
import {
|
|
3
|
+
BackendImplementationSchema,
|
|
4
|
+
type BackendImplementation,
|
|
5
|
+
} from "../lib/backendImplementation";
|
|
2
6
|
import { isValidBindHost } from "./networkHints";
|
|
3
7
|
|
|
4
8
|
export type InstanceCaptureMode = "simple" | "full";
|
|
5
9
|
|
|
6
10
|
export type InstanceStartOverrides = {
|
|
11
|
+
backend: BackendImplementation | null;
|
|
7
12
|
port: number | null;
|
|
8
13
|
basePath: string | null;
|
|
9
14
|
host: string | null;
|
|
@@ -49,6 +54,7 @@ function optionWithEquals(arg: string): { name: string; value: string } | null {
|
|
|
49
54
|
|
|
50
55
|
function emptyOverrides(): InstanceStartOverrides {
|
|
51
56
|
return {
|
|
57
|
+
backend: null,
|
|
52
58
|
port: null,
|
|
53
59
|
basePath: null,
|
|
54
60
|
host: null,
|
|
@@ -72,6 +78,12 @@ function setValueOption(
|
|
|
72
78
|
value: string | undefined,
|
|
73
79
|
): string | null {
|
|
74
80
|
switch (name) {
|
|
81
|
+
case "--backend": {
|
|
82
|
+
const backend = BackendImplementationSchema.safeParse(value?.trim().toLowerCase());
|
|
83
|
+
if (!backend.success) return "instance: --backend must be typescript or rust";
|
|
84
|
+
options.overrides.backend = backend.data;
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
75
87
|
case "--port":
|
|
76
88
|
case "-p": {
|
|
77
89
|
const port = parsePort(value);
|
|
@@ -162,6 +174,7 @@ export function parseInstanceArgs(argv: readonly string[]): InstanceArgsResult {
|
|
|
162
174
|
break;
|
|
163
175
|
case "--port":
|
|
164
176
|
case "-p":
|
|
177
|
+
case "--backend":
|
|
165
178
|
case "--base-path":
|
|
166
179
|
case "--host":
|
|
167
180
|
case "-H":
|
|
@@ -188,6 +201,7 @@ export function parseInstanceArgs(argv: readonly string[]): InstanceArgsResult {
|
|
|
188
201
|
return { kind: "error", message: "instance: list does not accept an instance name" };
|
|
189
202
|
}
|
|
190
203
|
if (
|
|
204
|
+
options.overrides.backend !== null ||
|
|
191
205
|
options.overrides.port !== null ||
|
|
192
206
|
options.overrides.basePath !== null ||
|
|
193
207
|
options.overrides.host !== null ||
|
|
@@ -211,6 +225,7 @@ export function parseInstanceArgs(argv: readonly string[]): InstanceArgsResult {
|
|
|
211
225
|
return { kind: "error", message: `instance: ${command} requires exactly one instance name` };
|
|
212
226
|
}
|
|
213
227
|
if (
|
|
228
|
+
options.overrides.backend !== null ||
|
|
214
229
|
options.overrides.port !== null ||
|
|
215
230
|
options.overrides.basePath !== null ||
|
|
216
231
|
options.overrides.host !== null ||
|
|
@@ -263,6 +278,7 @@ Usage:
|
|
|
263
278
|
agent-inspector instance restart <name> [options]
|
|
264
279
|
|
|
265
280
|
Start/restart options:
|
|
281
|
+
--backend <typescript|rust> Persist the backend implementation (default: rust when native package is available)
|
|
266
282
|
--port, -p <port> Public ingress port (auto-allocated on first start)
|
|
267
283
|
--base-path <path> Public Base Path (default: /inspector; use / for root)
|
|
268
284
|
--host, -H <host> Bind host (default: 127.0.0.1)
|
|
@@ -3,12 +3,17 @@ import {
|
|
|
3
3
|
MANAGED_INSTANCE_SHUTDOWN_PATH,
|
|
4
4
|
MANAGED_INSTANCE_TOKEN_HEADER,
|
|
5
5
|
} from "../lib/managedInstance";
|
|
6
|
+
import {
|
|
7
|
+
BackendImplementationSchema,
|
|
8
|
+
type BackendImplementation,
|
|
9
|
+
} from "../lib/backendImplementation";
|
|
6
10
|
|
|
7
11
|
const DEFAULT_CONTROL_TIMEOUT_MS = 2_000;
|
|
8
12
|
|
|
9
13
|
export type InstanceControlFetch = (input: string, init?: RequestInit) => Promise<Response>;
|
|
10
14
|
|
|
11
15
|
export type ExpectedInstanceControlIdentity = {
|
|
16
|
+
backend?: BackendImplementation;
|
|
12
17
|
instanceId: string;
|
|
13
18
|
launchId: string;
|
|
14
19
|
controlToken: string;
|
|
@@ -18,6 +23,7 @@ export type ExpectedInstanceControlIdentity = {
|
|
|
18
23
|
|
|
19
24
|
export type InstanceControlIdentity = {
|
|
20
25
|
schemaVersion: 1;
|
|
26
|
+
backend: BackendImplementation;
|
|
21
27
|
instanceId: string;
|
|
22
28
|
launchId: string;
|
|
23
29
|
supervisorPid: number;
|
|
@@ -45,6 +51,7 @@ function isRecord(value: unknown): value is Record<string, unknown> {
|
|
|
45
51
|
function parseIdentity(value: unknown): InstanceControlIdentity | null {
|
|
46
52
|
if (!isRecord(value)) return null;
|
|
47
53
|
const schemaVersion = value["schemaVersion"];
|
|
54
|
+
const backend = BackendImplementationSchema.safeParse(value["backend"] ?? "typescript");
|
|
48
55
|
const instanceId = value["instanceId"];
|
|
49
56
|
const launchId = value["launchId"];
|
|
50
57
|
const supervisorPid = value["supervisorPid"];
|
|
@@ -53,6 +60,7 @@ function parseIdentity(value: unknown): InstanceControlIdentity | null {
|
|
|
53
60
|
const upstreamPort = value["upstreamPort"];
|
|
54
61
|
if (
|
|
55
62
|
schemaVersion !== 1 ||
|
|
63
|
+
!backend.success ||
|
|
56
64
|
typeof instanceId !== "string" ||
|
|
57
65
|
instanceId.length === 0 ||
|
|
58
66
|
typeof launchId !== "string" ||
|
|
@@ -75,6 +83,7 @@ function parseIdentity(value: unknown): InstanceControlIdentity | null {
|
|
|
75
83
|
}
|
|
76
84
|
return {
|
|
77
85
|
schemaVersion: 1,
|
|
86
|
+
backend: backend.data,
|
|
78
87
|
instanceId,
|
|
79
88
|
launchId,
|
|
80
89
|
supervisorPid,
|
|
@@ -136,6 +145,7 @@ export async function probeInstanceControl(
|
|
|
136
145
|
if (
|
|
137
146
|
identity.instanceId !== expected.instanceId ||
|
|
138
147
|
identity.launchId !== expected.launchId ||
|
|
148
|
+
identity.backend !== (expected.backend ?? "typescript") ||
|
|
139
149
|
identity.publicPort !== expected.publicPort
|
|
140
150
|
) {
|
|
141
151
|
return { kind: "identity-mismatch" };
|
package/src/cli/instanceModel.ts
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import { randomBytes, randomUUID } from "node:crypto";
|
|
2
2
|
import { isAbsolute } from "node:path";
|
|
3
3
|
import { z } from "zod";
|
|
4
|
+
import {
|
|
5
|
+
BackendImplementationSchema,
|
|
6
|
+
type BackendImplementation,
|
|
7
|
+
} from "../lib/backendImplementation";
|
|
4
8
|
import { normalizePublicBasePath } from "../lib/basePath";
|
|
5
9
|
import { MANAGED_INSTANCE_CONTROL_PATH } from "../lib/managedInstance";
|
|
6
10
|
import { CaptureModeSchema, type CaptureMode } from "../lib/runtimeConfig";
|
|
@@ -124,6 +128,7 @@ export const InstanceRecordSchema = z
|
|
|
124
128
|
upstreamPort: PortSchema,
|
|
125
129
|
basePath: NormalizedBasePathSchema,
|
|
126
130
|
captureMode: CaptureModeSchema,
|
|
131
|
+
backend: BackendImplementationSchema.default("typescript"),
|
|
127
132
|
uiEnabled: z.boolean().default(true),
|
|
128
133
|
dataDir: z
|
|
129
134
|
.string()
|
|
@@ -156,6 +161,7 @@ export type InstanceRecordOutput = {
|
|
|
156
161
|
upstreamPort: number;
|
|
157
162
|
basePath: string;
|
|
158
163
|
captureMode: CaptureMode;
|
|
164
|
+
backend: BackendImplementation;
|
|
159
165
|
uiEnabled: boolean;
|
|
160
166
|
dataDir: string;
|
|
161
167
|
launch: {
|
|
@@ -171,6 +177,7 @@ export type CreateInstanceRecordInput = {
|
|
|
171
177
|
upstreamPort: number;
|
|
172
178
|
basePath?: string;
|
|
173
179
|
captureMode: CaptureMode;
|
|
180
|
+
backend?: BackendImplementation;
|
|
174
181
|
uiEnabled?: boolean;
|
|
175
182
|
dataDir: string;
|
|
176
183
|
};
|
|
@@ -212,6 +219,7 @@ export function createInstanceRecord(
|
|
|
212
219
|
upstreamPort: input.upstreamPort,
|
|
213
220
|
basePath: basePath.value,
|
|
214
221
|
captureMode: input.captureMode,
|
|
222
|
+
backend: input.backend ?? "typescript",
|
|
215
223
|
uiEnabled: input.uiEnabled ?? true,
|
|
216
224
|
dataDir: input.dataDir,
|
|
217
225
|
launch: createLaunch(factory, now),
|
|
@@ -257,6 +265,7 @@ export function toInstanceRecordOutput(record: InstanceRecord): InstanceRecordOu
|
|
|
257
265
|
upstreamPort: record.upstreamPort,
|
|
258
266
|
basePath: record.basePath,
|
|
259
267
|
captureMode: record.captureMode,
|
|
268
|
+
backend: record.backend,
|
|
260
269
|
uiEnabled: record.uiEnabled,
|
|
261
270
|
dataDir: record.dataDir,
|
|
262
271
|
launch: {
|