@opengeni/runtime 0.13.0 → 0.13.4
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/{chunk-65UDUFUO.js → chunk-JGUQSAG5.js} +1887 -172
- package/dist/chunk-JGUQSAG5.js.map +1 -0
- package/dist/{chunk-HAN5D27J.js → chunk-QYXOVIUK.js} +24 -4
- package/dist/chunk-QYXOVIUK.js.map +1 -0
- package/dist/{index-DGUrY_yR.d.ts → index-DU-KXVlx.d.ts} +328 -25
- package/dist/index.d.ts +25 -3
- package/dist/index.js +71 -675
- package/dist/index.js.map +1 -1
- package/dist/mcp-network.d.ts +4 -1
- package/dist/mcp-network.js +5 -1
- package/dist/sandbox/index.d.ts +1 -1
- package/dist/sandbox/index.js +39 -1
- package/package.json +3 -3
- package/src/index.ts +31 -9
- package/src/mcp-network.ts +28 -3
- package/src/sandbox/channel-a.ts +92 -46
- package/src/sandbox/exec-banner.ts +15 -0
- package/src/sandbox/index.ts +360 -145
- package/src/sandbox/provider-errors.ts +175 -0
- package/src/sandbox/routing/backend-resolver.ts +15 -0
- package/src/sandbox/routing/routing-session.ts +730 -36
- package/src/sandbox/select.ts +8 -0
- package/src/sandbox/turn-tool-cancellation.ts +366 -92
- package/src/sandbox/workspace-archive.ts +289 -0
- package/dist/chunk-65UDUFUO.js.map +0 -1
- package/dist/chunk-HAN5D27J.js.map +0 -1
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Provider sandbox failure classification.
|
|
3
|
+
*
|
|
4
|
+
* This is deliberately structural and fail-closed. DNS failures often contain
|
|
5
|
+
* the word "not found" (`ENOTFOUND`) while gRPC status values are also used by
|
|
6
|
+
* unrelated SDKs and command routers. A bare numeric status (including gRPC's
|
|
7
|
+
* numeric 5) is therefore not enough to retire a provider instance. Only
|
|
8
|
+
* authoritative, non-transient evidence may retire a provider instance and
|
|
9
|
+
* license lease recovery.
|
|
10
|
+
*/
|
|
11
|
+
export type ProviderSandboxFailureKind = "not_found" | "transient_transport" | "other";
|
|
12
|
+
|
|
13
|
+
export type ProviderSandboxFailure = {
|
|
14
|
+
kind: ProviderSandboxFailureKind;
|
|
15
|
+
/** Bounded, non-secret diagnostic suitable for logs and lifecycle events. */
|
|
16
|
+
diagnostic: string;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const GRPC_TRANSIENT = new Set([1, 2, 4, 13, 14]);
|
|
20
|
+
const TRANSIENT_CODES = new Set([
|
|
21
|
+
"CANCELLED",
|
|
22
|
+
"UNKNOWN",
|
|
23
|
+
"DEADLINE_EXCEEDED",
|
|
24
|
+
"INTERNAL",
|
|
25
|
+
"UNAVAILABLE",
|
|
26
|
+
"ECONNRESET",
|
|
27
|
+
"ECONNREFUSED",
|
|
28
|
+
"ETIMEDOUT",
|
|
29
|
+
"EAI_AGAIN",
|
|
30
|
+
"ENOTFOUND",
|
|
31
|
+
]);
|
|
32
|
+
const NOT_FOUND_CODES = new Set(["404", "NOT_FOUND", "RESOURCE_NOT_FOUND", "SANDBOX_NOT_FOUND"]);
|
|
33
|
+
const SAFE_CODE = /^[A-Z0-9._:-]{1,64}$/;
|
|
34
|
+
|
|
35
|
+
type ErrorSignals = {
|
|
36
|
+
numbers: number[];
|
|
37
|
+
codes: string[];
|
|
38
|
+
messages: string[];
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
function safeRead(record: object, key: string): unknown {
|
|
42
|
+
try {
|
|
43
|
+
return Reflect.get(record, key);
|
|
44
|
+
} catch {
|
|
45
|
+
return undefined;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function collectSignals(value: unknown): ErrorSignals {
|
|
50
|
+
const out: ErrorSignals = { numbers: [], codes: [], messages: [] };
|
|
51
|
+
const seen = new Set<object>();
|
|
52
|
+
const visit = (current: unknown, depth: number): void => {
|
|
53
|
+
if (depth > 3 || current === null || current === undefined) return;
|
|
54
|
+
if (typeof current === "string") {
|
|
55
|
+
if (current.length <= 512) out.messages.push(current);
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
if (typeof current !== "object" || seen.has(current)) return;
|
|
59
|
+
seen.add(current);
|
|
60
|
+
for (const key of [
|
|
61
|
+
"status",
|
|
62
|
+
"statusCode",
|
|
63
|
+
"httpStatus",
|
|
64
|
+
"httpStatusCode",
|
|
65
|
+
"responseStatus",
|
|
66
|
+
"errorCode",
|
|
67
|
+
"code",
|
|
68
|
+
]) {
|
|
69
|
+
const signal = safeRead(current, key);
|
|
70
|
+
if (typeof signal === "number" && Number.isFinite(signal)) out.numbers.push(signal);
|
|
71
|
+
if (typeof signal === "string") {
|
|
72
|
+
const code = signal.trim().toUpperCase();
|
|
73
|
+
if (SAFE_CODE.test(code)) out.codes.push(code);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
for (const key of ["name", "message", "details", "cause"]) {
|
|
77
|
+
const signal = safeRead(current, key);
|
|
78
|
+
if (typeof signal === "string" && signal.length <= 512) out.messages.push(signal);
|
|
79
|
+
else visit(signal, depth + 1);
|
|
80
|
+
}
|
|
81
|
+
visit(safeRead(current, "response"), depth + 1);
|
|
82
|
+
visit(safeRead(current, "error"), depth + 1);
|
|
83
|
+
};
|
|
84
|
+
visit(value, 0);
|
|
85
|
+
return out;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function safeDiagnostic(signals: ErrorSignals): string {
|
|
89
|
+
const code = signals.codes.find(Boolean);
|
|
90
|
+
const numeric = signals.numbers.find((value) => Number.isFinite(value));
|
|
91
|
+
return [code ? `code=${code}` : "", numeric !== undefined ? `status=${numeric}` : ""]
|
|
92
|
+
.filter(Boolean)
|
|
93
|
+
.join(" ");
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function hasTransientSignal(signals: ErrorSignals): boolean {
|
|
97
|
+
return (
|
|
98
|
+
signals.codes.some((code) => TRANSIENT_CODES.has(code)) ||
|
|
99
|
+
signals.numbers.some((status) => GRPC_TRANSIENT.has(status))
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function hasModalTerminalSandboxMessage(signals: ErrorSignals): boolean {
|
|
104
|
+
return signals.messages.some(
|
|
105
|
+
(message) =>
|
|
106
|
+
/^Modal sandbox [A-Za-z0-9_-]+ is no longer running\.$/.test(message) ||
|
|
107
|
+
/^Modal sandbox [A-Za-z0-9_-]+ not found \(has been terminated\)$/.test(message),
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** Typed transport evidence always dominates nested/string NotFound text. */
|
|
112
|
+
export function classifyProviderSandboxFailure(
|
|
113
|
+
backendId: string,
|
|
114
|
+
error: unknown,
|
|
115
|
+
): ProviderSandboxFailure {
|
|
116
|
+
if (backendId === "selfhosted" || !error) {
|
|
117
|
+
return { kind: "other", diagnostic: "selfhosted-or-empty" };
|
|
118
|
+
}
|
|
119
|
+
const signals = collectSignals(error);
|
|
120
|
+
const diagnostic = safeDiagnostic(signals) || "unclassified_provider_failure";
|
|
121
|
+
|
|
122
|
+
if (hasTransientSignal(signals)) {
|
|
123
|
+
return { kind: "transient_transport", diagnostic };
|
|
124
|
+
}
|
|
125
|
+
if (
|
|
126
|
+
signals.numbers.some((status) => status === 404) ||
|
|
127
|
+
signals.codes.some((code) => NOT_FOUND_CODES.has(code))
|
|
128
|
+
) {
|
|
129
|
+
return { kind: "not_found", diagnostic };
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Modal's SDK emits these exact terminal grammars after resolving one exact
|
|
133
|
+
// sandbox id. Generic "not found" prose is intentionally not authoritative.
|
|
134
|
+
if (backendId === "modal" && hasModalTerminalSandboxMessage(signals)) {
|
|
135
|
+
return {
|
|
136
|
+
kind: "not_found",
|
|
137
|
+
diagnostic:
|
|
138
|
+
diagnostic === "unclassified_provider_failure"
|
|
139
|
+
? "modal_terminal_message"
|
|
140
|
+
: `${diagnostic} modal_terminal_message`,
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return { kind: "other", diagnostic };
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export function isProviderSandboxNotFoundError(backendId: string, error: unknown): boolean {
|
|
148
|
+
return classifyProviderSandboxFailure(backendId, error).kind === "not_found";
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Whether a failure from an operation routed through an already-established
|
|
153
|
+
* sandbox proves that exact provider sandbox disappeared.
|
|
154
|
+
*
|
|
155
|
+
* Routed operations can legitimately return HTTP 404 / `NOT_FOUND` for paths,
|
|
156
|
+
* files, ports, processes, and other subresources. Those generic signals are
|
|
157
|
+
* sufficient for provider create/resume APIs, but they must never retire the
|
|
158
|
+
* live sandbox identity after an arbitrary operation. Only an explicit
|
|
159
|
+
* sandbox-scoped provider code or Modal's exact terminal sandbox grammar is
|
|
160
|
+
* authoritative on this path. Transient transport evidence still dominates.
|
|
161
|
+
*/
|
|
162
|
+
export function isProviderSandboxGoneDuringRoutedOperation(
|
|
163
|
+
backendId: string,
|
|
164
|
+
error: unknown,
|
|
165
|
+
): boolean {
|
|
166
|
+
if (backendId === "selfhosted" || !error) return false;
|
|
167
|
+
const signals = collectSignals(error);
|
|
168
|
+
if (hasTransientSignal(signals)) return false;
|
|
169
|
+
if (signals.codes.includes("SANDBOX_NOT_FOUND")) return true;
|
|
170
|
+
return backendId === "modal" && hasModalTerminalSandboxMessage(signals);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export function isProviderSandboxTransientError(backendId: string, error: unknown): boolean {
|
|
174
|
+
return classifyProviderSandboxFailure(backendId, error).kind === "transient_transport";
|
|
175
|
+
}
|
|
@@ -47,6 +47,18 @@ export interface ActiveBackendResolverDeps {
|
|
|
47
47
|
defaultBackend: RoutableBackendSession;
|
|
48
48
|
/** A label for the default backend (its backend id: "modal"/"selfhosted"/…). */
|
|
49
49
|
defaultKind: string;
|
|
50
|
+
/**
|
|
51
|
+
* Re-resolve the session's HOME backend after the route cache is fenced.
|
|
52
|
+
*
|
|
53
|
+
* The normal path returns `defaultBackend` (the turn's already-established
|
|
54
|
+
* handle), but an API-side same-target repair can rematerialize that lease
|
|
55
|
+
* while a worker turn is still alive. Such a repair advances the route epoch
|
|
56
|
+
* without changing the null pointer; callers that can resume the current
|
|
57
|
+
* lease-owned identity may provide this callback so the next epoch does not
|
|
58
|
+
* keep dispatching to the dead pre-repair object. This callback must never
|
|
59
|
+
* create a provider or replay an operation.
|
|
60
|
+
*/
|
|
61
|
+
resolveDefaultBackend?: (pointer: ActivePointer) => Promise<ResolvedActiveBackend>;
|
|
50
62
|
/** Look up a first-class sandbox by id (the swap target). Returns null when the
|
|
51
63
|
* id is unknown or not in this workspace (the caller 409s the swap). */
|
|
52
64
|
getSandbox(sandboxId: string): Promise<RoutableSandbox | null>;
|
|
@@ -236,6 +248,9 @@ export function makeActiveBackendResolver(
|
|
|
236
248
|
"the active sandbox was detached to the session default (home), but this turn started pinned to a machine and established no home box; the detach is accepted and takes effect on the next turn — this turn has no active home box",
|
|
237
249
|
);
|
|
238
250
|
}
|
|
251
|
+
if (deps.resolveDefaultBackend) {
|
|
252
|
+
return await deps.resolveDefaultBackend(pointer);
|
|
253
|
+
}
|
|
239
254
|
return { session: deps.defaultBackend, sandboxId: null, kind: deps.defaultKind };
|
|
240
255
|
}
|
|
241
256
|
|