@opengeni/runtime 0.2.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.
Files changed (65) hide show
  1. package/dist/chunk-2PO56VAL.js +3478 -0
  2. package/dist/chunk-2PO56VAL.js.map +1 -0
  3. package/dist/index.d.ts +912 -0
  4. package/dist/index.js +3663 -0
  5. package/dist/index.js.map +1 -0
  6. package/dist/sandbox/index.d.ts +1738 -0
  7. package/dist/sandbox/index.js +187 -0
  8. package/dist/sandbox/index.js.map +1 -0
  9. package/package.json +49 -0
  10. package/src/bundled_hashicorp_terraform_skills/LICENSE +373 -0
  11. package/src/bundled_hashicorp_terraform_skills/README.md +18 -0
  12. package/src/bundled_hashicorp_terraform_skills/UPSTREAM_GIT_SHA +1 -0
  13. package/src/bundled_hashicorp_terraform_skills/azure-verified-modules/SKILL.md +613 -0
  14. package/src/bundled_hashicorp_terraform_skills/checkov/SKILL.md +43 -0
  15. package/src/bundled_hashicorp_terraform_skills/refactor-module/SKILL.md +538 -0
  16. package/src/bundled_hashicorp_terraform_skills/social-media-marketing/SKILL.md +35 -0
  17. package/src/bundled_hashicorp_terraform_skills/terraform-search-import/SKILL.md +372 -0
  18. package/src/bundled_hashicorp_terraform_skills/terraform-search-import/references/MANUAL-IMPORT.md +113 -0
  19. package/src/bundled_hashicorp_terraform_skills/terraform-search-import/scripts/list_resources.sh +38 -0
  20. package/src/bundled_hashicorp_terraform_skills/terraform-stacks/SKILL.md +480 -0
  21. package/src/bundled_hashicorp_terraform_skills/terraform-stacks/references/api-monitoring.md +543 -0
  22. package/src/bundled_hashicorp_terraform_skills/terraform-stacks/references/component-blocks.md +476 -0
  23. package/src/bundled_hashicorp_terraform_skills/terraform-stacks/references/deployment-blocks.md +391 -0
  24. package/src/bundled_hashicorp_terraform_skills/terraform-stacks/references/examples.md +1529 -0
  25. package/src/bundled_hashicorp_terraform_skills/terraform-stacks/references/linked-stacks.md +187 -0
  26. package/src/bundled_hashicorp_terraform_skills/terraform-stacks/references/troubleshooting.md +671 -0
  27. package/src/bundled_hashicorp_terraform_skills/terraform-style-guide/SKILL.md +353 -0
  28. package/src/bundled_hashicorp_terraform_skills/terraform-test/SKILL.md +451 -0
  29. package/src/bundled_hashicorp_terraform_skills/terraform-test/references/CI_CD.md +80 -0
  30. package/src/bundled_hashicorp_terraform_skills/terraform-test/references/EXAMPLES.md +314 -0
  31. package/src/bundled_hashicorp_terraform_skills/terraform-test/references/MOCK_PROVIDERS.md +171 -0
  32. package/src/codex-tool-search.ts +267 -0
  33. package/src/context-compaction.ts +538 -0
  34. package/src/history-sanitizer.ts +719 -0
  35. package/src/index.ts +3299 -0
  36. package/src/sandbox/capabilities.ts +69 -0
  37. package/src/sandbox/channel-a.ts +1031 -0
  38. package/src/sandbox/display-stack.ts +231 -0
  39. package/src/sandbox/errors.ts +34 -0
  40. package/src/sandbox/index.ts +832 -0
  41. package/src/sandbox/providers/blaxel.ts +35 -0
  42. package/src/sandbox/providers/cloudflare.ts +24 -0
  43. package/src/sandbox/providers/daytona.ts +34 -0
  44. package/src/sandbox/providers/docker.ts +17 -0
  45. package/src/sandbox/providers/e2b.ts +36 -0
  46. package/src/sandbox/providers/index.ts +107 -0
  47. package/src/sandbox/providers/local.ts +13 -0
  48. package/src/sandbox/providers/modal.ts +55 -0
  49. package/src/sandbox/providers/none.ts +13 -0
  50. package/src/sandbox/providers/runloop.ts +32 -0
  51. package/src/sandbox/providers/selfhosted.ts +96 -0
  52. package/src/sandbox/providers/types.ts +38 -0
  53. package/src/sandbox/providers/vercel.ts +29 -0
  54. package/src/sandbox/recording.ts +286 -0
  55. package/src/sandbox/routing/backend-resolver.ts +189 -0
  56. package/src/sandbox/routing/routing-session.ts +455 -0
  57. package/src/sandbox/select.ts +371 -0
  58. package/src/sandbox/selfhosted/capabilities.ts +255 -0
  59. package/src/sandbox/selfhosted/control-rpc.ts +351 -0
  60. package/src/sandbox/selfhosted/session.ts +930 -0
  61. package/src/sandbox/selfhosted/testing.ts +230 -0
  62. package/src/sandbox/stream-port.ts +185 -0
  63. package/src/sandbox/stream-token.ts +90 -0
  64. package/src/sandbox/terminal-server.ts +203 -0
  65. package/src/sandbox-computer.ts +835 -0
@@ -0,0 +1,35 @@
1
+ import { BlaxelSandboxClient } from "@openai/agents-extensions/sandbox/blaxel";
2
+ import { CAPABILITY_DESCRIPTORS } from "../capabilities";
3
+ import { SandboxConfigError } from "../errors";
4
+ import type { ProviderRegistration } from "./types";
5
+
6
+ export const blaxelProvider: ProviderRegistration = {
7
+ backend: "blaxel",
8
+ descriptor: CAPABILITY_DESCRIPTORS.blaxel,
9
+ validateCredentials(settings) {
10
+ if (!settings.blaxelApiKey) {
11
+ throw new SandboxConfigError("blaxel", "OPENGENI_BLAXEL_API_KEY is required");
12
+ }
13
+ },
14
+ build({ settings, environment }) {
15
+ // Blaxel exposes ports ON DEMAND (the only such backend) — its options take
16
+ // no `exposedPorts: number[]` list; 6080 is resolved at handshake time, so
17
+ // we do NOT pre-declare it here (this is why the factory's 6080-merge is
18
+ // gated on !supportsOnDemandPorts).
19
+ const options: NonNullable<ConstructorParameters<typeof BlaxelSandboxClient>[0]> = {
20
+ apiKey: settings.blaxelApiKey!,
21
+ env: environment,
22
+ };
23
+ if (settings.blaxelImage) options.image = settings.blaxelImage;
24
+ if (settings.blaxelRegion) options.region = settings.blaxelRegion;
25
+ if (settings.blaxelExposedPortPublic !== undefined) {
26
+ options.exposedPortPublic = settings.blaxelExposedPortPublic;
27
+ }
28
+ if (settings.blaxelExposedPortUrlTtlSeconds) {
29
+ options.exposedPortUrlTtlS = settings.blaxelExposedPortUrlTtlSeconds;
30
+ }
31
+ if (settings.blaxelMemoryMb) options.memory = settings.blaxelMemoryMb;
32
+ if (settings.blaxelTtl) options.ttl = settings.blaxelTtl;
33
+ return new BlaxelSandboxClient(options);
34
+ },
35
+ };
@@ -0,0 +1,24 @@
1
+ import { CloudflareSandboxClient } from "@openai/agents-extensions/sandbox/cloudflare";
2
+ import { CAPABILITY_DESCRIPTORS } from "../capabilities";
3
+ import { SandboxConfigError } from "../errors";
4
+ import type { ProviderRegistration } from "./types";
5
+
6
+ export const cloudflareProvider: ProviderRegistration = {
7
+ backend: "cloudflare",
8
+ descriptor: CAPABILITY_DESCRIPTORS.cloudflare,
9
+ validateCredentials(settings) {
10
+ // workerUrl is the addressing root for the Cloudflare Sandbox Worker — there
11
+ // is no construction without it (it is the one non-optional client option).
12
+ if (!settings.cloudflareWorkerUrl) {
13
+ throw new SandboxConfigError("cloudflare", "OPENGENI_CLOUDFLARE_WORKER_URL is required");
14
+ }
15
+ },
16
+ build({ settings, exposedPorts }) {
17
+ const options: NonNullable<ConstructorParameters<typeof CloudflareSandboxClient>[0]> = {
18
+ workerUrl: settings.cloudflareWorkerUrl!,
19
+ exposedPorts,
20
+ };
21
+ if (settings.cloudflareApiKey) options.apiKey = settings.cloudflareApiKey;
22
+ return new CloudflareSandboxClient(options);
23
+ },
24
+ };
@@ -0,0 +1,34 @@
1
+ import { DaytonaSandboxClient } from "@openai/agents-extensions/sandbox/daytona";
2
+ import { CAPABILITY_DESCRIPTORS } from "../capabilities";
3
+ import { SandboxConfigError } from "../errors";
4
+ import type { ProviderRegistration } from "./types";
5
+
6
+ export const daytonaProvider: ProviderRegistration = {
7
+ backend: "daytona",
8
+ descriptor: CAPABILITY_DESCRIPTORS.daytona,
9
+ validateCredentials(settings) {
10
+ if (!settings.daytonaApiKey) {
11
+ throw new SandboxConfigError("daytona", "OPENGENI_DAYTONA_API_KEY is required");
12
+ }
13
+ },
14
+ build({ settings, environment, exposedPorts }) {
15
+ const options: NonNullable<ConstructorParameters<typeof DaytonaSandboxClient>[0]> = {
16
+ apiKey: settings.daytonaApiKey!,
17
+ env: environment,
18
+ exposedPorts,
19
+ };
20
+ if (settings.daytonaApiUrl) options.apiUrl = settings.daytonaApiUrl;
21
+ if (settings.daytonaTarget) options.target = settings.daytonaTarget;
22
+ if (settings.daytonaImage) options.image = settings.daytonaImage;
23
+ if (settings.daytonaSnapshotName) options.sandboxSnapshotName = settings.daytonaSnapshotName;
24
+ // autoStopInterval=0 disables the idle-kill, so forward 0 explicitly.
25
+ if (settings.daytonaAutoStopInterval !== undefined) {
26
+ options.autoStopInterval = settings.daytonaAutoStopInterval;
27
+ }
28
+ if (settings.daytonaTimeoutSeconds) options.timeoutSec = settings.daytonaTimeoutSeconds;
29
+ if (settings.daytonaExposedPortUrlTtlSeconds) {
30
+ options.exposedPortUrlTtlS = settings.daytonaExposedPortUrlTtlSeconds;
31
+ }
32
+ return new DaytonaSandboxClient(options);
33
+ },
34
+ };
@@ -0,0 +1,17 @@
1
+ import { DockerSandboxClient } from "@openai/agents/sandbox/local";
2
+ import { CAPABILITY_DESCRIPTORS } from "../capabilities";
3
+ import type { ProviderRegistration } from "./types";
4
+
5
+ export const dockerProvider: ProviderRegistration = {
6
+ backend: "docker",
7
+ descriptor: CAPABILITY_DESCRIPTORS.docker,
8
+ // Local dev container — no credentials. (The dockerNetwork decoration is
9
+ // applied by the factory, not here: it wraps the constructed client.)
10
+ validateCredentials() {},
11
+ build({ settings, exposedPorts }) {
12
+ return new DockerSandboxClient({
13
+ image: settings.dockerImage,
14
+ exposedPorts,
15
+ });
16
+ },
17
+ };
@@ -0,0 +1,36 @@
1
+ import { E2BSandboxClient } from "@openai/agents-extensions/sandbox/e2b";
2
+ import { CAPABILITY_DESCRIPTORS } from "../capabilities";
3
+ import { SandboxConfigError } from "../errors";
4
+ import type { ProviderRegistration } from "./types";
5
+
6
+ export const e2bProvider: ProviderRegistration = {
7
+ backend: "e2b",
8
+ descriptor: CAPABILITY_DESCRIPTORS.e2b,
9
+ validateCredentials(settings) {
10
+ // The underlying e2b SDK reads E2B_API_KEY from process.env; we mirror it to
11
+ // settings so a misconfigured deployment fails fast here instead of deep in
12
+ // the SDK at create() time.
13
+ if (!settings.e2bApiKey) {
14
+ throw new SandboxConfigError("e2b", "OPENGENI_E2B_API_KEY is required");
15
+ }
16
+ },
17
+ build({ settings, environment, exposedPorts }) {
18
+ const options: NonNullable<ConstructorParameters<typeof E2BSandboxClient>[0]> = {
19
+ env: environment,
20
+ exposedPorts,
21
+ };
22
+ if (settings.e2bTemplate) options.template = settings.e2bTemplate;
23
+ // e2b's `timeout` is in SECONDS (the SDK multiplies by 1000 internally) —
24
+ // a different unit from Modal's `timeoutMs`.
25
+ if (settings.e2bTimeoutSeconds) options.timeout = settings.e2bTimeoutSeconds;
26
+ if (settings.e2bTimeoutAction) options.timeoutAction = settings.e2bTimeoutAction;
27
+ if (settings.e2bAllowInternetAccess !== undefined) {
28
+ options.allowInternetAccess = settings.e2bAllowInternetAccess;
29
+ }
30
+ if (settings.e2bAutoResume !== undefined) options.autoResume = settings.e2bAutoResume;
31
+ if (settings.e2bWorkspacePersistence) {
32
+ options.workspacePersistence = settings.e2bWorkspacePersistence;
33
+ }
34
+ return new E2BSandboxClient(options);
35
+ },
36
+ };
@@ -0,0 +1,107 @@
1
+ // The provider registry — PROVIDER_REGISTRY maps each SandboxBackend to its
2
+ // ProviderRegistration. This is the data structure createSandboxClient drives
3
+ // (replacing the old flat if/else chain). The module also owns the
4
+ // descriptor.backendId === SDK client.backendId assertion (deferred from P0.1):
5
+ // it must construct the real SDK clients, so it lives here rather than in the
6
+ // contracts-only capabilities self-test.
7
+
8
+ import type { Settings } from "@opengeni/config";
9
+ import { SandboxBackend } from "@opengeni/contracts";
10
+ import { assertDescriptorRegistryInvariants } from "../capabilities";
11
+ import { blaxelProvider } from "./blaxel";
12
+ import { cloudflareProvider } from "./cloudflare";
13
+ import { daytonaProvider } from "./daytona";
14
+ import { dockerProvider } from "./docker";
15
+ import { e2bProvider } from "./e2b";
16
+ import { localProvider } from "./local";
17
+ import { modalProvider } from "./modal";
18
+ import { noneProvider } from "./none";
19
+ import { runloopProvider } from "./runloop";
20
+ import { selfhostedProvider } from "./selfhosted";
21
+ import type { ProviderRegistration } from "./types";
22
+ import { vercelProvider } from "./vercel";
23
+
24
+ export const PROVIDER_REGISTRY: Record<SandboxBackend, ProviderRegistration> = {
25
+ docker: dockerProvider,
26
+ modal: modalProvider,
27
+ local: localProvider,
28
+ none: noneProvider,
29
+ daytona: daytonaProvider,
30
+ runloop: runloopProvider,
31
+ e2b: e2bProvider,
32
+ blaxel: blaxelProvider,
33
+ cloudflare: cloudflareProvider,
34
+ vercel: vercelProvider,
35
+ selfhosted: selfhostedProvider,
36
+ };
37
+
38
+ // Stub settings carrying every per-provider credential, used ONLY by the
39
+ // boot-time backendId assertion to construct each client without tripping
40
+ // validateCredentials. The SDK client constructors are pure option-stores (the
41
+ // underlying provider SDK is required lazily at create()/resume() time, never at
42
+ // construction — verified against @openai/agents-extensions 0.11.6), so this is
43
+ // safe with no provider peer dep installed and no network.
44
+ const ASSERTION_STUB_SETTINGS = {
45
+ dockerImage: "opengeni-sandbox:local",
46
+ modalAppName: "opengeni-sandbox",
47
+ modalTimeoutSeconds: 900,
48
+ daytonaApiKey: "stub",
49
+ runloopApiKey: "stub",
50
+ runloopTunnel: true,
51
+ e2bApiKey: "stub",
52
+ blaxelApiKey: "stub",
53
+ cloudflareWorkerUrl: "https://stub.example.com",
54
+ vercelToken: "stub",
55
+ vercelProjectId: "stub",
56
+ } as unknown as Settings;
57
+
58
+ /**
59
+ * Assert the descriptor table AND that each registered provider's SDK client
60
+ * reports the backendId its descriptor claims. The latter is the
61
+ * deferred-from-P0.1 invariant — it can only run here because it constructs the
62
+ * real clients. Called once at registry build (and from a unit test).
63
+ */
64
+ export function assertProviderRegistryInvariants(): void {
65
+ assertDescriptorRegistryInvariants();
66
+ for (const backend of SandboxBackend.options) {
67
+ const registration = PROVIDER_REGISTRY[backend];
68
+ if (registration.backend !== backend) {
69
+ throw new Error(`PROVIDER_REGISTRY["${backend}"].backend mismatch (got "${registration.backend}")`);
70
+ }
71
+ if (registration.descriptor.backend !== backend) {
72
+ throw new Error(`PROVIDER_REGISTRY["${backend}"].descriptor.backend mismatch (got "${registration.descriptor.backend}")`);
73
+ }
74
+ if (backend === "none") {
75
+ // "none" has no SDK client (build returns undefined); the descriptor
76
+ // backendId "none" is self-consistent.
77
+ if (registration.descriptor.backendId !== "none") {
78
+ throw new Error(`"none" descriptor.backendId must be "none" (got "${registration.descriptor.backendId}")`);
79
+ }
80
+ continue;
81
+ }
82
+ const client = registration.build({
83
+ settings: ASSERTION_STUB_SETTINGS,
84
+ environment: {},
85
+ exposedPorts: [],
86
+ });
87
+ const sdkBackendId = (client as { backendId?: unknown } | undefined)?.backendId;
88
+ if (typeof sdkBackendId !== "string") {
89
+ throw new Error(`Provider "${backend}" SDK client has no string backendId`);
90
+ }
91
+ if (sdkBackendId !== registration.descriptor.backendId) {
92
+ throw new Error(
93
+ `Provider "${backend}" backendId mismatch: descriptor.backendId="${registration.descriptor.backendId}" but SDK client.backendId="${sdkBackendId}"`,
94
+ );
95
+ }
96
+ }
97
+ }
98
+
99
+ // Boot-validate the registry once at module load: the descriptor-table self-
100
+ // test PLUS the descriptor.backendId === SDK client.backendId assertion (the
101
+ // deferred-from-P0.1 invariant). The SDK client constructors are pure option-
102
+ // stores (no network, no peer-dep require at construction), so this is a cheap,
103
+ // side-effect-free guard that fails fast on any drift between the static matrix
104
+ // and the installed @openai/agents-extensions.
105
+ assertProviderRegistryInvariants();
106
+
107
+ export type { ProviderRegistration, ProviderConstructionContext } from "./types";
@@ -0,0 +1,13 @@
1
+ import { UnixLocalSandboxClient } from "@openai/agents/sandbox/local";
2
+ import { CAPABILITY_DESCRIPTORS } from "../capabilities";
3
+ import type { ProviderRegistration } from "./types";
4
+
5
+ export const localProvider: ProviderRegistration = {
6
+ backend: "local",
7
+ descriptor: CAPABILITY_DESCRIPTORS.local,
8
+ // UnixLocalSandboxClient runs in-process — no credentials, no options.
9
+ validateCredentials() {},
10
+ build() {
11
+ return new UnixLocalSandboxClient();
12
+ },
13
+ };
@@ -0,0 +1,55 @@
1
+ import { ModalImageSelector, ModalSandboxClient } from "@openai/agents-extensions/sandbox/modal";
2
+ import { effectiveModalIdleTimeoutSeconds } from "@opengeni/config";
3
+ import { CAPABILITY_DESCRIPTORS } from "../capabilities";
4
+ import { SandboxConfigError } from "../errors";
5
+ import type { ProviderRegistration } from "./types";
6
+
7
+ export const modalProvider: ProviderRegistration = {
8
+ backend: "modal",
9
+ descriptor: CAPABILITY_DESCRIPTORS.modal,
10
+ validateCredentials(settings) {
11
+ // both-or-neither (preserves existing validation at config validateSettings).
12
+ if (Boolean(settings.modalTokenId) !== Boolean(settings.modalTokenSecret)) {
13
+ throw new SandboxConfigError(
14
+ "modal",
15
+ "OPENGENI_MODAL_TOKEN_ID and OPENGENI_MODAL_TOKEN_SECRET must both be set or both omitted",
16
+ );
17
+ }
18
+ if (!settings.modalAppName) {
19
+ throw new SandboxConfigError("modal", "OPENGENI_MODAL_APP_NAME is required");
20
+ }
21
+ },
22
+ build({ settings, environment, exposedPorts }) {
23
+ const options: NonNullable<ConstructorParameters<typeof ModalSandboxClient>[0]> = {
24
+ appName: settings.modalAppName,
25
+ timeoutMs: settings.modalTimeoutSeconds * 1000,
26
+ exposedPorts,
27
+ env: environment,
28
+ };
29
+ // gap-fill (module 03 §4.1): these SDK options were previously unmapped.
30
+ // ALWAYS pin idleTimeoutMs (sandbox-file-persistence): an UNSET idle timeout
31
+ // lets the SDK send idleTimeoutSecs=undefined, so Modal applies its short
32
+ // server-default idle-reap and kills an idle (between-turns) box LONG before
33
+ // OpenGeni's reaper can resume+snapshot it. effectiveModalIdleTimeoutSeconds
34
+ // defaults this to the hard lifetime so the box survives its full warm window
35
+ // and the reaper — not Modal's idle-reap — governs teardown (and snapshots
36
+ // /workspace first).
37
+ options.idleTimeoutMs = effectiveModalIdleTimeoutSeconds(settings) * 1000;
38
+ if (settings.modalWorkspacePersistence) {
39
+ options.workspacePersistence = settings.modalWorkspacePersistence;
40
+ }
41
+ if (settings.modalImageRef) {
42
+ options.image = ModalImageSelector.fromTag(settings.modalImageRef);
43
+ }
44
+ if (settings.modalTokenId) {
45
+ options.tokenId = settings.modalTokenId;
46
+ }
47
+ if (settings.modalTokenSecret) {
48
+ options.tokenSecret = settings.modalTokenSecret;
49
+ }
50
+ if (settings.modalEnvironment) {
51
+ options.environment = settings.modalEnvironment;
52
+ }
53
+ return new ModalSandboxClient(options);
54
+ },
55
+ };
@@ -0,0 +1,13 @@
1
+ import { CAPABILITY_DESCRIPTORS } from "../capabilities";
2
+ import type { ProviderRegistration } from "./types";
3
+
4
+ export const noneProvider: ProviderRegistration = {
5
+ backend: "none",
6
+ descriptor: CAPABILITY_DESCRIPTORS.none,
7
+ // No sandbox: nothing to validate, and build() returns undefined. The factory
8
+ // short-circuits on "none" before calling build, but we keep build honest.
9
+ validateCredentials() {},
10
+ build() {
11
+ return undefined;
12
+ },
13
+ };
@@ -0,0 +1,32 @@
1
+ import { RunloopSandboxClient } from "@openai/agents-extensions/sandbox/runloop";
2
+ import { CAPABILITY_DESCRIPTORS } from "../capabilities";
3
+ import { SandboxConfigError } from "../errors";
4
+ import type { ProviderRegistration } from "./types";
5
+
6
+ export const runloopProvider: ProviderRegistration = {
7
+ backend: "runloop",
8
+ descriptor: CAPABILITY_DESCRIPTORS.runloop,
9
+ validateCredentials(settings) {
10
+ if (!settings.runloopApiKey) {
11
+ throw new SandboxConfigError("runloop", "OPENGENI_RUNLOOP_API_KEY is required");
12
+ }
13
+ },
14
+ build({ settings, environment, exposedPorts }) {
15
+ const options: NonNullable<ConstructorParameters<typeof RunloopSandboxClient>[0]> = {
16
+ apiKey: settings.runloopApiKey!,
17
+ env: environment,
18
+ exposedPorts,
19
+ // Tunnel v2: one tunnel for all ports. Defaults to true in our config.
20
+ tunnel: settings.runloopTunnel,
21
+ };
22
+ if (settings.runloopBaseUrl) options.baseUrl = settings.runloopBaseUrl;
23
+ if (settings.runloopBlueprintName) options.blueprintName = settings.runloopBlueprintName;
24
+ if (settings.runloopBlueprintId) options.blueprintId = settings.runloopBlueprintId;
25
+ // Runloop's keep-alive lives under the timeouts bag (keepAliveTimeoutMs),
26
+ // NOT a top-level field — the units differ from Modal's idleTimeoutMs.
27
+ if (settings.runloopKeepAliveSeconds) {
28
+ options.timeouts = { keepAliveTimeoutMs: settings.runloopKeepAliveSeconds * 1000 };
29
+ }
30
+ return new RunloopSandboxClient(options);
31
+ },
32
+ };
@@ -0,0 +1,96 @@
1
+ // Bring-your-own-compute: the user's own machine, enrolled via the Rust agent,
2
+ // is reached over the NATS request/reply control plane (the agent subscribes to
3
+ // `agent.<workspace>.<agentId>.rpc`; the subject IS the registry). There is NO
4
+ // provider SDK and NO per-box credential — "the agent is the box".
5
+ //
6
+ // M3 ships the REAL `SelfhostedSandboxClient`: its `create()`/`resume()` return a
7
+ // `SelfhostedSession` presenting the structural surface (`exec`/`readFile`/
8
+ // `writeFile`/`resolveExposedPort`/`serializeSessionState`) that Channel-A, the
9
+ // viewer, and computer-use consume unchanged — backed by a `ControlRpc` seam
10
+ // (request/reply encoded via `@opengeni/agent-proto`) instead of a provider SDK.
11
+ // `serializeSessionState`/`deserializeSessionState` round-trip `{agentId}` ONLY:
12
+ // resume = re-address the live subject, never a cold re-create (the machine is
13
+ // not recreatable). The live NATS request/reply transport + Accounts land in M4
14
+ // behind the SAME `ControlRpc`.
15
+
16
+ import type { Settings } from "@opengeni/config";
17
+ import { CAPABILITY_DESCRIPTORS } from "../capabilities";
18
+ import {
19
+ NatsControlRpc,
20
+ type ControlRpc,
21
+ type NatsRequestConnection,
22
+ } from "../selfhosted/control-rpc";
23
+ import {
24
+ SelfhostedSandboxClient,
25
+ type SelfhostedRelayConfig,
26
+ } from "../selfhosted/session";
27
+ import type { ProviderRegistration } from "./types";
28
+
29
+ /**
30
+ * Resolve the relay-URL shape config from settings. M8b threads the real relay
31
+ * deployment URL (`OPENGENI_SELFHOSTED_RELAY_URL`, ops-repo IaC) behind this seam:
32
+ * `resolveExposedPort` returns `{host, port, tls, path, query}` so `buildStreamUrl`
33
+ * assembles the relay dial URL. A path-less URL defaults to the relay's `/stream`
34
+ * route; an unconfigured deployment falls back to a placeholder host (the URL shape
35
+ * is still well-formed; the relay is simply unreachable until configured).
36
+ */
37
+ function resolveRelayConfig(settings: Settings): SelfhostedRelayConfig {
38
+ const raw = settings.selfhostedRelayUrl?.trim();
39
+ if (!raw) {
40
+ return { host: "relay.opengeni.local", port: 443, tls: true, path: "/stream" };
41
+ }
42
+ try {
43
+ const url = new URL(raw.includes("://") ? raw : `wss://${raw}`);
44
+ const tls = url.protocol === "wss:" || url.protocol === "https:";
45
+ const port = url.port ? Number(url.port) : tls ? 443 : 80;
46
+ const path = url.pathname && url.pathname !== "/" ? url.pathname : "/stream";
47
+ return { host: url.hostname, port, tls, path };
48
+ } catch {
49
+ return { host: raw, port: 443, tls: true, path: "/stream" };
50
+ }
51
+ }
52
+
53
+ /**
54
+ * The default `ControlRpc` factory for a registry-built client: a
55
+ * `NatsControlRpc` whose connection factory returns null — there is NO live NATS
56
+ * connection wired into the agent-loop-free runtime leaf at build() time (the
57
+ * API/worker inject the live `@opengeni/events` connection per request in M4).
58
+ * A null connection surfaces `agent_offline` on every op rather than throwing at
59
+ * construction, so boot never requires a live NATS — exactly the M3 ruling.
60
+ */
61
+ function defaultControlRpcFactory(): ControlRpc {
62
+ return new NatsControlRpc(async (): Promise<NatsRequestConnection | null> => null);
63
+ }
64
+
65
+ export const selfhostedProvider: ProviderRegistration = {
66
+ backend: "selfhosted",
67
+ descriptor: CAPABILITY_DESCRIPTORS.selfhosted,
68
+ /**
69
+ * No per-box credentials: the machine is reached over the agent's own
70
+ * enrollment. The enrollment-signing + relay-token secrets are deployment-level
71
+ * config that lands with the connectivity/enrollment milestones (M4/M5) — and
72
+ * the whole feature is gated by a `sandboxSelfhostedEnabled` flag (default off)
73
+ * that does not yet exist in Settings. So validation is LENIENT (no-op) in M3:
74
+ * boot must never break, and there is nothing per-box to validate. M4/M5 add
75
+ * the (flag-gated) signing/relay presence checks here behind the same seam.
76
+ */
77
+ validateCredentials() {},
78
+ /**
79
+ * Build the registry client. `create()`/`resume()` bind a `SelfhostedSession`
80
+ * to the agent subject; the per-request `{workspaceId, agentId, controlRpc}`
81
+ * are supplied by the resume path (the lease's enrollment) — the registry
82
+ * client carries the relay config + the default (offline-until-M4) ControlRpc
83
+ * factory and a backendId-correct surface for `assertProviderRegistryInvariants`.
84
+ */
85
+ build({ settings }) {
86
+ return new SelfhostedSandboxClient({
87
+ // The workspaceId is bound per-request by the resume path (the API/worker
88
+ // construct a request-scoped client with the lease's workspace + a live
89
+ // ControlRpc). The registry-built client is the boot/assertion shape; an
90
+ // empty workspaceId is fine until a session is bound with a real one.
91
+ workspaceId: "",
92
+ relay: resolveRelayConfig(settings),
93
+ controlRpcFactory: defaultControlRpcFactory,
94
+ });
95
+ },
96
+ };
@@ -0,0 +1,38 @@
1
+ // The uniform per-provider registration shape (module 03 §3.1).
2
+ //
3
+ // One file per provider implements ProviderRegistration; PROVIDER_REGISTRY maps
4
+ // each SandboxBackend to its registration. This replaces the flat if/else chain
5
+ // that createSandboxClient used to be.
6
+
7
+ import type { Settings } from "@opengeni/config";
8
+ import type { CapabilityDescriptor, SandboxBackend } from "@opengeni/contracts";
9
+
10
+ export interface ProviderConstructionContext {
11
+ settings: Settings;
12
+ /** The env map for the box (collectSandboxEnvironment / per-run environment). */
13
+ environment: Record<string, string>;
14
+ /**
15
+ * Parsed exposed ports (config string -> number[]); already includes the
16
+ * desktop stream port (6080) when this is a desktop tier with desktop enabled
17
+ * and the provider cannot expose ports on demand (the merge happens in
18
+ * createSandboxClient before build()).
19
+ */
20
+ exposedPorts: number[];
21
+ }
22
+
23
+ export interface ProviderRegistration {
24
+ backend: SandboxBackend;
25
+ descriptor: CapabilityDescriptor;
26
+ /**
27
+ * Validate that the settings carry the credentials/config this provider
28
+ * REQUIRES. Throw SandboxConfigError on any missing/contradictory field.
29
+ * Pure — no network. Called by both the factory and a deploy-time preflight.
30
+ * The factory calls this before build(), so build() may assume valid settings.
31
+ */
32
+ validateCredentials(settings: Settings): void;
33
+ /**
34
+ * Build the raw SDK SandboxClient. Returns undefined ONLY for "none".
35
+ * The factory calls validateCredentials() first, so build() can assume valid.
36
+ */
37
+ build(ctx: ProviderConstructionContext): unknown;
38
+ }
@@ -0,0 +1,29 @@
1
+ import { VercelSandboxClient } from "@openai/agents-extensions/sandbox/vercel";
2
+ import { CAPABILITY_DESCRIPTORS } from "../capabilities";
3
+ import { SandboxConfigError } from "../errors";
4
+ import type { ProviderRegistration } from "./types";
5
+
6
+ export const vercelProvider: ProviderRegistration = {
7
+ backend: "vercel",
8
+ descriptor: CAPABILITY_DESCRIPTORS.vercel,
9
+ validateCredentials(settings) {
10
+ // Vercel needs the access token + the project/team it scopes to.
11
+ if (!settings.vercelToken) {
12
+ throw new SandboxConfigError("vercel", "OPENGENI_VERCEL_TOKEN is required");
13
+ }
14
+ if (!settings.vercelProjectId) {
15
+ throw new SandboxConfigError("vercel", "OPENGENI_VERCEL_PROJECT_ID is required");
16
+ }
17
+ },
18
+ build({ settings, environment, exposedPorts }) {
19
+ const options: NonNullable<ConstructorParameters<typeof VercelSandboxClient>[0]> = {
20
+ token: settings.vercelToken!,
21
+ projectId: settings.vercelProjectId!,
22
+ env: environment,
23
+ exposedPorts,
24
+ };
25
+ if (settings.vercelTeamId) options.teamId = settings.vercelTeamId;
26
+ if (settings.vercelRuntime) options.runtime = settings.vercelRuntime;
27
+ return new VercelSandboxClient(options);
28
+ },
29
+ };