@opengeni/runtime 3.0.0-canary.0 → 3.0.1-canary.0
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-46BFUGFL.js → chunk-FQM5VV5D.js} +243 -49
- package/dist/chunk-FQM5VV5D.js.map +1 -0
- package/dist/{chunk-46VVJC6K.js → chunk-KBYDX5N6.js} +21 -11
- package/dist/chunk-KBYDX5N6.js.map +1 -0
- package/dist/index.js +6 -2
- package/dist/operational-instructions.d.ts +1 -1
- package/dist/sandbox/index.d.ts +1 -0
- package/dist/sandbox/index.js +5 -1
- package/dist/sandbox/materialization-verification-error.d.ts +27 -0
- package/dist/sandbox/providers/modal-command-control.d.ts +6 -0
- package/dist/sandbox/providers/modal-command-session.d.ts +1 -0
- package/dist/sandbox/providers/modal-materialization-verification.d.ts +7 -0
- package/dist/sandbox/routing/routing-session.d.ts +4 -0
- package/dist/workspace-tool-gateway.js +2 -2
- package/package.json +11 -11
- package/src/bundled_default_skills/opengeni-help/SKILL.md +103 -0
- package/src/index.ts +9 -4
- package/src/operational-instructions.ts +2 -0
- package/src/sandbox/index.ts +6 -0
- package/src/sandbox/materialization-verification-error.ts +65 -0
- package/src/sandbox/providers/modal-command-control.ts +49 -4
- package/src/sandbox/providers/modal-command-session.ts +4 -0
- package/src/sandbox/providers/modal-materialization-verification.ts +122 -0
- package/src/sandbox/routing/routing-session.ts +75 -10
- package/dist/chunk-46BFUGFL.js.map +0 -1
- package/dist/chunk-46VVJC6K.js.map +0 -1
|
@@ -217,12 +217,36 @@ export class ModalCommandControl {
|
|
|
217
217
|
command: ModalProviderCommand,
|
|
218
218
|
yieldTimeMs: number,
|
|
219
219
|
signal?: AbortSignal,
|
|
220
|
+
): Promise<ModalProviderOutputPage> {
|
|
221
|
+
return this.readOutput(command, yieldTimeMs, signal);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/** Fixed internal probes own their cancellation controller and must drain
|
|
225
|
+
* both reads before relinquishing the surrounding provider-operation gate.
|
|
226
|
+
* Retained-command readers keep their existing signal/receipt contract.
|
|
227
|
+
*/
|
|
228
|
+
async readProbe(
|
|
229
|
+
command: ModalProviderCommand,
|
|
230
|
+
yieldTimeMs: number,
|
|
231
|
+
cancellation: AbortController,
|
|
232
|
+
): Promise<ModalProviderOutputPage> {
|
|
233
|
+
return this.readOutput(command, yieldTimeMs, cancellation.signal, (error) =>
|
|
234
|
+
cancellation.abort(error),
|
|
235
|
+
);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
private async readOutput(
|
|
239
|
+
command: ModalProviderCommand,
|
|
240
|
+
yieldTimeMs: number,
|
|
241
|
+
signal?: AbortSignal,
|
|
242
|
+
cancelSiblings?: (error: unknown) => void,
|
|
220
243
|
): Promise<ModalProviderOutputPage> {
|
|
221
244
|
signal?.throwIfAborted();
|
|
222
245
|
this.assertIdentity(command);
|
|
223
246
|
const next = structuredClone(command);
|
|
224
|
-
|
|
225
|
-
|
|
247
|
+
let failure: { error: unknown } | undefined;
|
|
248
|
+
const reads = (["stdout", "stderr"] as const)
|
|
249
|
+
.map(async (stream) => {
|
|
226
250
|
const cursor = command.streams[stream];
|
|
227
251
|
if (cursor.exitCode !== null) return null;
|
|
228
252
|
const descriptor = stream === "stdout" ? 1 : 2;
|
|
@@ -261,8 +285,29 @@ export class ModalCommandControl {
|
|
|
261
285
|
};
|
|
262
286
|
}
|
|
263
287
|
return null;
|
|
264
|
-
})
|
|
265
|
-
|
|
288
|
+
})
|
|
289
|
+
.map((read) =>
|
|
290
|
+
!cancelSiblings
|
|
291
|
+
? read
|
|
292
|
+
: read.catch((error: unknown) => {
|
|
293
|
+
// A logical read owns both output streams. Promise.all alone releases
|
|
294
|
+
// its caller on the first rejection while a sibling RPC may still be
|
|
295
|
+
// running. Cancel that sibling, retain the original error, and drain
|
|
296
|
+
// both reads before returning control to mutation/capture ownership.
|
|
297
|
+
failure ??= { error };
|
|
298
|
+
cancelSiblings(error);
|
|
299
|
+
throw error;
|
|
300
|
+
}),
|
|
301
|
+
);
|
|
302
|
+
const pages = cancelSiblings
|
|
303
|
+
? await Promise.allSettled(reads).then((results) => {
|
|
304
|
+
if (failure) throw failure.error;
|
|
305
|
+
return results.map((result) => {
|
|
306
|
+
if (result.status === "rejected") throw result.reason;
|
|
307
|
+
return result.value;
|
|
308
|
+
});
|
|
309
|
+
})
|
|
310
|
+
: await Promise.all(reads);
|
|
266
311
|
const stdoutExit = next.streams.stdout.exitCode;
|
|
267
312
|
const stderrExit = next.streams.stderr.exitCode;
|
|
268
313
|
if (stdoutExit !== null && stderrExit !== null && stdoutExit !== stderrExit)
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
admittedProviderCommandHandle,
|
|
12
12
|
} from "../provider-command-session";
|
|
13
13
|
import type { ModalCommandControl, ModalProviderCommand } from "./modal-command-control";
|
|
14
|
+
import { verifyModalMaterializedPath } from "./modal-materialization-verification";
|
|
14
15
|
|
|
15
16
|
type Entry = {
|
|
16
17
|
command: ModalProviderCommand;
|
|
@@ -33,6 +34,7 @@ export function installModalCommandSession(
|
|
|
33
34
|
session: ProviderCommandSession & {
|
|
34
35
|
execCommand?: ChannelASession["execCommand"];
|
|
35
36
|
writeStdin?: ChannelASession["writeStdin"];
|
|
37
|
+
verifyMaterializedPath?: (path: string, workdir: string) => Promise<void>;
|
|
36
38
|
},
|
|
37
39
|
control: ModalCommandControl,
|
|
38
40
|
): void {
|
|
@@ -41,6 +43,8 @@ export function installModalCommandSession(
|
|
|
41
43
|
const originalWrite = session.writeStdin?.bind(session);
|
|
42
44
|
const cancelLegacyStart = session.cancelPendingExecCommand?.bind(session);
|
|
43
45
|
const pendingStarts = new Set<AbortController>();
|
|
46
|
+
session.verifyMaterializedPath = (path, workdir) =>
|
|
47
|
+
verifyModalMaterializedPath(control, path, workdir, pendingStarts);
|
|
44
48
|
const entries = new Map<number, Entry>();
|
|
45
49
|
// Current SDK setup commands are not retained/admitted commands. Give their
|
|
46
50
|
// live observer handles a disjoint, adapter-local range; never use a missing
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import {
|
|
2
|
+
SandboxMaterializationVerificationError,
|
|
3
|
+
retainMaterializationVerificationDiagnostic,
|
|
4
|
+
type MaterializationVerificationDiagnostic,
|
|
5
|
+
} from "../materialization-verification-error";
|
|
6
|
+
import type { ModalCommandControl, ModalProviderCommand } from "./modal-command-control";
|
|
7
|
+
|
|
8
|
+
const MARKER = "__OPENGENI_MATERIALIZED_PATH_VISIBLE__";
|
|
9
|
+
const OUTPUT_LIMIT = 16 * 1024;
|
|
10
|
+
|
|
11
|
+
/** A fixed read-only probe, not an admitted user command. Advance its private
|
|
12
|
+
* provider cursor locally; never borrow the surrounding mutation's retained
|
|
13
|
+
* handle or relax retained-command persistence requirements. The caller keeps
|
|
14
|
+
* the original backend and provider-operation gate for the entire observation.
|
|
15
|
+
*/
|
|
16
|
+
export async function verifyModalMaterializedPath(
|
|
17
|
+
control: Pick<ModalCommandControl, "start" | "readProbe">,
|
|
18
|
+
path: string,
|
|
19
|
+
workdir: string,
|
|
20
|
+
pending: Set<AbortController>,
|
|
21
|
+
observationTimeoutMs = 30_000,
|
|
22
|
+
): Promise<void> {
|
|
23
|
+
const quote = (value: string) => `'${value.replaceAll("'", `'"'"'`)}'`;
|
|
24
|
+
const command = `test -e ${quote(path)} && printf %s ${quote(MARKER)}`;
|
|
25
|
+
const cancellation = new AbortController();
|
|
26
|
+
const deadlineReason = new Error("Materialization visibility observation deadline exceeded");
|
|
27
|
+
let providerCommand: ModalProviderCommand | undefined;
|
|
28
|
+
let stdout = "";
|
|
29
|
+
let stderr = "";
|
|
30
|
+
const diagnostic = (
|
|
31
|
+
reason: MaterializationVerificationDiagnostic["reason"],
|
|
32
|
+
exitCode: number | null = null,
|
|
33
|
+
): MaterializationVerificationDiagnostic => ({
|
|
34
|
+
reason,
|
|
35
|
+
path,
|
|
36
|
+
workdir,
|
|
37
|
+
command,
|
|
38
|
+
output: (stdout + stderr).slice(0, OUTPUT_LIMIT),
|
|
39
|
+
exitCode,
|
|
40
|
+
providerSessionId: null,
|
|
41
|
+
...(providerCommand
|
|
42
|
+
? {
|
|
43
|
+
providerExecution: {
|
|
44
|
+
sandboxId: providerCommand.sandboxId,
|
|
45
|
+
taskId: providerCommand.taskId,
|
|
46
|
+
execId: providerCommand.execId,
|
|
47
|
+
},
|
|
48
|
+
}
|
|
49
|
+
: {}),
|
|
50
|
+
});
|
|
51
|
+
const timer = setTimeout(() => {
|
|
52
|
+
cancellation.abort(deadlineReason);
|
|
53
|
+
}, observationTimeoutMs);
|
|
54
|
+
timer.unref();
|
|
55
|
+
pending.add(cancellation);
|
|
56
|
+
try {
|
|
57
|
+
providerCommand = await control.start(
|
|
58
|
+
{ cmd: command, workdir, shell: "sh", login: false, tty: false },
|
|
59
|
+
cancellation.signal,
|
|
60
|
+
);
|
|
61
|
+
for (;;) {
|
|
62
|
+
cancellation.signal.throwIfAborted();
|
|
63
|
+
const page = await control.readProbe(providerCommand, 1_000, cancellation);
|
|
64
|
+
// Cancellation ends observation, not the process. Never accept a late
|
|
65
|
+
// success after the attempt requested cancellation.
|
|
66
|
+
cancellation.signal.throwIfAborted();
|
|
67
|
+
if (
|
|
68
|
+
page.command.sandboxId !== providerCommand.sandboxId ||
|
|
69
|
+
page.command.taskId !== providerCommand.taskId ||
|
|
70
|
+
page.command.execId !== providerCommand.execId
|
|
71
|
+
) {
|
|
72
|
+
throw new SandboxMaterializationVerificationError(diagnostic("invalid_response"));
|
|
73
|
+
}
|
|
74
|
+
providerCommand = page.command;
|
|
75
|
+
for (const chunk of page.chunks) {
|
|
76
|
+
if (chunk.stream === "stdout") stdout += chunk.text;
|
|
77
|
+
else stderr += chunk.text;
|
|
78
|
+
}
|
|
79
|
+
if (stdout.length + stderr.length > OUTPUT_LIMIT) {
|
|
80
|
+
throw new SandboxMaterializationVerificationError(diagnostic("invalid_response"));
|
|
81
|
+
}
|
|
82
|
+
if (page.exitCode === null) {
|
|
83
|
+
// An empty provider page may return immediately. Yield to cancellation
|
|
84
|
+
// and the deadline instead of starving timers with resolved promises.
|
|
85
|
+
await new Promise<void>((resolve) => setImmediate(resolve));
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
if (page.exitCode === 0 && stdout === MARKER) return;
|
|
89
|
+
throw new SandboxMaterializationVerificationError(
|
|
90
|
+
diagnostic(
|
|
91
|
+
page.exitCode === 1
|
|
92
|
+
? "path_not_visible"
|
|
93
|
+
: page.exitCode === 0
|
|
94
|
+
? "invalid_response"
|
|
95
|
+
: "command_failed",
|
|
96
|
+
page.exitCode,
|
|
97
|
+
),
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
} catch (error) {
|
|
101
|
+
if (cancellation.signal.reason === deadlineReason) {
|
|
102
|
+
throw new SandboxMaterializationVerificationError(
|
|
103
|
+
{
|
|
104
|
+
...diagnostic("command_pending"),
|
|
105
|
+
causeMessage: "Observation deadline exceeded; provider process completion is unconfirmed",
|
|
106
|
+
},
|
|
107
|
+
{ cause: error },
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
if (!(error instanceof SandboxMaterializationVerificationError)) {
|
|
111
|
+
retainMaterializationVerificationDiagnostic(error, {
|
|
112
|
+
...diagnostic("command_error"),
|
|
113
|
+
causeMessage: error instanceof Error ? error.message : String(error),
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
throw error;
|
|
117
|
+
} finally {
|
|
118
|
+
cancellation.abort(new Error("Materialization visibility observation finished"));
|
|
119
|
+
clearTimeout(timer);
|
|
120
|
+
pending.delete(cancellation);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
@@ -34,7 +34,13 @@ import {
|
|
|
34
34
|
type ProviderCommandSession,
|
|
35
35
|
} from "../provider-command-session";
|
|
36
36
|
import type { SandboxProviderCommand } from "@opengeni/contracts";
|
|
37
|
-
import { hasTypedExecHandleLoss } from "../exec-banner";
|
|
37
|
+
import { hasTypedExecHandleLoss, parseExecResponseBanner } from "../exec-banner";
|
|
38
|
+
import {
|
|
39
|
+
SandboxMaterializationVerificationError,
|
|
40
|
+
materializationVerificationDiagnostic,
|
|
41
|
+
retainMaterializationVerificationDiagnostic,
|
|
42
|
+
type MaterializationFailureReason,
|
|
43
|
+
} from "../materialization-verification-error";
|
|
38
44
|
import { CAPABILITY_DESCRIPTORS, type SandboxBackend } from "@opengeni/contracts";
|
|
39
45
|
import { SelfhostedControlError } from "../selfhosted/control-rpc";
|
|
40
46
|
import {
|
|
@@ -102,6 +108,8 @@ export interface RoutableBackendSession extends ProviderCommandSession {
|
|
|
102
108
|
pathExists?(path: string, runAs?: string): Promise<boolean>;
|
|
103
109
|
viewImage?(args: unknown): Promise<unknown>;
|
|
104
110
|
materializeEntry?(args: unknown): Promise<void>;
|
|
111
|
+
/** Provider-owned fixed read-only probe; never an agent command surface. */
|
|
112
|
+
verifyMaterializedPath?(path: string, workdir: string): Promise<void>;
|
|
105
113
|
supportsPty?(): boolean;
|
|
106
114
|
resolveExposedPort?(port: number): Promise<ExposedPortEndpoint>;
|
|
107
115
|
serializeSessionState?(): Promise<unknown>;
|
|
@@ -299,6 +307,7 @@ export type RoutingSandboxOperationObservation = {
|
|
|
299
307
|
op: string;
|
|
300
308
|
outcome: "ok" | "not_found" | "failed";
|
|
301
309
|
durationMs: number;
|
|
310
|
+
materializationFailureReason?: MaterializationFailureReason;
|
|
302
311
|
};
|
|
303
312
|
|
|
304
313
|
export type RoutingSandboxOperationObserver = (
|
|
@@ -1689,11 +1698,13 @@ export class RoutingSandboxSession implements RoutableBackendSession {
|
|
|
1689
1698
|
): Promise<T> {
|
|
1690
1699
|
const startedAt = performance.now();
|
|
1691
1700
|
let outcome: RoutingSandboxOperationObservation["outcome"] = "failed";
|
|
1701
|
+
let materializationFailureReason: MaterializationFailureReason | undefined;
|
|
1692
1702
|
try {
|
|
1693
1703
|
const result = await withSandboxProviderOperation(backend.session, fn, onCaptureWait);
|
|
1694
1704
|
outcome = "ok";
|
|
1695
1705
|
return result;
|
|
1696
1706
|
} catch (error) {
|
|
1707
|
+
materializationFailureReason = materializationVerificationDiagnostic(error)?.reason;
|
|
1697
1708
|
if (
|
|
1698
1709
|
(op === "readFile" || op === "listDir" || op === "pathExists" || op === "viewImage") &&
|
|
1699
1710
|
isDefinitePathNotFoundError(error)
|
|
@@ -1708,6 +1719,7 @@ export class RoutingSandboxSession implements RoutableBackendSession {
|
|
|
1708
1719
|
op,
|
|
1709
1720
|
outcome,
|
|
1710
1721
|
durationMs: Math.max(0, performance.now() - startedAt),
|
|
1722
|
+
...(materializationFailureReason ? { materializationFailureReason } : {}),
|
|
1711
1723
|
});
|
|
1712
1724
|
} catch {
|
|
1713
1725
|
// Telemetry is never part of provider or durable-settlement authority.
|
|
@@ -2289,6 +2301,9 @@ async function assertProviderCanSeeMaterializedPath(
|
|
|
2289
2301
|
session: RoutableBackendSession,
|
|
2290
2302
|
path: string,
|
|
2291
2303
|
): Promise<void> {
|
|
2304
|
+
if (session.verifyMaterializedPath) {
|
|
2305
|
+
return session.verifyMaterializedPath(path, providerManifestRoot(session) ?? "/workspace");
|
|
2306
|
+
}
|
|
2292
2307
|
const command = `test -e ${shellSingleQuote(path)} && printf %s ${shellSingleQuote(
|
|
2293
2308
|
MATERIALIZED_PATH_MARKER,
|
|
2294
2309
|
)}`;
|
|
@@ -2300,18 +2315,68 @@ async function assertProviderCanSeeMaterializedPath(
|
|
|
2300
2315
|
tty: false,
|
|
2301
2316
|
};
|
|
2302
2317
|
let output: string;
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
} else if (session.exec) {
|
|
2306
|
-
output = formatExecResult(await session.exec(args));
|
|
2307
|
-
} else {
|
|
2318
|
+
let result: unknown;
|
|
2319
|
+
if (!session.execCommand && !session.exec) {
|
|
2308
2320
|
throw new RoutingUnsupportedError("materializeEntry.verify", "unknown");
|
|
2309
2321
|
}
|
|
2310
|
-
|
|
2311
|
-
|
|
2312
|
-
|
|
2313
|
-
|
|
2322
|
+
try {
|
|
2323
|
+
result = session.execCommand ? await session.execCommand(args) : await session.exec!(args);
|
|
2324
|
+
} catch (cause) {
|
|
2325
|
+
retainMaterializationVerificationDiagnostic(cause, {
|
|
2326
|
+
reason: "command_error",
|
|
2327
|
+
path,
|
|
2328
|
+
workdir: args.workdir,
|
|
2329
|
+
command,
|
|
2330
|
+
output: null,
|
|
2331
|
+
exitCode: null,
|
|
2332
|
+
providerSessionId: null,
|
|
2333
|
+
causeMessage: cause instanceof Error ? cause.message : String(cause),
|
|
2334
|
+
});
|
|
2335
|
+
throw cause;
|
|
2336
|
+
}
|
|
2337
|
+
try {
|
|
2338
|
+
output = formatExecResult(result);
|
|
2339
|
+
} catch (cause) {
|
|
2340
|
+
const record = result && typeof result === "object" ? (result as Record<string, unknown>) : {};
|
|
2341
|
+
const returnedOutput = [record.output, record.stderr, record.stdout]
|
|
2342
|
+
.filter((value): value is string => typeof value === "string")
|
|
2343
|
+
.join("\n");
|
|
2344
|
+
throw new SandboxMaterializationVerificationError({
|
|
2345
|
+
reason: "invalid_response",
|
|
2346
|
+
path,
|
|
2347
|
+
workdir: args.workdir,
|
|
2348
|
+
command,
|
|
2349
|
+
output: returnedOutput || null,
|
|
2350
|
+
exitCode: null,
|
|
2351
|
+
providerSessionId: null,
|
|
2352
|
+
causeMessage: cause instanceof Error ? cause.message : String(cause),
|
|
2353
|
+
});
|
|
2354
|
+
}
|
|
2355
|
+
const status = parseExecResponseBanner(output);
|
|
2356
|
+
const markerPresent = stripExecBanner(output).includes(MATERIALIZED_PATH_MARKER);
|
|
2357
|
+
if (
|
|
2358
|
+
markerPresent &&
|
|
2359
|
+
(status.kind === "absent" || (status.kind === "exited" && status.exitCode === 0))
|
|
2360
|
+
) {
|
|
2361
|
+
return;
|
|
2314
2362
|
}
|
|
2363
|
+
const reason: MaterializationFailureReason =
|
|
2364
|
+
status.kind === "running"
|
|
2365
|
+
? "command_pending"
|
|
2366
|
+
: status.kind === "exited" && status.exitCode === 1
|
|
2367
|
+
? "path_not_visible"
|
|
2368
|
+
: status.kind === "exited" && status.exitCode !== 0
|
|
2369
|
+
? "command_failed"
|
|
2370
|
+
: "invalid_response";
|
|
2371
|
+
throw new SandboxMaterializationVerificationError({
|
|
2372
|
+
reason,
|
|
2373
|
+
path,
|
|
2374
|
+
workdir: args.workdir,
|
|
2375
|
+
command,
|
|
2376
|
+
output,
|
|
2377
|
+
exitCode: status.kind === "exited" ? status.exitCode : null,
|
|
2378
|
+
providerSessionId: status.kind === "running" ? status.sessionId : null,
|
|
2379
|
+
});
|
|
2315
2380
|
}
|
|
2316
2381
|
|
|
2317
2382
|
function providerManifestRoot(session: RoutableBackendSession): string | null {
|